blob: 7c02ec9429f7fd83ed8b5a201b74a5c7a7623c06 [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 <gtest/gtest.h>
#include "src/developer/debug/zxdb/client/async_task.h"
#include "src/developer/debug/zxdb/client/mock_frame.h"
#include "src/developer/debug/zxdb/client/process.h"
#include "src/developer/debug/zxdb/client/thread.h"
#include "src/developer/debug/zxdb/common/scoped_temp_file.h"
#include "src/developer/debug/zxdb/debug_adapter/context_test.h"
#include "src/developer/debug/zxdb/symbols/compile_unit.h"
#include "src/developer/debug/zxdb/symbols/dwarf_lang.h"
#include "src/developer/debug/zxdb/symbols/function.h"
#include "src/developer/debug/zxdb/symbols/location.h"
#include "src/developer/debug/zxdb/symbols/symbol_context.h"
#include "src/developer/debug/zxdb/symbols/symbol_test_parent_setter.h"
namespace zxdb {
namespace {
class AsyncBacktraceSubscriptionTest : public DebugAdapterContextTest {
public:
void SetUp() override {
DebugAdapterContextTest::SetUp();
temp_file_ = std::make_unique<ScopedTempFile>();
fake_file_path_ = temp_file_->name();
}
std::unique_ptr<ScopedTempFile> temp_file_;
std::string fake_file_path_;
void DeinitializeAsyncBacktraceSubscription() override {
// Override the default behavior of disabling the `AsyncBacktraceSubscription` since we want to
// test async backtrace behavior.
}
};
class FakeAsyncTask : public AsyncTask {
public:
FakeAsyncTask(Session* session, uint64_t id, std::string name, Location loc = Location())
: AsyncTask(session), id_(id), name_(std::move(name)), loc_(std::move(loc)) {}
uint64_t GetId() const override { return id_; }
Type GetType() const override { return Type::kFuture; }
const Location& GetLocation() const override { return loc_; }
const Identifier& GetIdentifier() const override {
if (ident_.empty()) {
ident_ = Identifier(IdentifierComponent(name_));
}
return ident_;
}
std::string GetState() const override { return "Pending"; }
const std::vector<NamedValue>& GetValues() const override { return values_; }
std::vector<Ref> GetChildren() const override {
std::vector<Ref> refs;
for (const auto& child : children_) {
refs.push_back(*child);
}
return refs;
}
void AddChild(std::unique_ptr<FakeAsyncTask> child) { children_.push_back(std::move(child)); }
private:
uint64_t id_;
std::string name_;
std::vector<std::unique_ptr<FakeAsyncTask>> children_;
Location loc_;
mutable Identifier ident_;
std::vector<NamedValue> values_;
};
class FakeAsyncTaskProvider : public AsyncTaskProvider {
public:
explicit FakeAsyncTaskProvider(std::string fake_file_path)
: fake_file_path_(std::move(fake_file_path)) {}
bool CanHandle(Frame* frame) const override { return true; }
void GetTasks(
Frame* frame,
fit::callback<void(const Err&, std::vector<std::unique_ptr<AsyncTask>>)> cb) override {
std::vector<std::unique_ptr<AsyncTask>> tasks;
Location loc(0x1234, FileLine(fake_file_path_, 42), 0, SymbolContext::ForRelativeAddresses());
auto root = std::make_unique<FakeAsyncTask>(frame->session(), 1, "root", std::move(loc));
auto child = std::make_unique<FakeAsyncTask>(frame->session(), 2, "child");
child->AddChild(std::make_unique<FakeAsyncTask>(frame->session(), 3, "grandchild"));
root->AddChild(std::move(child));
tasks.push_back(std::move(root));
auto zero_id_task = std::make_unique<FakeAsyncTask>(frame->session(), 0, "zero_id_task");
tasks.push_back(std::move(zero_id_task));
cb(Err(), std::move(tasks));
}
private:
std::string fake_file_path_;
};
class RaceConditionAsyncTaskProvider : public AsyncTaskProvider {
public:
bool CanHandle(Frame* frame) const override { return true; }
void GetTasks(
Frame* frame,
fit::callback<void(const Err&, std::vector<std::unique_ptr<AsyncTask>>)> cb) override {
callbacks_.push_back({.session = frame->session(), .cb = std::move(cb)});
}
struct SavedCallback {
Session* session;
fit::callback<void(const Err&, std::vector<std::unique_ptr<AsyncTask>>)> cb;
};
std::vector<SavedCallback> callbacks_;
};
} // namespace
TEST_F(AsyncBacktraceSubscriptionTest, SingleThreadLifecycle) {
InitializeDebugging();
std::vector<dap::AsyncBacktraceUpdate> updates;
client().registerHandler(
[&](const dap::AsyncBacktraceUpdate& event) { updates.push_back(event); });
Process* process = InjectProcessWithModule(kProcessKoid, 0x1000);
process->AddAsyncTaskProviderForTesting(ExprLanguage::kRust,
std::make_unique<FakeAsyncTaskProvider>(fake_file_path_));
RunClient();
InjectThread(kProcessKoid, kThreadKoid);
context().OnStreamReadable();
loop().RunUntilNoTasks();
RunPendingClientCalls();
// Expect an update from `ThreadObserver::DidCreateThread`.
ASSERT_EQ(updates.size(), 1u);
EXPECT_EQ(updates[0].id, static_cast<double>(kThreadKoid));
EXPECT_EQ(updates[0].name, "test 19028730");
EXPECT_TRUE(updates[0].tasks.has_value());
EXPECT_EQ(updates[0].tasks.value().size(), 0u);
updates.clear();
Thread* thread = process->GetThreadFromKoid(kThreadKoid);
ASSERT_NE(thread, nullptr);
std::vector<std::unique_ptr<Frame>> frames;
auto cu = fxl::MakeRefCounted<CompileUnit>(DwarfTag::kCompileUnit, fxl::WeakPtr<ModuleSymbols>(),
fxl::RefPtr<DwarfUnit>(), DwarfLang::kRust, "test.rs",
std::optional<uint64_t>());
auto func = fxl::MakeRefCounted<Function>(DwarfTag::kSubprogram);
func->set_assigned_name("executor");
SymbolTestParentSetter parent_setter(func, cu);
Location loc(0x1234, FileLine(), 0, SymbolContext::ForRelativeAddresses(), func);
frames.push_back(std::make_unique<MockFrame>(&session(), thread, loc, 0x1000));
InjectExceptionWithStack(kProcessKoid, kThreadKoid, debug_ipc::ExceptionType::kSingleStep,
std::move(frames), /*has_all_frames=*/false);
// Force the thread to sync its stack frames.
//
// This results in the invocation order `OnThreadStopped` -> `DidUpdateStackFrames` ->
// `CollectAndReportAsyncBacktrace`'s `Sync` callback, which allows us to verify that
// `DidUpdateStackFrames` does not cancel the pending async-backtrace collection callback when
// the thread has not yet resumed (i.e. when `CurrentStopSupportsFrames()` is true).
EXPECT_TRUE(thread->CurrentStopSupportsFrames());
context().OnStreamReadable();
loop().RunUntilNoTasks();
RunPendingClientCalls();
// Expect a non-cancelled update from `ThreadObserver::OnThreadStopped`.
ASSERT_EQ(updates.size(), 1u);
EXPECT_EQ(updates[0].id, static_cast<double>(kThreadKoid));
EXPECT_EQ(updates[0].name, "test 19028730");
EXPECT_TRUE(updates[0].tasks.has_value());
EXPECT_EQ(updates[0].tasks.value().size(), 2u);
ASSERT_TRUE(updates[0].tasks.value()[0].id.has_value());
EXPECT_EQ(updates[0].tasks.value()[0].id.value(), "0x1");
EXPECT_EQ(updates[0].tasks.value()[0].name, "root");
EXPECT_FALSE(updates[0].tasks.value()[1].id.has_value());
EXPECT_EQ(updates[0].tasks.value()[1].name, "zero_id_task");
updates.clear();
thread->Continue(false);
// Process the Continue response to update thread state.
loop().RunUntilNoTasks();
RunPendingClientCalls();
EXPECT_FALSE(thread->CurrentStopSupportsFrames());
// Expect an update from `ThreadObserver::DidUpdateStackFrames` (Thread Resumed).
ASSERT_EQ(updates.size(), 1u);
EXPECT_EQ(updates[0].id, static_cast<double>(kThreadKoid));
EXPECT_EQ(updates[0].name, "test 19028730");
EXPECT_TRUE(updates[0].tasks.has_value());
EXPECT_EQ(updates[0].tasks.value().size(), 0u);
updates.clear();
debug_ipc::NotifyThreadExiting notify_exit;
notify_exit.record.id = {.process = kProcessKoid, .thread = kThreadKoid};
notify_exit.record.state = debug_ipc::ThreadRecord::State::kDead;
session().DispatchNotifyThreadExiting(notify_exit);
context().OnStreamReadable();
loop().RunUntilNoTasks();
RunPendingClientCalls();
// Expect an update from `ThreadObserver::WillDestroyThread`.
ASSERT_EQ(updates.size(), 1u);
EXPECT_EQ(updates[0].id, static_cast<double>(kThreadKoid));
EXPECT_EQ(updates[0].name, "test 19028730");
EXPECT_FALSE(updates[0].tasks.has_value());
}
TEST_F(AsyncBacktraceSubscriptionTest, NestedAsyncTaskStructure) {
InitializeDebugging();
std::vector<dap::AsyncBacktraceUpdate> updates;
client().registerHandler(
[&](const dap::AsyncBacktraceUpdate& event) { updates.push_back(event); });
Process* process = InjectProcessWithModule(kProcessKoid, 0x1000);
process->AddAsyncTaskProviderForTesting(ExprLanguage::kRust,
std::make_unique<FakeAsyncTaskProvider>(fake_file_path_));
Thread* thread = InjectThread(kProcessKoid, kThreadKoid);
context().OnStreamReadable();
loop().RunUntilNoTasks();
RunPendingClientCalls();
// Expect an update from `ThreadObserver::DidCreateThread`.
ASSERT_EQ(updates.size(), 1u);
EXPECT_EQ(updates[0].id, static_cast<double>(kThreadKoid));
EXPECT_EQ(updates[0].name, "test 19028730");
EXPECT_TRUE(updates[0].tasks.has_value());
EXPECT_EQ(updates[0].tasks.value().size(), 0u);
updates.clear();
std::vector<std::unique_ptr<Frame>> frames;
auto cu = fxl::MakeRefCounted<CompileUnit>(DwarfTag::kCompileUnit, fxl::WeakPtr<ModuleSymbols>(),
fxl::RefPtr<DwarfUnit>(), DwarfLang::kRust, "test.rs",
std::optional<uint64_t>());
auto func = fxl::MakeRefCounted<Function>(DwarfTag::kSubprogram);
func->set_assigned_name("executor");
SymbolTestParentSetter parent_setter(func, cu);
Location loc(0x1234, FileLine(), 0, SymbolContext::ForRelativeAddresses(), func);
frames.push_back(std::make_unique<MockFrame>(&session(), thread, loc, 0));
InjectExceptionWithStack(kProcessKoid, kThreadKoid, debug_ipc::ExceptionType::kSingleStep,
std::move(frames), true);
context().OnStreamReadable();
loop().RunUntilNoTasks();
RunPendingClientCalls();
// Expect an update from `ThreadObserver::OnThreadStopped`.
ASSERT_EQ(updates.size(), 1u);
EXPECT_EQ(updates[0].id, static_cast<double>(kThreadKoid));
EXPECT_EQ(updates[0].name, "test 19028730");
EXPECT_TRUE(updates[0].tasks.has_value());
EXPECT_EQ(updates[0].tasks.value().size(), 2u);
ASSERT_TRUE(updates[0].tasks.value()[0].id.has_value());
EXPECT_EQ(updates[0].tasks.value()[0].id.value(), "0x1");
EXPECT_EQ(updates[0].tasks.value()[0].name, "root");
EXPECT_EQ(updates[0].tasks.value()[0].children.size(), 1u);
ASSERT_TRUE(updates[0].tasks.value()[0].children[0].id.has_value());
EXPECT_EQ(updates[0].tasks.value()[0].children[0].id.value(), "0x2");
EXPECT_EQ(updates[0].tasks.value()[0].children[0].name, "child");
EXPECT_EQ(updates[0].tasks.value()[0].children[0].children.size(), 1u);
ASSERT_TRUE(updates[0].tasks.value()[0].children[0].children[0].id.has_value());
EXPECT_EQ(updates[0].tasks.value()[0].children[0].children[0].id.value(), "0x3");
EXPECT_EQ(updates[0].tasks.value()[0].children[0].children[0].name, "grandchild");
EXPECT_EQ(updates[0].tasks.value()[0].children[0].children[0].children.size(), 0u);
EXPECT_FALSE(updates[0].tasks.value()[1].id.has_value());
EXPECT_EQ(updates[0].tasks.value()[1].name, "zero_id_task");
// Verify that the file and line fields are correctly handled.
EXPECT_TRUE(updates[0].tasks.value()[0].file.has_value());
EXPECT_EQ(updates[0].tasks.value()[0].file.value(), fake_file_path_);
EXPECT_TRUE(updates[0].tasks.value()[0].line.has_value());
EXPECT_EQ(updates[0].tasks.value()[0].line.value(), 42);
}
TEST_F(AsyncBacktraceSubscriptionTest, ConcurrentBacktracesSameThread) {
InitializeDebugging();
std::vector<dap::AsyncBacktraceUpdate> updates;
client().registerHandler(
[&](const dap::AsyncBacktraceUpdate& event) { updates.push_back(event); });
Process* process = InjectProcessWithModule(kProcessKoid, 0x1000);
auto provider = std::make_unique<RaceConditionAsyncTaskProvider>();
auto* provider_ptr = provider.get();
process->AddAsyncTaskProviderForTesting(ExprLanguage::kRust, std::move(provider));
Thread* thread = InjectThread(kProcessKoid, kThreadKoid);
context().OnStreamReadable();
loop().RunUntilNoTasks();
RunPendingClientCalls();
updates.clear();
auto cu = fxl::MakeRefCounted<CompileUnit>(DwarfTag::kCompileUnit, fxl::WeakPtr<ModuleSymbols>(),
fxl::RefPtr<DwarfUnit>(), DwarfLang::kRust, "test.rs",
std::optional<uint64_t>());
auto func = fxl::MakeRefCounted<Function>(DwarfTag::kSubprogram);
func->set_assigned_name("executor");
SymbolTestParentSetter parent_setter(func, cu);
Location loc(0x1234, FileLine(), 0, SymbolContext::ForRelativeAddresses(), func);
std::vector<std::unique_ptr<Frame>> frames1;
frames1.push_back(std::make_unique<MockFrame>(&session(), thread, loc, 0));
InjectExceptionWithStack(kProcessKoid, kThreadKoid, debug_ipc::ExceptionType::kSingleStep,
std::move(frames1), true);
// Triggering a second update should cancel the first callback.
std::vector<std::unique_ptr<Frame>> frames2;
frames2.push_back(std::make_unique<MockFrame>(&session(), thread, loc, 0));
InjectExceptionWithStack(kProcessKoid, kThreadKoid, debug_ipc::ExceptionType::kSingleStep,
std::move(frames2), true);
context().OnStreamReadable();
loop().RunUntilNoTasks();
RunPendingClientCalls();
ASSERT_EQ(provider_ptr->callbacks_.size(), 2u);
// Execute the first callback. It shouldn't emit an update because it was cancelled.
auto cb1 = std::move(provider_ptr->callbacks_[0]);
std::vector<std::unique_ptr<AsyncTask>> tasks1;
tasks1.push_back(std::make_unique<FakeAsyncTask>(cb1.session, 10, "task_1"));
cb1.cb(Err(), std::move(tasks1));
context().OnStreamReadable();
loop().RunUntilNoTasks();
RunPendingClientCalls();
ASSERT_EQ(updates.size(), 0u);
// Execute the second callback. It should emit an update.
auto cb2 = std::move(provider_ptr->callbacks_[1]);
std::vector<std::unique_ptr<AsyncTask>> tasks2;
tasks2.push_back(std::make_unique<FakeAsyncTask>(cb2.session, 11, "task_2"));
cb2.cb(Err(), std::move(tasks2));
context().OnStreamReadable();
loop().RunUntilNoTasks();
RunPendingClientCalls();
ASSERT_EQ(updates.size(), 1u);
EXPECT_TRUE(updates[0].tasks.has_value());
EXPECT_EQ(updates[0].tasks.value().size(), 1u);
ASSERT_TRUE(updates[0].tasks.value()[0].id.has_value());
EXPECT_EQ(updates[0].tasks.value()[0].id.value(), "0xb");
EXPECT_EQ(updates[0].tasks.value()[0].name, "task_2");
}
TEST_F(AsyncBacktraceSubscriptionTest, ConcurrentBacktracesSeparateThreads) {
InitializeDebugging();
std::vector<dap::AsyncBacktraceUpdate> updates;
client().registerHandler(
[&](const dap::AsyncBacktraceUpdate& event) { updates.push_back(event); });
Process* process = InjectProcessWithModule(kProcessKoid, 0x1000);
auto provider = std::make_unique<RaceConditionAsyncTaskProvider>();
auto* provider_ptr = provider.get();
process->AddAsyncTaskProviderForTesting(ExprLanguage::kRust, std::move(provider));
constexpr uint64_t kThreadKoid1 = kThreadKoid;
constexpr uint64_t kThreadKoid2 = kThreadKoid + 1;
Thread* thread1 = InjectThread(kProcessKoid, kThreadKoid1);
Thread* thread2 = InjectThread(kProcessKoid, kThreadKoid2);
context().OnStreamReadable();
loop().RunUntilNoTasks();
RunPendingClientCalls();
updates.clear();
auto cu = fxl::MakeRefCounted<CompileUnit>(DwarfTag::kCompileUnit, fxl::WeakPtr<ModuleSymbols>(),
fxl::RefPtr<DwarfUnit>(), DwarfLang::kRust, "test.rs",
std::optional<uint64_t>());
auto func = fxl::MakeRefCounted<Function>(DwarfTag::kSubprogram);
func->set_assigned_name("executor");
SymbolTestParentSetter parent_setter(func, cu);
Location loc(0x1234, FileLine(), 0, SymbolContext::ForRelativeAddresses(), func);
// Triggering updates from different threads shouldn't affect each other.
std::vector<std::unique_ptr<Frame>> frames1;
frames1.push_back(std::make_unique<MockFrame>(&session(), thread1, loc, 0));
InjectExceptionWithStack(kProcessKoid, kThreadKoid1, debug_ipc::ExceptionType::kSingleStep,
std::move(frames1), true);
std::vector<std::unique_ptr<Frame>> frames2;
frames2.push_back(std::make_unique<MockFrame>(&session(), thread2, loc, 0));
InjectExceptionWithStack(kProcessKoid, kThreadKoid2, debug_ipc::ExceptionType::kSingleStep,
std::move(frames2), true);
context().OnStreamReadable();
loop().RunUntilNoTasks();
RunPendingClientCalls();
ASSERT_EQ(provider_ptr->callbacks_.size(), 2u);
// Execute the first callback. It should emit an update, since the second (concurrent)
// `ThreadObserver::OnThreadStopped` event is for a different thread.
auto cb1 = std::move(provider_ptr->callbacks_[0]);
std::vector<std::unique_ptr<AsyncTask>> tasks1;
tasks1.push_back(std::make_unique<FakeAsyncTask>(cb1.session, 101, "task_1"));
cb1.cb(Err(), std::move(tasks1));
context().OnStreamReadable();
loop().RunUntilNoTasks();
RunPendingClientCalls();
ASSERT_EQ(updates.size(), 1u);
EXPECT_TRUE(updates[0].tasks.has_value());
EXPECT_EQ(updates[0].tasks.value().size(), 1u);
ASSERT_TRUE(updates[0].tasks.value()[0].id.has_value());
EXPECT_EQ(updates[0].tasks.value()[0].id.value(), "0x65");
EXPECT_EQ(updates[0].tasks.value()[0].name, "task_1");
updates.clear();
// Execute the second callback and expect an update.
auto cb2 = std::move(provider_ptr->callbacks_[1]);
std::vector<std::unique_ptr<AsyncTask>> tasks2;
tasks2.push_back(std::make_unique<FakeAsyncTask>(cb2.session, 102, "task_2"));
cb2.cb(Err(), std::move(tasks2));
context().OnStreamReadable();
loop().RunUntilNoTasks();
RunPendingClientCalls();
ASSERT_EQ(updates.size(), 1u);
EXPECT_TRUE(updates[0].tasks.has_value());
EXPECT_EQ(updates[0].tasks.value().size(), 1u);
ASSERT_TRUE(updates[0].tasks.value()[0].id.has_value());
EXPECT_EQ(updates[0].tasks.value()[0].id.value(), "0x66");
EXPECT_EQ(updates[0].tasks.value()[0].name, "task_2");
}
} // namespace zxdb