blob: fabb88ce26c4620dfe865fac84e17a0d8b37a2ce [file] [log] [blame]
// Copyright 2018 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/thread_impl.h"
#include <gtest/gtest.h>
#include "src/developer/debug/shared/message_loop.h"
#include "src/developer/debug/zxdb/client/frame.h"
#include "src/developer/debug/zxdb/client/mock_frame.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/step_thread_controller.h"
#include "src/developer/debug/zxdb/client/test_thread_observer.h"
#include "src/developer/debug/zxdb/client/thread_controller.h"
#include "src/developer/debug/zxdb/common/err.h"
#include "src/developer/debug/zxdb/symbols/input_location.h"
namespace zxdb {
namespace {
class ThreadImplTest : public RemoteAPITest {};
// This ThreadController always responds with "continue".
class ContinueThreadController : public ThreadController {
public:
// The parameter is a variable to set when the OnThreadStop() function is called. It must outlive
// this class.
explicit ContinueThreadController(bool* got_stop) : got_stop_(got_stop) {}
~ContinueThreadController() override = default;
// ThreadController implementation.
void InitWithThread(Thread* thread, fit::callback<void(const Err&)> cb) override {
SetThread(thread);
cb(Err());
}
ContinueOp GetContinueOp() override { return ContinueOp::Continue(); }
StopOp OnThreadStop(debug_ipc::ExceptionType stop_type,
const std::vector<fxl::WeakPtr<Breakpoint>>& hit_breakpoints) override {
*got_stop_ = true;
return kContinue;
}
const char* GetName() const override { return "Continue"; }
private:
bool* got_stop_;
};
// This ThreadController always reports "kUnexpected" for stops.
class UnexpectedThreadController : public ThreadController {
public:
explicit UnexpectedThreadController() = default;
~UnexpectedThreadController() override = default;
// ThreadController implementation.
void InitWithThread(Thread* thread, fit::callback<void(const Err&)> cb) override {
SetThread(thread);
cb(Err());
}
ContinueOp GetContinueOp() override { return ContinueOp::Continue(); }
StopOp OnThreadStop(debug_ipc::ExceptionType stop_type,
const std::vector<fxl::WeakPtr<Breakpoint>>& hit_breakpoints) override {
return kUnexpected;
}
const char* GetName() const override { return "Unexpected"; }
};
// This ThreadController responds twice with kFuture and then once with kStop.
class FutureThreadController : public ThreadController {
public:
explicit FutureThreadController() = default;
~FutureThreadController() override = default;
// ThreadController implementation.
void InitWithThread(Thread* thread, fit::callback<void(const Err&)> cb) override {
SetThread(thread);
cb(Err());
}
ContinueOp GetContinueOp() override { return ContinueOp::Continue(); }
StopOp OnThreadStop(debug_ipc::ExceptionType stop_type,
const std::vector<fxl::WeakPtr<Breakpoint>>& hit_breakpoints) override {
if (futures_remaining_ == 0)
return kStopDone;
futures_remaining_--;
return kFuture;
}
const char* GetName() const override { return "Future"; }
private:
int futures_remaining_ = 2;
};
} // namespace
TEST_F(ThreadImplTest, Frames) {
// Make a process and thread for notifying about.
constexpr uint64_t kProcessKoid = 1234;
InjectProcess(kProcessKoid);
constexpr uint64_t kThreadKoid = 5678;
Thread* thread = InjectThread(kProcessKoid, kThreadKoid);
// The thread should be in a running state with no frames.
EXPECT_TRUE(thread->GetStack().empty());
EXPECT_FALSE(thread->GetStack().has_all_frames());
constexpr uint64_t kAddress1 = 0x12345678;
constexpr uint64_t kStack1 = 0x7890;
constexpr uint64_t kAddress2 = 0x34567890;
constexpr uint64_t kStack2 = 0x7800;
// Notify of thread stop.
debug_ipc::NotifyException break_notification;
break_notification.type = debug_ipc::ExceptionType::kSoftwareBreakpoint;
break_notification.thread.id = {.process = kProcessKoid, .thread = kThreadKoid};
break_notification.thread.state = debug_ipc::ThreadRecord::State::kBlocked;
break_notification.thread.frames.emplace_back(kAddress1, kStack1, kStack2);
InjectException(break_notification);
// There should be one frame with the address of the stop.
EXPECT_FALSE(thread->GetStack().has_all_frames());
const Stack& stack = thread->GetStack();
ASSERT_EQ(1u, stack.size());
EXPECT_EQ(kAddress1, stack[0]->GetAddress());
EXPECT_EQ(kStack1, stack[0]->GetStackPointer());
// Construct what the full stack will be returned to the thread. The top element should match the
// one already there.
debug_ipc::ThreadStatusReply expected_reply;
expected_reply.record = break_notification.thread; // Copies existing frame.
expected_reply.record.stack_amount = debug_ipc::ThreadRecord::StackAmount::kFull;
expected_reply.record.frames.emplace_back(kAddress2, kStack2, 0);
mock_remote_api()->set_thread_status_reply(expected_reply);
// Asynchronously request the frames.
thread->GetStack().SyncFrames([](const Err&) { debug::MessageLoop::Current()->QuitNow(); });
loop().Run();
// The thread should have the new stack we provided.
EXPECT_TRUE(thread->GetStack().has_all_frames());
ASSERT_EQ(2u, stack.size());
EXPECT_EQ(kAddress1, stack[0]->GetAddress());
EXPECT_EQ(kStack1, stack[0]->GetStackPointer());
EXPECT_EQ(kAddress2, stack[1]->GetAddress());
EXPECT_EQ(kStack2, stack[1]->GetStackPointer());
// Resuming the thread should clear the frames.
mock_remote_api()->set_resume_quits_loop(true);
thread->Continue(false);
EXPECT_EQ(0u, thread->GetStack().size());
EXPECT_FALSE(thread->GetStack().has_all_frames());
loop().Run();
// After resuming we don't actually know what state the thread is in so nothing should change. If
// we have better thread state notifications in the future, it would be nice if the thread
// reported itself as running and the stack was cleared at this point.
}
// Tests that general exceptions still run thread controllers. If the exception is at an address
// where the thread controller says "stop", that thread controller should be notified so it can be
// deleted. It doesn't matter what caused the stop if the thread controller thinks its done.
//
// For this exception case, the thread should always stop, even if the controllers say "continue."
TEST_F(ThreadImplTest, ControllersWithGeneralException) {
constexpr uint64_t kProcessKoid = 1234;
InjectProcess(kProcessKoid);
constexpr uint64_t kThreadKoid = 5678;
Thread* thread = InjectThread(kProcessKoid, kThreadKoid);
// Notify of thread stop.
constexpr uint64_t kAddress1 = 0x12345678;
constexpr uint64_t kStack1 = 0x7890;
debug_ipc::NotifyException notification;
notification.type = debug_ipc::ExceptionType::kSoftwareBreakpoint;
notification.thread.id = {.process = kProcessKoid, .thread = kThreadKoid};
notification.thread.state = debug_ipc::ThreadRecord::State::kBlocked;
notification.thread.frames.emplace_back(kAddress1, kStack1);
InjectException(notification);
// Set a controller that always says to continue.
bool got_stop = false;
thread->ContinueWith(std::make_unique<ContinueThreadController>(&got_stop),
[](const Err& err) {});
EXPECT_FALSE(got_stop); // Should not have been called.
// Start watching for thread events starting now.
TestThreadObserver thread_observer(thread);
// Notify on thread stop again (this is the same address as above but it doesn't matter).
notification.type = debug_ipc::ExceptionType::kGeneral;
InjectException(notification);
// The controller should have been notified and the thread should have issued a stop notification
// even though the controller said to continue.
EXPECT_TRUE(got_stop);
EXPECT_TRUE(thread_observer.got_stopped());
}
// Tests conditions where thread controllers report unexpected stop types.
TEST_F(ThreadImplTest, ControllersUnexpected) {
constexpr uint64_t kProcessKoid = 1234;
InjectProcess(kProcessKoid);
constexpr uint64_t kThreadKoid = 5678;
Thread* thread = InjectThread(kProcessKoid, kThreadKoid);
TestThreadObserver thread_observer(thread);
// Notify of thread stop.
constexpr uint64_t kAddress1 = 0x12345678;
constexpr uint64_t kStack1 = 0x7890;
debug_ipc::NotifyException notification;
notification.type = debug_ipc::ExceptionType::kSoftwareBreakpoint;
notification.thread.id = {.process = kProcessKoid, .thread = kThreadKoid};
notification.thread.state = debug_ipc::ThreadRecord::State::kBlocked;
notification.thread.frames.emplace_back(kAddress1, kStack1);
InjectException(notification);
// No controllers means the thread should report "stopped".
EXPECT_TRUE(thread_observer.got_stopped());
thread_observer.set_got_stopped(false);
// Add the controller that always reports unexpected.
thread->ContinueWith(std::make_unique<UnexpectedThreadController>(), [](const Err& err) {});
// Notify on thread stop again (this is the same address as above but it doesn't matter).
notification.type = debug_ipc::ExceptionType::kSingleStep;
InjectException(notification);
// When all controllers report unexpected, the thread should stop.
EXPECT_TRUE(thread_observer.got_stopped());
thread_observer.set_got_stopped(false);
// Add a continue controller and throw the exception. There should be one controller voting
// "continue" and one voting "unexpected" which should continue the thread.
bool continue_got_stop = false;
thread->ContinueWith(std::make_unique<ContinueThreadController>(&continue_got_stop),
[](const Err& err) {});
InjectException(notification);
EXPECT_FALSE(thread_observer.got_stopped());
}
// Sends an exception with no stack. Any thread controllers should get deleted.
TEST_F(ThreadImplTest, StopNoStack) {
constexpr uint64_t kProcessKoid = 1234;
InjectProcess(kProcessKoid);
constexpr uint64_t kThreadKoid = 5678;
Thread* thread = InjectThread(kProcessKoid, kThreadKoid);
TestThreadObserver thread_observer(thread);
// Normal thread stop with a location to begin stepping. Then step.
std::vector<std::unique_ptr<Frame>> frames;
frames.push_back(std::make_unique<MockFrame>(
&session(), thread, Location(Location::State::kAddress, 0x7890), 0x1234));
InjectExceptionWithStack(kProcessKoid, kThreadKoid, debug_ipc::ExceptionType::kSoftwareBreakpoint,
std::move(frames), false);
thread->ContinueWith(std::make_unique<StepThreadController>(StepMode::kInstruction),
[](const Err& err) {});
ThreadImpl* thread_impl = static_cast<ThreadImpl*>(thread);
EXPECT_FALSE(thread_impl->controllers_.empty());
// Notify on thread stop with no stack.
thread_observer.set_got_stopped(false);
InjectExceptionWithStack(kProcessKoid, kThreadKoid, debug_ipc::ExceptionType::kSoftwareBreakpoint,
{}, false);
thread_observer.set_got_stopped(false);
// The thread controllers should be gone.
EXPECT_TRUE(thread_impl->controllers_.empty());
}
TEST_F(ThreadImplTest, JumpTo) {
constexpr uint64_t kProcessKoid = 1234;
InjectProcess(kProcessKoid);
constexpr uint64_t kThreadKoid = 5678;
Thread* thread = InjectThread(kProcessKoid, kThreadKoid);
// Notify of thread stop.
constexpr uint64_t kAddress1 = 0x12345678;
constexpr uint64_t kStack = 0x7890;
debug_ipc::NotifyException notification;
notification.type = debug_ipc::ExceptionType::kSoftwareBreakpoint;
notification.thread.id = {.process = kProcessKoid, .thread = kThreadKoid};
notification.thread.state = debug_ipc::ThreadRecord::State::kBlocked;
notification.thread.frames.emplace_back(kAddress1, kStack);
InjectException(notification);
// Canned response for thread status.
constexpr uint64_t kDestAddress = 0x7828374510a;
debug_ipc::ThreadStatusReply status;
status.record = notification.thread; // Copies existing frame.
status.record.stack_amount = debug_ipc::ThreadRecord::StackAmount::kFull;
status.record.frames[0].ip = kDestAddress;
mock_remote_api()->set_thread_status_reply(status);
// Do jump.
bool called = false;
Err out_err;
thread->JumpTo(kDestAddress, [&called, &out_err](const Err& err) {
called = true;
out_err = err;
debug::MessageLoop::Current()->QuitNow();
});
EXPECT_FALSE(called);
// The command should have sent a request to write to the IP. There should be a single register
// with a value of the new address.
const auto& written = mock_remote_api()->last_write_registers().registers;
ASSERT_EQ(1u, written.size());
ASSERT_EQ(sizeof(uint64_t), written[0].data.size());
EXPECT_EQ(0, memcmp(&kDestAddress, &written[0].data[0], sizeof(uint64_t)));
// The callback should be asynchronously received.
loop().Run();
EXPECT_TRUE(called);
EXPECT_FALSE(out_err.has_error());
// The thread should be in the new location.
EXPECT_EQ(kDestAddress, thread->GetStack()[0]->GetAddress());
}
TEST_F(ThreadImplTest, FutureThreadController) {
constexpr uint64_t kProcessKoid = 1234;
InjectProcess(kProcessKoid);
constexpr uint64_t kThreadKoid = 5678;
Thread* thread = InjectThread(kProcessKoid, kThreadKoid);
TestThreadObserver thread_observer(thread);
// Notify of thread stop.
constexpr uint64_t kAddress1 = 0x12345678;
constexpr uint64_t kStack1 = 0x7890;
debug_ipc::NotifyException notification;
notification.type = debug_ipc::ExceptionType::kSingleStep;
notification.thread.id = {.process = kProcessKoid, .thread = kThreadKoid};
notification.thread.state = debug_ipc::ThreadRecord::State::kBlocked;
notification.thread.frames.emplace_back(kAddress1, kStack1);
InjectException(notification);
thread->ContinueWith(std::make_unique<FutureThreadController>(), [](const Err& err) {});
thread_observer.set_got_stopped(false);
// Notify on thread stop (this is the same address as above but it doesn't matter). The thread
// controller should report "future" and not notify.
InjectException(notification);
EXPECT_FALSE(thread_observer.got_stopped());
// Declare that future stop as complete. This will return another future.
thread->ResumeFromAsyncThreadController(std::nullopt);
EXPECT_FALSE(thread_observer.got_stopped());
// The third time the controller should report stop which should notify.
thread->ResumeFromAsyncThreadController(std::nullopt);
EXPECT_TRUE(thread_observer.got_stopped());
}
} // namespace zxdb