blob: d1138b5e6ab7d6d89b47229174c8c67373cc3b4d [file]
// Copyright 2026 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "src/developer/debug/zxdb/client/tracepoint_capture.h"
#include <gtest/gtest.h>
#include "src/developer/debug/shared/message_loop.h"
#include "src/developer/debug/zxdb/client/mock_frame.h"
#include "src/developer/debug/zxdb/client/mock_stack_delegate.h"
#include "src/developer/debug/zxdb/client/pretty_stack_manager.h"
#include "src/developer/debug/zxdb/client/remote_api_test.h"
#include "src/developer/debug/zxdb/client/session.h"
#include "src/developer/debug/zxdb/client/stack.h"
#include "src/developer/debug/zxdb/client/thread.h"
#include "src/developer/debug/zxdb/expr/expr_value.h"
#include "src/developer/debug/zxdb/expr/mock_eval_context.h"
#include "src/developer/debug/zxdb/symbols/base_type.h"
#include "src/developer/debug/zxdb/symbols/code_block.h"
#include "src/developer/debug/zxdb/symbols/collection.h"
#include "src/developer/debug/zxdb/symbols/function.h"
#include "src/developer/debug/zxdb/symbols/location.h"
#include "src/developer/debug/zxdb/symbols/symbol_test_parent_setter.h"
#include "src/developer/debug/zxdb/symbols/type_test_support.h"
#include "src/developer/debug/zxdb/symbols/variable.h"
namespace zxdb {
namespace {
class EvalMockFrame : public MockFrame {
public:
using MockFrame::MockFrame;
void set_custom_eval_context(fxl::RefPtr<EvalContext> context) {
eval_context_ = std::move(context);
}
fxl::RefPtr<EvalContext> GetEvalContext() const override {
if (eval_context_)
return eval_context_;
return MockFrame::GetEvalContext();
}
private:
fxl::RefPtr<EvalContext> eval_context_;
};
class TracepointCaptureTest : public RemoteAPITest {};
TEST_F(TracepointCaptureTest, MatchTracepointFrames) {
MockStackDelegate delegate(&session());
Stack stack(&delegate);
delegate.set_stack(&stack);
TracepointSpec spec;
spec.function = "ProcessData";
// Empty stack returns empty vector.
auto matched = MatchTracepointFrames(&stack, spec, FrameMatchMode::kLiveStopTopFrame);
ASSERT_TRUE(matched.ok());
EXPECT_TRUE(matched.value().frames.empty());
auto phys_frame = std::make_unique<MockFrame>(&session(), nullptr, 0x1000, 0x2000,
"my::ns::ProcessData", FileLine("bar.cc", 20));
MockFrame* phys_frame_ptr = phys_frame.get();
auto inline_frame = std::make_unique<MockFrame>(&session(), nullptr, 0x1000, 0x2000,
"my::ns::Helper", FileLine("foo.cc", 10));
MockFrame* inline_frame_ptr = inline_frame.get();
auto caller_frame = std::make_unique<MockFrame>(&session(), nullptr, 0x2000, 0x2020,
"my::ns::ProcessData", FileLine("baz.cc", 30));
MockFrame* caller_frame_ptr = caller_frame.get();
std::vector<std::unique_ptr<Frame>> frames;
frames.push_back(std::move(inline_frame));
frames.push_back(std::move(phys_frame));
frames.push_back(std::move(caller_frame));
stack.SetFramesForTest(std::move(frames), true);
// Exact function suffix match with inline callee traversal at the same PC.
matched = MatchTracepointFrames(&stack, spec, FrameMatchMode::kLiveStopTopFrame);
ASSERT_TRUE(matched.ok());
ASSERT_EQ(matched.value().frames.size(), 1u);
EXPECT_EQ(matched.value().frames[0], phys_frame_ptr);
// kAllMatchingFrames returns all matching frames across the stack.
matched = MatchTracepointFrames(&stack, spec, FrameMatchMode::kAllMatchingFrames);
ASSERT_TRUE(matched.ok());
ASSERT_EQ(matched.value().frames.size(), 2u);
EXPECT_EQ(matched.value().frames[0], phys_frame_ptr);
EXPECT_EQ(matched.value().frames[1], caller_frame_ptr);
// Watch specification matches top frame in kLiveStopTopFrame and rejects kAllMatchingFrames.
TracepointSpec watch_spec;
TracepointWatchSpec watch;
watch.address = 0x2000;
watch_spec.watch = watch;
matched = MatchTracepointFrames(&stack, watch_spec, FrameMatchMode::kLiveStopTopFrame);
ASSERT_TRUE(matched.ok());
ASSERT_EQ(matched.value().frames.size(), 1u);
EXPECT_EQ(matched.value().frames[0], inline_frame_ptr);
matched = MatchTracepointFrames(&stack, watch_spec, FrameMatchMode::kAllMatchingFrames);
EXPECT_TRUE(matched.has_error());
// A special identifier such as $plt(...) does not match a normal function with the same name.
TracepointSpec plt_spec;
plt_spec.function = "$plt(ProcessData)";
matched = MatchTracepointFrames(&stack, plt_spec, FrameMatchMode::kAllMatchingFrames);
ASSERT_TRUE(matched.ok()) << matched.err().msg();
EXPECT_TRUE(matched.value().frames.empty());
}
TEST_F(TracepointCaptureTest, CaptureRegistersAndSummary) {
std::vector<debug::RegisterValue> regs;
debug::RegisterValue reg1;
reg1.id = debug::RegisterID::kX64_rax;
reg1.data = {0x34, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
regs.push_back(reg1);
Location loc(0x1000, FileLine("test.cc", 42), 0, SymbolContext::ForRelativeAddresses());
auto frame = std::make_unique<MockFrame>(&session(), nullptr, loc, 0x2000, 0x2010, regs);
TracepointSpec spec;
spec.function = "TestFunc";
spec.registers = true;
bool done = false;
CaptureTracepointFrame(fxl::WeakPtr<Thread>(), frame->GetWeakPtr(), spec,
[&done](ErrOr<TracepointFrameCapture> result) {
done = true;
ASSERT_TRUE(result.ok());
const auto& cap = result.value();
EXPECT_TRUE(cap.matched);
ASSERT_TRUE(cap.frame.has_value());
EXPECT_EQ(cap.frame->ip, 0x1000u);
EXPECT_EQ(cap.frame->sp, 0x2000u);
ASSERT_TRUE(cap.registers.has_value());
EXPECT_FALSE(cap.registers->empty());
EXPECT_NE(cap.registers->find("rax"), cap.registers->end());
});
loop().RunUntilNoTasks();
EXPECT_TRUE(done);
// Empty cached general registers still complete synchronously without an async round-trip.
auto empty_reg_frame = std::make_unique<MockFrame>(&session(), nullptr, loc, 0x2000, 0x2010,
std::vector<debug::RegisterValue>{});
bool empty_done = false;
CaptureTracepointFrame(fxl::WeakPtr<Thread>(), empty_reg_frame->GetWeakPtr(), spec,
[&empty_done](ErrOr<TracepointFrameCapture> result) {
empty_done = true;
ASSERT_TRUE(result.ok());
ASSERT_TRUE(result.value().registers.has_value());
EXPECT_TRUE(result.value().registers->empty());
});
EXPECT_TRUE(empty_done);
}
TEST_F(TracepointCaptureTest, ConditionEvaluation) {
Location loc(0x1000, FileLine("test.cc", 42), 0, SymbolContext::ForRelativeAddresses());
auto frame = std::make_unique<MockFrame>(&session(), nullptr, loc, 0x2000, 0x2010);
// Condition true: 1 == 1
{
TracepointSpec spec;
spec.condition = "1 == 1";
bool done = false;
CaptureTracepointFrame(fxl::WeakPtr<Thread>(), frame->GetWeakPtr(), spec,
[&done](ErrOr<TracepointFrameCapture> result) {
done = true;
ASSERT_TRUE(result.ok());
EXPECT_TRUE(result.value().matched);
});
loop().RunUntilNoTasks();
EXPECT_TRUE(done);
}
// Condition false: 1 == 2
{
TracepointSpec spec;
spec.condition = "1 == 2";
bool done = false;
CaptureTracepointFrame(fxl::WeakPtr<Thread>(), frame->GetWeakPtr(), spec,
[&done](ErrOr<TracepointFrameCapture> result) {
done = true;
ASSERT_TRUE(result.ok());
EXPECT_FALSE(result.value().matched);
});
loop().RunUntilNoTasks();
EXPECT_TRUE(done);
}
// Condition error: malformed expression
{
TracepointSpec spec;
spec.condition = "(+";
bool done = false;
CaptureTracepointFrame(fxl::WeakPtr<Thread>(), frame->GetWeakPtr(), spec,
[&done](ErrOr<TracepointFrameCapture> result) {
done = true;
ASSERT_TRUE(result.ok());
EXPECT_FALSE(result.value().matched);
EXPECT_FALSE(result.value().errors.empty());
});
loop().RunUntilNoTasks();
EXPECT_TRUE(done);
}
}
TEST_F(TracepointCaptureTest, DuplicateExpressions) {
Location loc(0x1000, FileLine("test.cc", 42), 0, SymbolContext::ForRelativeAddresses());
auto frame = std::make_unique<MockFrame>(&session(), nullptr, loc, 0x2000, 0x2010);
TracepointSpec spec;
spec.expressions = {"10 + 20", "10 + 20", "30 + 40"};
bool done = false;
CaptureTracepointFrame(fxl::WeakPtr<Thread>(), frame->GetWeakPtr(), spec,
[&done](ErrOr<TracepointFrameCapture> result) {
done = true;
ASSERT_TRUE(result.ok());
const auto& cap = result.value();
EXPECT_TRUE(cap.matched);
EXPECT_EQ(cap.evaluations.size(), 2u);
EXPECT_TRUE(cap.evaluations.count("10 + 20"));
EXPECT_TRUE(cap.evaluations.count("30 + 40"));
});
loop().RunUntilNoTasks();
EXPECT_TRUE(done);
}
TEST_F(TracepointCaptureTest, MatchTracepointFramesWithAddress) {
MockStackDelegate delegate(&session());
Stack stack(&delegate);
delegate.set_stack(&stack);
TracepointSpec spec;
spec.address = 0x1000;
// Empty stack returns empty vector.
auto matched = MatchTracepointFrames(&stack, spec, FrameMatchMode::kLiveStopTopFrame);
ASSERT_TRUE(matched.ok());
EXPECT_TRUE(matched.value().frames.empty());
auto frame1 = std::make_unique<MockFrame>(&session(), nullptr, 0x1000, 0x2000, "func1",
FileLine("foo.cc", 10));
MockFrame* frame1_ptr = frame1.get();
auto frame2 = std::make_unique<MockFrame>(&session(), nullptr, 0x2000, 0x2020, "func2",
FileLine("bar.cc", 20));
auto frame3 = std::make_unique<MockFrame>(&session(), nullptr, 0x1000, 0x2040, "func3",
FileLine("baz.cc", 30));
MockFrame* frame3_ptr = frame3.get();
std::vector<std::unique_ptr<Frame>> frames;
frames.push_back(std::move(frame1));
frames.push_back(std::move(frame2));
frames.push_back(std::move(frame3));
stack.SetFramesForTest(std::move(frames), true);
// Top frame matches address 0x1000.
matched = MatchTracepointFrames(&stack, spec, FrameMatchMode::kLiveStopTopFrame);
ASSERT_TRUE(matched.ok());
ASSERT_EQ(matched.value().frames.size(), 1u);
EXPECT_EQ(matched.value().frames[0], frame1_ptr);
// Top frame does not match address 0x9999.
TracepointSpec non_matching_spec;
non_matching_spec.address = 0x9999;
matched = MatchTracepointFrames(&stack, non_matching_spec, FrameMatchMode::kLiveStopTopFrame);
ASSERT_TRUE(matched.ok());
EXPECT_TRUE(matched.value().frames.empty());
// kAllMatchingFrames returns all frames matching address 0x1000.
matched = MatchTracepointFrames(&stack, spec, FrameMatchMode::kAllMatchingFrames);
ASSERT_TRUE(matched.ok());
ASSERT_EQ(matched.value().frames.size(), 2u);
EXPECT_EQ(matched.value().frames[0], frame1_ptr);
EXPECT_EQ(matched.value().frames[1], frame3_ptr);
}
TEST_F(TracepointCaptureTest, MatchTracepointFramesWithFileLine) {
MockStackDelegate delegate(&session());
Stack stack(&delegate);
delegate.set_stack(&stack);
TracepointSpec spec;
spec.file_line = FileLine("target.cc", 42);
// Set up an inline frame and physical frame sharing the top PC (0x1000).
auto frame_inline = std::make_unique<MockFrame>(&session(), nullptr, 0x1000, 0x2000,
"InlinedFunc", FileLine("target.cc", 42));
MockFrame* inline_ptr = frame_inline.get();
auto frame_phys = std::make_unique<MockFrame>(&session(), nullptr, 0x1000, 0x2000, "PhysFunc",
FileLine("other.cc", 10));
auto frame_other = std::make_unique<MockFrame>(&session(), nullptr, 0x2000, 0x2020, "CallerFunc",
FileLine("target.cc", 42));
MockFrame* other_ptr = frame_other.get();
std::vector<std::unique_ptr<Frame>> frames;
frames.push_back(std::move(frame_inline));
frames.push_back(std::move(frame_phys));
frames.push_back(std::move(frame_other));
stack.SetFramesForTest(std::move(frames), true);
// In kLiveStopTopFrame, matches inline frame sharing top IP with matching file_line.
auto matched = MatchTracepointFrames(&stack, spec, FrameMatchMode::kLiveStopTopFrame);
ASSERT_TRUE(matched.ok());
ASSERT_EQ(matched.value().frames.size(), 1u);
EXPECT_EQ(matched.value().frames[0], inline_ptr);
// In kAllMatchingFrames, matches all frames across stack matching file_line.
matched = MatchTracepointFrames(&stack, spec, FrameMatchMode::kAllMatchingFrames);
ASSERT_TRUE(matched.ok());
ASSERT_EQ(matched.value().frames.size(), 2u);
EXPECT_EQ(matched.value().frames[0], inline_ptr);
EXPECT_EQ(matched.value().frames[1], other_ptr);
}
TEST_F(TracepointCaptureTest, CaptureLocals) {
Location loc(0x1000, FileLine("test.cc", 42), 0, SymbolContext::ForRelativeAddresses());
auto frame = std::make_unique<MockFrame>(&session(), nullptr, loc, 0x2000, 0x2010);
TracepointSpec spec;
spec.function = "TestFunc";
spec.locals = true;
bool done = false;
CaptureTracepointFrame(fxl::WeakPtr<Thread>(), frame->GetWeakPtr(), spec,
[&done](ErrOr<TracepointFrameCapture> result) {
done = true;
ASSERT_TRUE(result.ok());
const auto& cap = result.value();
EXPECT_TRUE(cap.matched);
ASSERT_TRUE(cap.locals.has_value());
EXPECT_TRUE(cap.locals->empty());
// A frame without symbols reports why no locals were captured.
ASSERT_EQ(cap.errors.size(), 1u);
EXPECT_EQ(cap.errors[0],
"There is no symbol information for the frame.");
});
loop().RunUntilNoTasks();
EXPECT_TRUE(done);
}
TEST_F(TracepointCaptureTest, CaptureBacktrace) {
constexpr uint64_t kProcessKoid = 1234;
InjectProcessWithModule(kProcessKoid, 0x100000);
constexpr uint64_t kThreadKoid = 5678;
Thread* thread = InjectThread(kProcessKoid, kThreadKoid);
std::vector<std::unique_ptr<Frame>> frames;
frames.push_back(std::make_unique<MockFrame>(&session(), thread, 0x1000, 0x2000, "Func1",
FileLine("foo.cc", 10)));
frames.push_back(std::make_unique<MockFrame>(&session(), thread, 0x2000, 0x2020, "Func2",
FileLine("bar.cc", 20)));
thread->GetStack().SetFramesForTest(std::move(frames), true);
TracepointSpec spec;
spec.function = "Func1";
spec.backtrace = true;
bool done = false;
CaptureTracepointFrame(thread->GetWeakPtr(), thread->GetStack()[0]->GetWeakPtr(), spec,
[&done](ErrOr<TracepointFrameCapture> result) {
done = true;
ASSERT_TRUE(result.ok());
const auto& cap = result.value();
EXPECT_TRUE(cap.matched);
ASSERT_TRUE(cap.backtrace.has_value());
EXPECT_EQ(cap.backtrace->size(), 2u);
EXPECT_EQ((*cap.backtrace)[0].ip, 0x1000u);
EXPECT_EQ((*cap.backtrace)[1].ip, 0x2000u);
});
loop().RunUntilNoTasks();
EXPECT_TRUE(done);
}
TEST_F(TracepointCaptureTest, NonTopmostFrameRegisters) {
constexpr uint64_t kProcessKoid = 1234;
InjectProcessWithModule(kProcessKoid, 0x100000);
constexpr uint64_t kThreadKoid = 5678;
Thread* thread = InjectThread(kProcessKoid, kThreadKoid);
std::vector<debug::RegisterValue> regs;
debug::RegisterValue reg1;
reg1.id = debug::RegisterID::kX64_rax;
reg1.data = {0x78, 0x56, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00};
regs.push_back(reg1);
std::vector<std::unique_ptr<Frame>> frames;
frames.push_back(std::make_unique<MockFrame>(&session(), thread, 0x1000, 0x2000, "TopFunc",
FileLine("foo.cc", 10)));
frames.push_back(std::make_unique<MockFrame>(
&session(), thread,
Location(0x2000, FileLine("bar.cc", 20), 0, SymbolContext::ForRelativeAddresses()), 0x3000,
0x3010, regs));
thread->GetStack().SetFramesForTest(std::move(frames), true);
TracepointSpec spec;
spec.registers = true;
bool done = false;
CaptureTracepointFrame(thread->GetWeakPtr(), thread->GetStack()[1]->GetWeakPtr(), spec,
[&done](ErrOr<TracepointFrameCapture> result) {
done = true;
ASSERT_TRUE(result.ok());
const auto& cap = result.value();
EXPECT_TRUE(cap.matched);
ASSERT_TRUE(cap.registers.has_value());
EXPECT_EQ(cap.registers->at("rax"), "0x12345678");
EXPECT_TRUE(cap.errors.empty());
});
loop().RunUntilNoTasks();
EXPECT_TRUE(done);
}
TEST_F(TracepointCaptureTest, RawBacktraceImpliesBacktrace) {
constexpr uint64_t kProcessKoid = 1234;
InjectProcessWithModule(kProcessKoid, 0x100000);
constexpr uint64_t kThreadKoid = 5678;
Thread* thread = InjectThread(kProcessKoid, kThreadKoid);
std::vector<std::unique_ptr<Frame>> frames;
frames.push_back(std::make_unique<MockFrame>(&session(), thread, 0x1000, 0x2000, "Func1",
FileLine("foo.cc", 10)));
frames.push_back(std::make_unique<MockFrame>(&session(), thread, 0x2000, 0x2020, "Func2",
FileLine("bar.cc", 20)));
thread->GetStack().SetFramesForTest(std::move(frames), true);
TracepointSpec spec;
spec.function = "Func1";
spec.raw_backtrace = true;
spec.backtrace = false;
bool done = false;
CaptureTracepointFrame(thread->GetWeakPtr(), thread->GetStack()[0]->GetWeakPtr(), spec,
[&done](ErrOr<TracepointFrameCapture> result) {
done = true;
ASSERT_TRUE(result.ok());
const auto& cap = result.value();
EXPECT_TRUE(cap.matched);
ASSERT_TRUE(cap.backtrace.has_value());
EXPECT_EQ(cap.backtrace->size(), 2u);
});
loop().RunUntilNoTasks();
EXPECT_TRUE(done);
}
TEST_F(TracepointCaptureTest, TracepointSpecValidate) {
// Empty spec -> error.
{
TracepointSpec spec;
EXPECT_TRUE(spec.Validate().has_error());
}
// Mutually exclusive target fields set simultaneously -> error.
{
TracepointSpec spec;
spec.function = "MyFunc";
spec.file_line = FileLine("foo.cc", 10);
EXPECT_TRUE(spec.Validate().has_error());
}
{
TracepointSpec spec;
spec.function = "MyFunc";
spec.address = 0x1000;
EXPECT_TRUE(spec.Validate().has_error());
}
{
TracepointSpec spec;
spec.file_line = FileLine("foo.cc", 10);
TracepointWatchSpec watch;
watch.address = 0x2000;
spec.watch = watch;
EXPECT_TRUE(spec.Validate().has_error());
}
{
TracepointSpec spec;
spec.address = 0x1000;
TracepointWatchSpec watch;
watch.address = 0x2000;
spec.watch = watch;
EXPECT_TRUE(spec.Validate().has_error());
}
// file_line with empty file string or invalid FileLine -> error.
{
TracepointSpec spec;
spec.file_line = FileLine();
EXPECT_TRUE(spec.Validate().has_error());
}
{
TracepointSpec spec;
spec.file_line = FileLine("", 0);
EXPECT_TRUE(spec.Validate().has_error());
}
// watch without a resolved address -> error even when expression is set.
{
TracepointSpec spec;
TracepointWatchSpec watch;
watch.expression = "";
watch.address = std::nullopt;
spec.watch = watch;
EXPECT_TRUE(spec.Validate().has_error());
}
{
TracepointSpec spec;
TracepointWatchSpec watch;
watch.expression = "x";
watch.address = std::nullopt;
spec.watch = watch;
EXPECT_TRUE(spec.Validate().has_error());
}
// watch with unsupported access_type -> error.
{
TracepointSpec spec;
TracepointWatchSpec watch;
watch.address = 0x1000;
watch.access_type = debug_ipc::BreakpointType::kSoftware;
spec.watch = watch;
EXPECT_TRUE(spec.Validate().has_error());
}
{
TracepointSpec spec;
TracepointWatchSpec watch;
watch.address = 0x1000;
watch.expression = "x";
watch.access_type = debug_ipc::BreakpointType::kHardware;
spec.watch = watch;
EXPECT_TRUE(spec.Validate().has_error());
}
// Valid specs -> ok.
{
TracepointSpec spec;
spec.function = "MyFunc";
EXPECT_TRUE(spec.Validate().ok());
}
{
TracepointSpec spec;
spec.file_line = FileLine("foo.cc", 42);
EXPECT_TRUE(spec.Validate().ok());
}
{
TracepointSpec spec;
spec.address = 0x1000;
EXPECT_TRUE(spec.Validate().ok());
}
{
TracepointSpec spec;
TracepointWatchSpec watch;
watch.expression = "x";
watch.address = 0x1000;
watch.access_type = debug_ipc::BreakpointType::kWrite;
spec.watch = watch;
EXPECT_TRUE(spec.Validate().ok());
}
{
TracepointSpec spec;
TracepointWatchSpec watch;
watch.address = 0x2000;
watch.access_type = debug_ipc::BreakpointType::kReadWrite;
spec.watch = watch;
EXPECT_TRUE(spec.Validate().ok());
}
}
TEST_F(TracepointCaptureTest, MatchTracepointFramesAdversarial) {
MockStackDelegate delegate(&session());
Stack stack(&delegate);
delegate.set_stack(&stack);
TracepointSpec spec;
spec.function = "TargetFunc";
// Empty stack returns empty vector without crashing in both modes.
{
auto res1 = MatchTracepointFrames(&stack, spec, FrameMatchMode::kLiveStopTopFrame);
ASSERT_TRUE(res1.ok());
EXPECT_TRUE(res1.value().frames.empty());
auto res2 = MatchTracepointFrames(&stack, spec, FrameMatchMode::kAllMatchingFrames);
ASSERT_TRUE(res2.ok());
EXPECT_TRUE(res2.value().frames.empty());
}
// FrameMatchMode::kLiveStopTopFrame falls back to frame 0 when no inline frame at the top IP
// matches spec.function or spec.file_line, without walking into caller frames at lower IPs.
{
auto frame0 = std::make_unique<MockFrame>(&session(), nullptr, 0x1000, 0x2000, "UnrelatedFunc",
FileLine("unrelated.cc", 10));
MockFrame* frame0_ptr = frame0.get();
auto frame1 = std::make_unique<MockFrame>(&session(), nullptr, 0x2000, 0x2020, "TargetFunc",
FileLine("target.cc", 20));
std::vector<std::unique_ptr<Frame>> frames;
frames.push_back(std::move(frame0));
frames.push_back(std::move(frame1));
stack.SetFramesForTest(std::move(frames), true);
auto res = MatchTracepointFrames(&stack, spec, FrameMatchMode::kLiveStopTopFrame);
ASSERT_TRUE(res.ok());
ASSERT_EQ(res.value().frames.size(), 1u);
EXPECT_EQ(res.value().frames[0], frame0_ptr);
TracepointSpec fl_spec;
fl_spec.file_line = FileLine("target.cc", 20);
auto fl_res = MatchTracepointFrames(&stack, fl_spec, FrameMatchMode::kLiveStopTopFrame);
ASSERT_TRUE(fl_res.ok());
ASSERT_EQ(fl_res.value().frames.size(), 1u);
EXPECT_EQ(fl_res.value().frames[0], frame0_ptr);
}
// Invalid specs (empty, empty function, multiple targets, watch without address) fail validation
// in both matching modes instead of falling through.
{
TracepointSpec empty_spec;
EXPECT_TRUE(
MatchTracepointFrames(&stack, empty_spec, FrameMatchMode::kLiveStopTopFrame).has_error());
EXPECT_TRUE(
MatchTracepointFrames(&stack, empty_spec, FrameMatchMode::kAllMatchingFrames).has_error());
TracepointSpec empty_func_spec;
empty_func_spec.function = "";
EXPECT_TRUE(MatchTracepointFrames(&stack, empty_func_spec, FrameMatchMode::kLiveStopTopFrame)
.has_error());
EXPECT_TRUE(MatchTracepointFrames(&stack, empty_func_spec, FrameMatchMode::kAllMatchingFrames)
.has_error());
TracepointSpec multi_target_spec;
multi_target_spec.function = "TargetFunc";
multi_target_spec.address = 0x1000;
EXPECT_TRUE(MatchTracepointFrames(&stack, multi_target_spec, FrameMatchMode::kLiveStopTopFrame)
.has_error());
TracepointSpec unresolved_watch_spec;
TracepointWatchSpec watch;
watch.expression = "x";
unresolved_watch_spec.watch = watch;
EXPECT_TRUE(
MatchTracepointFrames(&stack, unresolved_watch_spec, FrameMatchMode::kLiveStopTopFrame)
.has_error());
}
// FrameMatchMode::kAllMatchingFrames recursion cap: stack with >10 matching frames caps at 10.
{
std::vector<std::unique_ptr<Frame>> frames;
frames.reserve(15);
for (size_t i = 0; i < 15; ++i) {
frames.push_back(std::make_unique<MockFrame>(&session(), nullptr, 0x1000 + (i * 0x10),
0x2000 + (i * 0x20), "RecursiveFunc",
FileLine("recurse.cc", 10)));
}
stack.SetFramesForTest(std::move(frames), true);
TracepointSpec recurse_spec;
recurse_spec.function = "RecursiveFunc";
// With max_matching_frames = std::nullopt (default), returns all 15 frames with truncated ==
// false.
auto res = MatchTracepointFrames(&stack, recurse_spec, FrameMatchMode::kAllMatchingFrames);
ASSERT_TRUE(res.ok());
EXPECT_EQ(res.value().frames.size(), 15u);
EXPECT_FALSE(res.value().truncated);
// With max_matching_frames = 10, returns 10 frames with truncated == true.
recurse_spec.max_matching_frames = 10;
auto capped_res =
MatchTracepointFrames(&stack, recurse_spec, FrameMatchMode::kAllMatchingFrames);
ASSERT_TRUE(capped_res.ok());
EXPECT_EQ(capped_res.value().frames.size(), 10u);
EXPECT_TRUE(capped_res.value().truncated);
// Validate() and MatchTracepointFrames() both reject max_matching_frames = 0.
recurse_spec.max_matching_frames = 0;
EXPECT_TRUE(recurse_spec.Validate().has_error());
EXPECT_TRUE(MatchTracepointFrames(&stack, recurse_spec, FrameMatchMode::kAllMatchingFrames)
.has_error());
EXPECT_TRUE(
MatchTracepointFrames(&stack, recurse_spec, FrameMatchMode::kLiveStopTopFrame).has_error());
}
// Malformed spec.function identifier string (e.g. "foo::(") returns Err cleanly.
{
TracepointSpec bad_spec;
bad_spec.function = "foo::(";
auto res = MatchTracepointFrames(&stack, bad_spec, FrameMatchMode::kLiveStopTopFrame);
EXPECT_TRUE(res.has_error());
}
}
TEST_F(TracepointCaptureTest, CaptureTracepointFrameAdversarial) {
constexpr uint64_t kProcessKoid = 1234;
InjectProcessWithModule(kProcessKoid, 0x100000);
constexpr uint64_t kThreadKoid = 5678;
Thread* thread = InjectThread(kProcessKoid, kThreadKoid);
Location loc(0x1000, FileLine("test.cc", 42), 0, SymbolContext::ForRelativeAddresses());
auto frame = std::make_unique<MockFrame>(&session(), thread, loc, 0x2000, 0x2010);
// Null/invalid weak_frame passed to CaptureTracepointFrame -> returns Err via callback.
{
TracepointSpec spec;
spec.function = "TestFunc";
bool done = false;
CaptureTracepointFrame(thread->GetWeakPtr(), fxl::WeakPtr<Frame>(), spec,
[&done](ErrOr<TracepointFrameCapture> result) {
done = true;
EXPECT_TRUE(result.has_error());
});
loop().RunUntilNoTasks();
EXPECT_TRUE(done);
}
// Condition evaluation failure (syntax error "1 + *" or undefined variable "nonexistent_var"):
// verify callback succeeds with capture.matched == false and errors populated.
{
TracepointSpec spec;
spec.condition = "1 + *";
bool done = false;
CaptureTracepointFrame(thread->GetWeakPtr(), frame->GetWeakPtr(), spec,
[&done](ErrOr<TracepointFrameCapture> result) {
done = true;
ASSERT_TRUE(result.ok());
EXPECT_FALSE(result.value().matched);
EXPECT_FALSE(result.value().errors.empty());
});
loop().RunUntilNoTasks();
EXPECT_TRUE(done);
}
{
TracepointSpec spec;
spec.condition = "nonexistent_var";
bool done = false;
CaptureTracepointFrame(thread->GetWeakPtr(), frame->GetWeakPtr(), spec,
[&done](ErrOr<TracepointFrameCapture> result) {
done = true;
ASSERT_TRUE(result.ok());
EXPECT_FALSE(result.value().matched);
EXPECT_FALSE(result.value().errors.empty());
});
loop().RunUntilNoTasks();
EXPECT_TRUE(done);
}
// Condition returning non-scalar/struct (e.g. (void)0) vs boolean/integer 0 (not matched)
// vs non-zero integer/pointer (matched).
{
TracepointSpec spec;
spec.condition = "(void)0";
bool done = false;
CaptureTracepointFrame(thread->GetWeakPtr(), frame->GetWeakPtr(), spec,
[&done](ErrOr<TracepointFrameCapture> result) {
done = true;
ASSERT_TRUE(result.ok());
EXPECT_FALSE(result.value().matched);
EXPECT_FALSE(result.value().errors.empty());
});
loop().RunUntilNoTasks();
EXPECT_TRUE(done);
}
{
TracepointSpec spec;
spec.condition = "0";
bool done = false;
CaptureTracepointFrame(thread->GetWeakPtr(), frame->GetWeakPtr(), spec,
[&done](ErrOr<TracepointFrameCapture> result) {
done = true;
ASSERT_TRUE(result.ok());
EXPECT_FALSE(result.value().matched);
});
loop().RunUntilNoTasks();
EXPECT_TRUE(done);
}
{
TracepointSpec spec;
spec.condition = "42";
bool done = false;
CaptureTracepointFrame(thread->GetWeakPtr(), frame->GetWeakPtr(), spec,
[&done](ErrOr<TracepointFrameCapture> result) {
done = true;
ASSERT_TRUE(result.ok());
EXPECT_TRUE(result.value().matched);
});
loop().RunUntilNoTasks();
EXPECT_TRUE(done);
}
{
TracepointSpec spec;
spec.condition = "(void*)0x1234";
bool done = false;
CaptureTracepointFrame(thread->GetWeakPtr(), frame->GetWeakPtr(), spec,
[&done](ErrOr<TracepointFrameCapture> result) {
done = true;
ASSERT_TRUE(result.ok());
EXPECT_TRUE(result.value().matched);
});
loop().RunUntilNoTasks();
EXPECT_TRUE(done);
}
// Multiple expressions where one succeeds ("1 + 2") and one fails ("bad_ident"):
// verify both entries exist in evaluations, valid one has result/type, invalid one has error,
// and capture.errors records the error.
{
TracepointSpec spec;
spec.function = "TestFunc";
spec.expressions = {"1 + 2", "bad_ident"};
bool done = false;
CaptureTracepointFrame(thread->GetWeakPtr(), frame->GetWeakPtr(), spec,
[&done](ErrOr<TracepointFrameCapture> result) {
done = true;
ASSERT_TRUE(result.ok());
const auto& cap = result.value();
EXPECT_TRUE(cap.matched);
EXPECT_EQ(cap.evaluations.size(), 2u);
ASSERT_TRUE(cap.evaluations.count("1 + 2"));
EXPECT_EQ(cap.evaluations.at("1 + 2").result, "3");
ASSERT_TRUE(cap.evaluations.at("1 + 2").type.has_value());
EXPECT_FALSE(cap.evaluations.at("1 + 2").type->empty());
ASSERT_TRUE(cap.evaluations.at("bad_ident").error.has_value());
EXPECT_FALSE(cap.evaluations.at("bad_ident").error->empty());
EXPECT_FALSE(cap.errors.empty());
});
loop().RunUntilNoTasks();
EXPECT_TRUE(done);
}
// raw_backtrace = true vs backtrace = true with PrettyStackManager matchers:
// backtrace = true elides matching frames (elided == true, non-empty elide_description),
// whereas raw_backtrace = true captures all frames with elided == false.
{
std::vector<std::unique_ptr<Frame>> frames;
frames.push_back(std::make_unique<MockFrame>(&session(), thread, 0x1000, 0x2000, "TopFunc",
FileLine("top.cc", 10)));
frames.push_back(std::make_unique<MockFrame>(&session(), thread, 0x2000, 0x2020, "Collapse1",
FileLine("c.cc", 20)));
frames.push_back(std::make_unique<MockFrame>(&session(), thread, 0x3000, 0x2040, "Collapse2",
FileLine("c.cc", 30)));
thread->GetStack().SetFramesForTest(std::move(frames), true);
auto pretty_manager = session().system().pretty_stack_manager();
ASSERT_TRUE(pretty_manager);
PrettyStackManager::StackGlob glob(
"CustomCollapse", {PrettyFrameGlob::Func("Collapse1"), PrettyFrameGlob::Func("Collapse2")});
pretty_manager->SetMatchers({glob});
// Case A: backtrace = true -> elision occurs.
{
TracepointSpec spec;
spec.function = "TopFunc";
spec.backtrace = true;
bool done = false;
CaptureTracepointFrame(thread->GetWeakPtr(), thread->GetStack()[0]->GetWeakPtr(), spec,
[&done](ErrOr<TracepointFrameCapture> result) {
done = true;
ASSERT_TRUE(result.ok());
const auto& cap = result.value();
ASSERT_TRUE(cap.backtrace.has_value());
ASSERT_EQ(cap.backtrace->size(), 3u);
EXPECT_FALSE((*cap.backtrace)[0].elided);
EXPECT_FALSE((*cap.backtrace)[1].elided);
EXPECT_EQ((*cap.backtrace)[1].elide_description, "CustomCollapse");
EXPECT_TRUE((*cap.backtrace)[2].elided);
EXPECT_EQ((*cap.backtrace)[2].elide_description, "CustomCollapse");
});
loop().RunUntilNoTasks();
EXPECT_TRUE(done);
}
// Case B: raw_backtrace = true (even with backtrace = false initially) -> no elision.
{
TracepointSpec spec;
spec.function = "TopFunc";
spec.raw_backtrace = true;
spec.backtrace = false;
bool done = false;
CaptureTracepointFrame(thread->GetWeakPtr(), thread->GetStack()[0]->GetWeakPtr(), spec,
[&done](ErrOr<TracepointFrameCapture> result) {
done = true;
ASSERT_TRUE(result.ok());
const auto& cap = result.value();
ASSERT_TRUE(cap.backtrace.has_value());
ASSERT_EQ(cap.backtrace->size(), 3u);
for (const auto& rec : *cap.backtrace) {
EXPECT_FALSE(rec.elided);
EXPECT_TRUE(rec.elide_description.empty());
}
});
loop().RunUntilNoTasks();
EXPECT_TRUE(done);
}
// Case C: backtrace = true for non-top matched frame (stack[2]) matching glob -> not elided.
{
TracepointSpec spec;
spec.function = "Collapse2";
spec.backtrace = true;
bool done = false;
CaptureTracepointFrame(thread->GetWeakPtr(), thread->GetStack()[2]->GetWeakPtr(), spec,
[&done](ErrOr<TracepointFrameCapture> result) {
done = true;
ASSERT_TRUE(result.ok());
const auto& cap = result.value();
ASSERT_TRUE(cap.backtrace.has_value());
ASSERT_EQ(cap.backtrace->size(), 3u);
for (const auto& rec : *cap.backtrace) {
EXPECT_FALSE(rec.elided);
}
});
loop().RunUntilNoTasks();
EXPECT_TRUE(done);
}
}
}
TEST_F(TracepointCaptureTest, CompositeCollectionFormatting) {
constexpr uint64_t kProcessKoid = 1234;
InjectProcessWithModule(kProcessKoid, 0x100000);
constexpr uint64_t kThreadKoid = 5678;
Thread* thread = InjectThread(kProcessKoid, kThreadKoid);
auto function = fxl::MakeRefCounted<Function>(DwarfTag::kSubprogram);
function->set_assigned_name("TestFunc");
function->set_code_ranges(AddressRanges(AddressRange(0x1000, 0x2000)));
auto block = fxl::MakeRefCounted<CodeBlock>(DwarfTag::kLexicalBlock);
block->set_code_ranges(AddressRanges(AddressRange(0x1000, 0x2000)));
SymbolTestParentSetter parent(block, function);
auto int32_type = MakeInt32Type();
auto struct_type =
MakeCollectionType(DwarfTag::kStructureType, "Point", {{"a", int32_type}, {"b", int32_type}});
auto local_var = fxl::MakeRefCounted<Variable>(DwarfTag::kVariable);
local_var->set_assigned_name("pt");
local_var->set_type(struct_type);
block->set_variables({LazySymbol(local_var)});
function->set_inner_blocks({LazySymbol(block)});
Location location(0x1050, FileLine("file.cc", 10), 0, SymbolContext::ForRelativeAddresses(),
function);
auto mock_eval = fxl::MakeRefCounted<MockEvalContext>();
ExprValue struct_val(struct_type, {0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00});
mock_eval->AddVariable(local_var.get(), struct_val);
mock_eval->AddVariable("pt_expr", struct_val);
auto frame = std::make_unique<EvalMockFrame>(&session(), thread, location, 0x2000, 0x2010);
frame->set_custom_eval_context(mock_eval);
TracepointSpec spec;
spec.locals = true;
spec.expressions = {"pt_expr"};
bool done = false;
CaptureTracepointFrame(thread->GetWeakPtr(), frame->GetWeakPtr(), spec,
[&done](ErrOr<TracepointFrameCapture> result) {
done = true;
ASSERT_TRUE(result.ok());
const auto& cap = result.value();
EXPECT_TRUE(cap.matched);
ASSERT_TRUE(cap.locals.has_value());
ASSERT_EQ(cap.locals->count("pt"), 1u);
EXPECT_EQ(cap.locals->at("pt").value, "{a = 1, b = 2}");
EXPECT_EQ(cap.locals->at("pt").type, "Point");
ASSERT_EQ(cap.evaluations.count("pt_expr"), 1u);
EXPECT_EQ(cap.evaluations.at("pt_expr").result, "{a = 1, b = 2}");
EXPECT_EQ(cap.evaluations.at("pt_expr").type, "Point");
});
loop().RunUntilNoTasks();
EXPECT_TRUE(done);
}
TEST_F(TracepointCaptureTest, CaptureThreadBacktraceDirect) {
EXPECT_TRUE(CaptureThreadBacktrace(nullptr).empty());
constexpr uint64_t kProcessKoid = 1234;
InjectProcessWithModule(kProcessKoid, 0x100000);
constexpr uint64_t kThreadKoid = 5678;
Thread* thread = InjectThread(kProcessKoid, kThreadKoid);
std::vector<std::unique_ptr<Frame>> frames;
frames.push_back(std::make_unique<MockFrame>(&session(), thread, 0x1000, 0x2000, "GroupA1",
FileLine("a.cc", 10)));
frames.push_back(std::make_unique<MockFrame>(&session(), thread, 0x2000, 0x2020, "GroupA2",
FileLine("a.cc", 20)));
frames.push_back(std::make_unique<MockFrame>(&session(), thread, 0x3000, 0x2040, "GroupB1",
FileLine("b.cc", 10)));
frames.push_back(std::make_unique<MockFrame>(&session(), thread, 0x4000, 0x2060, "GroupB2",
FileLine("b.cc", 20)));
thread->GetStack().SetFramesForTest(std::move(frames), true);
auto pretty_manager = session().system().pretty_stack_manager();
ASSERT_TRUE(pretty_manager);
pretty_manager->SetMatchers({
PrettyStackManager::StackGlob(
"GroupA", {PrettyFrameGlob::Func("GroupA1"), PrettyFrameGlob::Func("GroupA2")}),
PrettyStackManager::StackGlob(
"GroupB", {PrettyFrameGlob::Func("GroupB1"), PrettyFrameGlob::Func("GroupB2")}),
});
// Without matched frames, both groups elide their trailing frames.
auto default_bt = CaptureThreadBacktrace(thread);
ASSERT_EQ(default_bt.size(), 4u);
EXPECT_FALSE(default_bt[0].elided);
EXPECT_TRUE(default_bt[1].elided);
EXPECT_FALSE(default_bt[2].elided);
EXPECT_TRUE(default_bt[3].elided);
// Passing matched frames from MatchTracepointFrames un-elides only the group containing a match.
TracepointSpec spec;
spec.function = "GroupB2";
auto match_res =
MatchTracepointFrames(&thread->GetStack(), spec, FrameMatchMode::kAllMatchingFrames);
ASSERT_TRUE(match_res.ok());
ASSERT_EQ(match_res.value().frames.size(), 1u);
auto selective_bt =
CaptureThreadBacktrace(thread, /*raw_backtrace=*/false, match_res.value().frames);
ASSERT_EQ(selective_bt.size(), 4u);
EXPECT_FALSE(selective_bt[0].elided);
EXPECT_TRUE(selective_bt[1].elided);
EXPECT_FALSE(selective_bt[2].elided);
EXPECT_FALSE(selective_bt[3].elided);
// Raw backtrace leaves all frames un-elided with empty descriptions.
auto raw_bt = CaptureThreadBacktrace(thread, /*raw_backtrace=*/true);
ASSERT_EQ(raw_bt.size(), 4u);
for (const auto& rec : raw_bt) {
EXPECT_FALSE(rec.elided);
EXPECT_TRUE(rec.elide_description.empty());
}
}
} // namespace
} // namespace zxdb