blob: 3c39bef54c4ac742cc6df60307b7ea5ebb516371 [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/debug_adapter/handlers/request_tracepoint.h"
#include <cstdint>
#include <map>
#include <string>
#include <utility>
#include <vector>
#include <gtest/gtest.h>
#include "src/developer/debug/zxdb/client/session.h"
#include "src/developer/debug/zxdb/client/system.h"
#include "src/developer/debug/zxdb/client/tracepoint.h"
#include "src/developer/debug/zxdb/client/tracepoint_capture.h"
#include "src/developer/debug/zxdb/debug_adapter/context_test.h"
namespace zxdb {
namespace {
class RequestTracepointTest : public DebugAdapterContextTest {
protected:
dap::ResponseOrError<dap::ZxdbSetTracepointsResponse> SendSetTracepoints(
std::vector<dap::ZxdbTracepointSpec> specs) {
dap::ZxdbSetTracepointsRequest req;
req.tracepoints = std::move(specs);
auto response = client().send(req);
context().OnStreamReadable();
loop().RunUntilNoTasks();
RunPendingClientCalls();
return response.get();
}
};
TEST_F(RequestTracepointTest, SetTracepointsRoundTrip) {
InitializeDebugging();
dap::ZxdbTracepointSpec spec;
spec.function = "MyFunction";
spec.max_matching_frames = 5;
spec.max_hits = 10;
spec.enabled = true;
spec.expressions = dap::array<dap::string>{"x", "y"};
spec.locals = true;
spec.registers = true;
auto got = SendSetTracepoints({spec});
EXPECT_FALSE(got.error);
ASSERT_EQ(got.response.tracepoints.size(), 1u);
EXPECT_GT(got.response.tracepoints[0].id, 0);
EXPECT_TRUE(got.response.tracepoints[0].max_matching_frames.has_value());
EXPECT_EQ(*got.response.tracepoints[0].max_matching_frames, 5);
EXPECT_EQ(got.response.tracepoints[0].max_hits, 10);
EXPECT_TRUE(got.response.tracepoints[0].enabled);
EXPECT_FALSE(got.response.tracepoints[0].auto_disabled);
EXPECT_EQ(context().dap_tracepoints().size(), 1u);
EXPECT_EQ(session().system().GetTracepoints().size(), 1u);
auto it = context().dap_tracepoints().find(got.response.tracepoints[0].id);
ASSERT_NE(it, context().dap_tracepoints().end());
ASSERT_TRUE(it->second);
EXPECT_TRUE(it->second->spec().max_matching_frames.has_value());
EXPECT_EQ(*it->second->spec().max_matching_frames, 5u);
}
TEST_F(RequestTracepointTest, ReconciliationAndIdPreservation) {
InitializeDebugging();
dap::ZxdbTracepointSpec spec1;
spec1.id = 10;
spec1.function = "FuncA";
spec1.max_hits = 5;
spec1.enabled = true;
auto got1 = SendSetTracepoints({spec1});
EXPECT_FALSE(got1.error);
ASSERT_EQ(got1.response.tracepoints.size(), 1u);
EXPECT_EQ(got1.response.tracepoints[0].id, 10);
auto it = context().dap_tracepoints().find(10);
ASSERT_NE(it, context().dap_tracepoints().end());
ASSERT_TRUE(it->second);
it->second->set_hit_count(3);
spec1.max_hits = 20;
dap::ZxdbTracepointSpec spec2;
spec2.function = "FuncB";
auto got2 = SendSetTracepoints({spec1, spec2});
EXPECT_FALSE(got2.error);
ASSERT_EQ(got2.response.tracepoints.size(), 2u);
EXPECT_EQ(got2.response.tracepoints[0].id, 10);
EXPECT_EQ(got2.response.tracepoints[0].hit_count, 3);
EXPECT_EQ(got2.response.tracepoints[0].max_hits, 20);
int64_t new_id = got2.response.tracepoints[1].id;
EXPECT_NE(new_id, 10);
EXPECT_EQ(context().dap_tracepoints().size(), 2u);
spec2.id = new_id;
auto got3 = SendSetTracepoints({spec2});
EXPECT_FALSE(got3.error);
ASSERT_EQ(got3.response.tracepoints.size(), 1u);
EXPECT_EQ(got3.response.tracepoints[0].id, new_id);
EXPECT_EQ(context().dap_tracepoints().size(), 1u);
EXPECT_EQ(context().dap_tracepoints().find(10), context().dap_tracepoints().end());
}
TEST_F(RequestTracepointTest, WatchpointResolution) {
InitializeDebugging();
dap::ZxdbTracepointSpec spec;
dap::ZxdbTracepointWatchSpec watch;
watch.data_id = "1234:0x2000:8";
watch.access_type = "readWrite";
spec.watch = watch;
auto got = SendSetTracepoints({spec});
EXPECT_FALSE(got.error);
ASSERT_EQ(got.response.tracepoints.size(), 1u);
ASSERT_EQ(context().dap_tracepoints().size(), 1u);
Tracepoint* tp = context().dap_tracepoints().begin()->second.get();
ASSERT_NE(tp, nullptr);
ASSERT_TRUE(tp->spec().watch.has_value());
EXPECT_EQ(tp->spec().watch->address, 0x2000u);
EXPECT_EQ(tp->spec().watch->byte_size, 8u);
EXPECT_EQ(tp->spec().watch->access_type, debug_ipc::BreakpointType::kReadWrite);
}
TEST_F(RequestTracepointTest, TracepointEventEmissionAndNonDapFiltering) {
InitializeDebugging();
dap::ZxdbTracepointSpec spec;
spec.id = 42;
spec.function = "MyFunction";
auto set_got = SendSetTracepoints({spec});
ASSERT_FALSE(set_got.error);
auto dap_it = context().dap_tracepoints().find(42);
ASSERT_NE(dap_it, context().dap_tracepoints().end());
Tracepoint* dap_tp = dap_it->second.get();
ASSERT_NE(dap_tp, nullptr);
bool event_received = false;
dap::ZxdbTracepointEvent received_event;
client().registerHandler([&](const dap::ZxdbTracepointEvent& evt) {
event_received = true;
received_event = evt;
});
// Console-owned tracepoints absent from dap_tracepoints_ must not emit DAP events.
Tracepoint* console_tp = session().system().CreateNewTracepoint();
TracepointHitRecord console_hit;
console_hit.tracepoint_id = console_tp->id();
context().OnTracepointHit(console_tp, console_hit);
context().OnTracepointHit(nullptr, console_hit);
loop().RunUntilNoTasks();
RunPendingClientCalls();
EXPECT_FALSE(event_received);
TracepointHitRecord hit;
hit.tracepoint_id = dap_tp->id();
hit.location = "src/foo.cc:123";
hit.hit_count = 3;
hit.max_hits = 10;
hit.thread_koid = 9999;
hit.dwell_ms = 1.25;
hit.truncated = true;
hit.capture.frame = TracepointFrameSummary{
.name = "MyFunction", .file = "src/foo.cc", .line = 123, .ip = 0x12345, .sp = 0x54321};
hit.capture.registers = std::map<std::string, std::string>{{"rax", "0x1"}};
hit.capture.locals =
std::map<std::string, TracepointValue>{{"count", {.value = "7", .type = "int"}}};
context().OnTracepointHit(dap_tp, hit);
loop().RunUntilNoTasks();
RunPendingClientCalls();
EXPECT_TRUE(event_received);
EXPECT_EQ(received_event.tracepoint_id, 42);
EXPECT_EQ(received_event.location, "src/foo.cc:123");
EXPECT_EQ(received_event.hit_count, 3);
EXPECT_EQ(received_event.max_hits, 10);
EXPECT_EQ(received_event.thread_koid, 9999);
EXPECT_DOUBLE_EQ(received_event.dwell_ms, 1.25);
EXPECT_TRUE(received_event.truncated);
ASSERT_TRUE(received_event.capture.frame.has_value());
EXPECT_EQ(received_event.capture.frame->name, "MyFunction");
ASSERT_TRUE(received_event.capture.registers.has_value());
EXPECT_NE(received_event.capture.registers->find("rax"), received_event.capture.registers->end());
ASSERT_TRUE(received_event.capture.locals.has_value());
EXPECT_NE(received_event.capture.locals->find("count"), received_event.capture.locals->end());
}
TEST_F(RequestTracepointTest, TracepointBacktraceElisionAndErrorEventEmission) {
InitializeDebugging();
dap::ZxdbTracepointSpec spec;
spec.id = 77;
spec.function = "MyFunction";
auto got_set = SendSetTracepoints({spec});
ASSERT_FALSE(got_set.error);
auto tp_it = context().dap_tracepoints().find(77);
ASSERT_NE(tp_it, context().dap_tracepoints().end());
Tracepoint* tp = tp_it->second.get();
ASSERT_NE(tp, nullptr);
bool event_received = false;
dap::ZxdbTracepointEvent received_event;
client().registerHandler([&](const dap::ZxdbTracepointEvent& evt) {
event_received = true;
received_event = evt;
});
TracepointHitRecord hit;
hit.tracepoint_id = tp->id();
hit.location = "MyFunction";
hit.hit_count = 1;
hit.max_hits = 15;
hit.thread_koid = 8888;
TracepointFrameRecord rec;
rec.frame_index = 0;
rec.ip = 0x2000;
rec.sp = 0x3000;
rec.name = "AsyncExecutor";
rec.elided = true;
rec.elide_description = "2 async executor frames";
hit.capture.backtrace = std::vector<TracepointFrameRecord>{rec};
hit.capture.evaluations["x + 1"] = TracepointEvaluation{.result = "42", .type = "int"};
hit.capture.errors = {"Condition error: bad_ident"};
hit.capture.matched = false;
hit.auto_disabled = true;
context().OnTracepointHit(tp, hit);
loop().RunUntilNoTasks();
RunPendingClientCalls();
EXPECT_TRUE(event_received);
EXPECT_EQ(received_event.tracepoint_id, 77);
EXPECT_EQ(received_event.location, "MyFunction");
EXPECT_EQ(received_event.hit_count, 1);
EXPECT_EQ(received_event.max_hits, 15);
EXPECT_EQ(received_event.thread_koid, 8888);
EXPECT_TRUE(received_event.auto_disabled);
EXPECT_FALSE(received_event.capture.matched);
ASSERT_TRUE(received_event.capture.backtrace.has_value());
ASSERT_EQ(received_event.capture.backtrace->size(), 1u);
EXPECT_TRUE((*received_event.capture.backtrace)[0].elided);
EXPECT_EQ((*received_event.capture.backtrace)[0].elide_description.value(),
"2 async executor frames");
ASSERT_TRUE(received_event.capture.evaluations.has_value());
EXPECT_TRUE(received_event.capture.evaluations->count("x + 1"));
ASSERT_EQ(received_event.capture.errors.size(), 1u);
EXPECT_EQ(received_event.capture.errors[0], "Condition error: bad_ident");
context().WillDestroyTracepoint(tp);
EXPECT_EQ(context().dap_tracepoints().find(77), context().dap_tracepoints().end());
}
TEST_F(RequestTracepointTest, InvalidDataIdInputsReturnUnverifiedInfo) {
InitializeDebugging();
for (const char* bad_id : {"0:0x2000:8", "1234_0x2000_8", "1234:0xffffffffffffffff:16"}) {
dap::ZxdbTracepointSpec spec;
spec.watch = dap::ZxdbTracepointWatchSpec{.data_id = bad_id};
auto got = SendSetTracepoints({spec});
EXPECT_FALSE(got.error) << bad_id;
ASSERT_EQ(got.response.tracepoints.size(), 1u);
EXPECT_FALSE(got.response.tracepoints[0].verified) << bad_id;
EXPECT_TRUE(got.response.tracepoints[0].message.has_value()) << bad_id;
}
}
TEST_F(RequestTracepointTest, NonPositiveRequestIntegersReturnDapError) {
InitializeDebugging();
EXPECT_TRUE(SendSetTracepoints({{.id = 0, .function = "F"}}).error);
EXPECT_TRUE(SendSetTracepoints({{.file = "foo.cc", .line = 0}}).error);
EXPECT_TRUE(SendSetTracepoints({{.function = "F", .max_matching_frames = 0}}).error);
EXPECT_TRUE(SendSetTracepoints({{.function = "F", .max_hits = 0}}).error);
dap::ZxdbTracepointSpec zero_watch;
zero_watch.watch = dap::ZxdbTracepointWatchSpec{.address = 0x1000, .byte_size = 0};
EXPECT_TRUE(SendSetTracepoints({zero_watch}).error);
}
TEST_F(RequestTracepointTest, RawBacktraceImpliesBacktrace) {
InitializeDebugging();
dap::ZxdbTracepointSpec spec;
spec.function = "MyFunction";
spec.raw_backtrace = true;
spec.backtrace = false;
auto got = SendSetTracepoints({spec});
EXPECT_FALSE(got.error);
ASSERT_EQ(got.response.tracepoints.size(), 1u);
ASSERT_EQ(context().dap_tracepoints().size(), 1u);
Tracepoint* tp = context().dap_tracepoints().begin()->second.get();
ASSERT_NE(tp, nullptr);
EXPECT_TRUE(tp->spec().raw_backtrace);
EXPECT_TRUE(tp->spec().backtrace);
}
TEST_F(RequestTracepointTest, UnrecognizedAccessType) {
InitializeDebugging();
dap::ZxdbTracepointSpec sideways_spec;
sideways_spec.watch =
dap::ZxdbTracepointWatchSpec{.data_id = "1234:0x2000:8", .access_type = "sideways"};
auto got_sideways = SendSetTracepoints({sideways_spec});
ASSERT_FALSE(got_sideways.error);
EXPECT_FALSE(got_sideways.response.tracepoints[0].verified);
EXPECT_EQ(got_sideways.response.tracepoints[0].message.value(), "Unknown accessType: sideways");
}
TEST_F(RequestTracepointTest, DeleteDapTracepointsSafeguard) {
InitializeDebugging();
dap::ZxdbTracepointSpec spec1{.id = 1, .function = "Func1"};
dap::ZxdbTracepointSpec spec2{.id = 2, .function = "Func2"};
auto got = SendSetTracepoints({spec1, spec2});
ASSERT_FALSE(got.error);
EXPECT_EQ(context().dap_tracepoints().size(), 2u);
context().DeleteDapTracepoints();
EXPECT_TRUE(context().dap_tracepoints().empty());
}
TEST_F(RequestTracepointTest, DuplicateAndAutoAssignedIdCollision) {
InitializeDebugging();
// Duplicate explicit IDs in the same request fail with a DAP error.
dap::ZxdbTracepointSpec dup1{.id = 5, .function = "FuncA"};
dap::ZxdbTracepointSpec dup2{.id = 5, .function = "FuncB"};
EXPECT_TRUE(SendSetTracepoints({dup1, dup2}).error);
// Auto-assigned ID followed by explicit id 1 in the same request avoids collision.
dap::ZxdbTracepointSpec spec_auto{.function = "FuncAuto"};
dap::ZxdbTracepointSpec spec_explicit{.id = 1, .function = "FuncExplicit"};
auto got = SendSetTracepoints({spec_auto, spec_explicit});
EXPECT_FALSE(got.error);
ASSERT_EQ(got.response.tracepoints.size(), 2u);
EXPECT_EQ(got.response.tracepoints[0].id, 2);
EXPECT_EQ(got.response.tracepoints[1].id, 1);
EXPECT_EQ(context().dap_tracepoints().size(), 2u);
}
TEST_F(RequestTracepointTest, InvalidSpecDeletesOldTracepoint) {
InitializeDebugging();
dap::ZxdbTracepointSpec spec1{.id = 42, .function = "ValidFunction"};
auto got1 = SendSetTracepoints({spec1});
ASSERT_FALSE(got1.error);
EXPECT_EQ(session().system().GetTracepoints().size(), 1u);
dap::ZxdbTracepointSpec invalid_spec{.id = 42, .file = "foo.cc"};
auto got2 = SendSetTracepoints({invalid_spec});
ASSERT_FALSE(got2.error);
ASSERT_EQ(got2.response.tracepoints.size(), 1u);
EXPECT_FALSE(got2.response.tracepoints[0].verified);
EXPECT_TRUE(session().system().GetTracepoints().empty());
EXPECT_TRUE(context().dap_tracepoints().empty());
}
TEST_F(RequestTracepointTest, SpecAddressAndLineValidationErrors) {
InitializeDebugging();
auto got_addr = SendSetTracepoints({{.address = -10}});
ASSERT_FALSE(got_addr.error);
ASSERT_EQ(got_addr.response.tracepoints.size(), 1u);
EXPECT_FALSE(got_addr.response.tracepoints[0].verified);
ASSERT_TRUE(got_addr.response.tracepoints[0].message.has_value());
EXPECT_EQ(got_addr.response.tracepoints[0].message.value(),
"Invalid negative tracepoint address.");
auto got_line = SendSetTracepoints({{.file = "main.cc"}});
ASSERT_FALSE(got_line.error);
ASSERT_EQ(got_line.response.tracepoints.size(), 1u);
EXPECT_FALSE(got_line.response.tracepoints[0].verified);
ASSERT_TRUE(got_line.response.tracepoints[0].message.has_value());
EXPECT_EQ(got_line.response.tracepoints[0].message.value(),
"Both file and line must be specified together for a line tracepoint.");
dap::ZxdbTracepointSpec neg_watch;
neg_watch.watch = dap::ZxdbTracepointWatchSpec{.address = -1};
auto got_watch = SendSetTracepoints({neg_watch});
ASSERT_FALSE(got_watch.error);
ASSERT_EQ(got_watch.response.tracepoints.size(), 1u);
EXPECT_FALSE(got_watch.response.tracepoints[0].verified);
ASSERT_TRUE(got_watch.response.tracepoints[0].message.has_value());
EXPECT_EQ(got_watch.response.tracepoints[0].message.value(), "Invalid negative watch address.");
}
TEST_F(RequestTracepointTest, WatchByteSizeOverrideAndOverflow) {
InitializeDebugging();
dap::ZxdbTracepointSpec overflow_spec;
overflow_spec.watch =
dap::ZxdbTracepointWatchSpec{.data_id = "1234:0xfffffffffffffffe:1", .byte_size = 4};
auto got_overflow = SendSetTracepoints({overflow_spec});
ASSERT_FALSE(got_overflow.error);
ASSERT_EQ(got_overflow.response.tracepoints.size(), 1u);
EXPECT_FALSE(got_overflow.response.tracepoints[0].verified);
ASSERT_TRUE(got_overflow.response.tracepoints[0].message.has_value());
EXPECT_EQ(got_overflow.response.tracepoints[0].message.value(),
"Watch address range overflows 64-bit address space.");
dap::ZxdbTracepointSpec override_spec;
override_spec.watch = dap::ZxdbTracepointWatchSpec{.data_id = "1234:0x2000:4", .byte_size = 8};
auto got_override = SendSetTracepoints({override_spec});
ASSERT_FALSE(got_override.error);
ASSERT_EQ(got_override.response.tracepoints.size(), 1u);
auto it = context().dap_tracepoints().find(got_override.response.tracepoints[0].id);
ASSERT_NE(it, context().dap_tracepoints().end());
ASSERT_TRUE(it->second);
ASSERT_TRUE(it->second->spec().watch.has_value());
EXPECT_EQ(it->second->spec().watch->byte_size, 8u);
}
} // namespace
} // namespace zxdb