| // 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/session.h" |
| |
| #include <set> |
| |
| #include <gtest/gtest.h> |
| |
| #include "src/developer/debug/zxdb/client/breakpoint.h" |
| #include "src/developer/debug/zxdb/client/breakpoint_settings.h" |
| #include "src/developer/debug/zxdb/client/filter.h" |
| #include "src/developer/debug/zxdb/client/job.h" |
| #include "src/developer/debug/zxdb/client/remote_api_test.h" |
| #include "src/developer/debug/zxdb/client/system.h" |
| #include "src/developer/debug/zxdb/client/thread.h" |
| #include "src/developer/debug/zxdb/client/thread_observer.h" |
| |
| namespace zxdb { |
| |
| namespace { |
| |
| using debug_ipc::MessageLoop; |
| |
| class SessionTest; |
| |
| class SessionSink : public RemoteAPI { |
| public: |
| explicit SessionSink(SessionTest* session_test) : session_test_(session_test) {} |
| ~SessionSink() override = default; |
| |
| // Returns the breakpoint IDs that the backend is supposed to know about now. |
| const std::set<uint32_t>& set_breakpoint_ids() const { return set_breakpoint_ids_; } |
| |
| // Returns the last received resume request sent by the client. |
| const debug_ipc::ResumeRequest& resume_request() const { return resume_request_; } |
| uint32_t resume_count() const { return resume_count_; } |
| |
| // Clears the last resume request and count. |
| void ResetResumeState() { |
| resume_request_ = debug_ipc::ResumeRequest(); |
| resume_count_ = 0; |
| } |
| |
| void AppendProcessRecord(const debug_ipc::ProcessRecord& record) { records_.push_back(record); } |
| |
| // Adds all current system breakpoints to the exception notification. The calling code doesn't |
| // have easy access to the backend IDs so this is where they come from. We assume the breakpoints |
| // will all be hit at the same time (the tests will set them at the same address). |
| void PopulateNotificationWithBreakpoints(debug_ipc::NotifyException* notify) { |
| notify->hit_breakpoints.clear(); |
| for (uint32_t id : set_breakpoint_ids_) { |
| notify->hit_breakpoints.emplace_back(); |
| notify->hit_breakpoints.back().id = id; |
| } |
| } |
| |
| void Resume(const debug_ipc::ResumeRequest& request, |
| fit::callback<void(const Err&, debug_ipc::ResumeReply)> cb) override { |
| resume_count_++; |
| resume_request_ = request; |
| MessageLoop::Current()->PostTask( |
| FROM_HERE, [cb = std::move(cb)]() mutable { cb(Err(), debug_ipc::ResumeReply()); }); |
| } |
| |
| void AddOrChangeBreakpoint( |
| const debug_ipc::AddOrChangeBreakpointRequest& request, |
| fit::callback<void(const Err&, debug_ipc::AddOrChangeBreakpointReply)> cb) override { |
| set_breakpoint_ids_.insert(request.breakpoint.id); |
| MessageLoop::Current()->PostTask(FROM_HERE, [cb = std::move(cb)]() mutable { |
| cb(Err(), debug_ipc::AddOrChangeBreakpointReply()); |
| }); |
| } |
| |
| void RemoveBreakpoint( |
| const debug_ipc::RemoveBreakpointRequest& request, |
| fit::callback<void(const Err&, debug_ipc::RemoveBreakpointReply)> cb) override { |
| set_breakpoint_ids_.erase(request.breakpoint_id); |
| MessageLoop::Current()->PostTask(FROM_HERE, [cb = std::move(cb)]() mutable { |
| cb(Err(), debug_ipc::RemoveBreakpointReply()); |
| }); |
| } |
| |
| void Attach(const debug_ipc::AttachRequest& request, |
| fit::callback<void(const Err&, debug_ipc::AttachReply)> cb) override {} |
| |
| void JobFilter(const debug_ipc::JobFilterRequest& request, |
| fit::callback<void(const Err&, debug_ipc::JobFilterReply)> cb) override; |
| |
| void Status(const debug_ipc::StatusRequest& request, |
| fit::callback<void(const Err&, debug_ipc::StatusReply)> cb) override; |
| |
| private: |
| SessionTest* session_test_; |
| debug_ipc::ResumeRequest resume_request_; |
| uint32_t resume_count_ = 0; |
| std::set<uint32_t> set_breakpoint_ids_; |
| |
| std::vector<debug_ipc::ProcessRecord> records_; |
| }; |
| |
| class SessionThreadObserver : public ThreadObserver { |
| public: |
| void ResetStopState() { |
| stop_count_ = 0; |
| breakpoints_.clear(); |
| } |
| |
| uint32_t stop_count() const { return stop_count_; } |
| |
| // The breakpoints that triggered from the last stop notification. |
| const std::vector<Breakpoint*> breakpoints() const { return breakpoints_; } |
| |
| void OnThreadStopped(Thread* thread, const StopInfo& info) override { |
| stop_count_++; |
| breakpoints_.clear(); |
| for (auto& bp : info.hit_breakpoints) { |
| if (bp) |
| breakpoints_.push_back(bp.get()); |
| } |
| } |
| |
| private: |
| uint32_t stop_count_ = 0; |
| std::vector<Breakpoint*> breakpoints_; |
| }; |
| |
| class SessionTest : public RemoteAPITest { |
| public: |
| SessionTest() = default; |
| ~SessionTest() override = default; |
| |
| SessionSink* sink() { return sink_; } |
| const std::vector<std::pair<std::string, uint64_t>>& existing_processes() const { |
| return existing_processes_; |
| } |
| |
| void AddExistingProcess(const char* name, uint64_t koid) { |
| existing_processes_.emplace_back(std::make_pair(std::string(name), koid)); |
| } |
| |
| protected: |
| std::unique_ptr<RemoteAPI> GetRemoteAPIImpl() override { |
| auto sink = std::make_unique<SessionSink>(this); |
| sink_ = sink.get(); |
| return std::move(sink); |
| } |
| |
| private: |
| SessionSink* sink_; // Owned by the session. |
| std::vector<std::pair<std::string, uint64_t>> existing_processes_; |
| }; |
| |
| void SessionSink::JobFilter(const debug_ipc::JobFilterRequest& request, |
| fit::callback<void(const Err&, debug_ipc::JobFilterReply)> cb) { |
| debug_ipc::JobFilterReply reply; |
| std::vector<zxdb::Target*> targets = session_test_->session().system().GetTargets(); |
| for (const auto& filter : request.filters) { |
| for (const auto& process : session_test_->existing_processes()) { |
| // Basic simulation of filtering. We only expect to find the filter string within the |
| // process name. |
| if (process.first.find(filter) != std::string::npos) { |
| reply.matched_processes.emplace_back(process.second); |
| } |
| } |
| } |
| Err err; |
| cb(err, reply); |
| } |
| |
| void SessionSink::Status(const debug_ipc::StatusRequest& request, |
| fit::callback<void(const Err&, debug_ipc::StatusReply)> cb) { |
| debug_ipc::StatusReply reply = {}; |
| reply.processes = records_; |
| cb(Err(), std::move(reply)); |
| } |
| |
| } // namespace |
| |
| // This is a larger test that covers exception notifications in the Session |
| // object as well as breakpoint controllers and auto continuation. It sets up two breakpoints at the |
| // same address and reports them hit with various combinations of responses from each breakpoint. |
| TEST_F(SessionTest, MultiBreakpointStop) { |
| constexpr uint64_t kProcessKoid = 1234; |
| Process* process = InjectProcess(kProcessKoid); |
| ASSERT_TRUE(process); |
| |
| // Thread with observer. |
| constexpr uint64_t kThreadKoid = 5678; |
| InjectThread(kProcessKoid, kThreadKoid); |
| SessionThreadObserver thread_observer; |
| session().thread_observers().AddObserver(&thread_observer); |
| |
| // An internal breakpoint. |
| Breakpoint* bp_internal = session().system().CreateNewInternalBreakpoint(); |
| |
| // The breakpoints are set at the same address. |
| constexpr uint64_t kAddress = 0x12345678; |
| BreakpointSettings bp_settings; |
| bp_settings.enabled = true; |
| bp_settings.locations.emplace_back(kAddress); |
| bp_internal->SetSettings(bp_settings); |
| |
| // Should have gotten the breakpoint registering itself. |
| ASSERT_EQ(1u, sink()->set_breakpoint_ids().size()); |
| |
| // Now make a user breakpoint at the same place, it should have registered. |
| Breakpoint* bp_user = session().system().CreateNewBreakpoint(); |
| bp_user->SetSettings(bp_settings); |
| ASSERT_EQ(2u, sink()->set_breakpoint_ids().size()); |
| |
| // Do a notification with both breakpoints getting hit. |
| sink()->ResetResumeState(); |
| thread_observer.ResetStopState(); |
| debug_ipc::NotifyException notify; |
| notify.type = debug_ipc::ExceptionType::kSoftwareBreakpoint; |
| notify.thread.process_koid = kProcessKoid; |
| notify.thread.thread_koid = kThreadKoid; |
| notify.thread.state = debug_ipc::ThreadRecord::State::kBlocked; |
| // Don't need stack pointers for this test. |
| notify.thread.frames.emplace_back(kAddress, 0); |
| sink()->PopulateNotificationWithBreakpoints(¬ify); |
| InjectException(notify); |
| |
| // The thread observer should be triggered since there is a regular user breakpoint responsible |
| // for this address. It should be the only one in the notification (internal ones don't get listed |
| // as a stop reason). |
| ASSERT_EQ(1u, thread_observer.breakpoints().size()); |
| EXPECT_EQ(bp_user, thread_observer.breakpoints()[0]); |
| |
| // Delete the breakpoints, they should notify the backend. |
| session().system().DeleteBreakpoint(bp_internal); |
| session().system().DeleteBreakpoint(bp_user); |
| EXPECT_TRUE(sink()->set_breakpoint_ids().empty()); |
| |
| // Cleanup. |
| session().thread_observers().RemoveObserver(&thread_observer); |
| } |
| |
| // Tests that one shot breakpoints get deleted when the agent notifies us that the breakpoint was |
| // hit and deleted. |
| TEST_F(SessionTest, OneShotBreakpointDelete) { |
| // Make a process and thread for notifying about. |
| constexpr uint64_t kProcessKoid = 1234; |
| InjectProcess(kProcessKoid); |
| constexpr uint64_t kThreadKoid = 5678; |
| InjectThread(kProcessKoid, kThreadKoid); |
| |
| // Create a breakpoint. |
| Breakpoint* bp = session().system().CreateNewBreakpoint(); |
| constexpr uint64_t kAddress = 0x12345678; |
| BreakpointSettings settings; |
| settings.enabled = true; |
| settings.locations.emplace_back(kAddress); |
| settings.one_shot = true; |
| bp->SetSettings(settings); |
| |
| // This will tell us if the breakpoint is deleted. |
| fxl::WeakPtr<Breakpoint> weak_bp = bp->GetWeakPtr(); |
| |
| debug_ipc::NotifyException notify; |
| notify.type = debug_ipc::ExceptionType::kSoftwareBreakpoint; |
| notify.thread.process_koid = kProcessKoid; |
| notify.thread.thread_koid = kThreadKoid; |
| notify.thread.state = debug_ipc::ThreadRecord::State::kBlocked; |
| // Don't need stack pointers for this test. |
| notify.thread.frames.emplace_back(kAddress, 0); |
| sink()->PopulateNotificationWithBreakpoints(¬ify); |
| |
| // There should have been one breakpoint populated, mark deleted. |
| ASSERT_EQ(1u, notify.hit_breakpoints.size()); |
| notify.hit_breakpoints[0].should_delete = true; |
| |
| // Notify of the breakpoint hit and delete. It should be deleted. |
| EXPECT_TRUE(weak_bp); |
| InjectException(notify); |
| EXPECT_FALSE(weak_bp); |
| } |
| |
| // Tests breakpoints with stop_mode == kNone. These are auto-resumed by the Session. |
| TEST_F(SessionTest, BreakpointStopNone) { |
| // Make a process and thread for notifying about. |
| constexpr uint64_t kProcessKoid = 1234; |
| InjectProcess(kProcessKoid); |
| constexpr uint64_t kThreadKoid = 5678; |
| InjectThread(kProcessKoid, kThreadKoid); |
| SessionThreadObserver thread_observer; |
| session().thread_observers().AddObserver(&thread_observer); |
| |
| // Create a breakpoint. |
| Breakpoint* bp = session().system().CreateNewBreakpoint(); |
| constexpr uint64_t kAddress = 0x12345678; |
| BreakpointSettings settings; |
| settings.enabled = true; |
| settings.stop_mode = BreakpointSettings::StopMode::kNone; |
| settings.locations.emplace_back(kAddress); |
| bp->SetSettings(settings); |
| |
| debug_ipc::NotifyException notify; |
| notify.type = debug_ipc::ExceptionType::kSoftwareBreakpoint; |
| notify.thread.process_koid = kProcessKoid; |
| notify.thread.thread_koid = kThreadKoid; |
| notify.thread.state = debug_ipc::ThreadRecord::State::kBlocked; |
| // Don't need stack pointers for this test. |
| notify.thread.frames.emplace_back(kAddress, 0); |
| sink()->PopulateNotificationWithBreakpoints(¬ify); |
| |
| // Notify of the breakpoint hit, it should continue the execution without notifying the thread. |
| notify.hit_breakpoints[0].hit_count = 1; |
| InjectException(notify); |
| EXPECT_EQ(1u, sink()->resume_count()); |
| EXPECT_EQ(0u, thread_observer.stop_count()); |
| |
| // Cleanup. |
| session().thread_observers().RemoveObserver(&thread_observer); |
| } |
| |
| // Tests a breakpoint with a hit_mult other than 1 |
| TEST_F(SessionTest, HitMultBreakpoint) { |
| // Make a process and thread for notifying about. |
| constexpr uint64_t kProcessKoid = 1234; |
| InjectProcess(kProcessKoid); |
| constexpr uint64_t kThreadKoid = 5678; |
| InjectThread(kProcessKoid, kThreadKoid); |
| SessionThreadObserver thread_observer; |
| session().thread_observers().AddObserver(&thread_observer); |
| |
| // Create a breakpoint. |
| Breakpoint* bp = session().system().CreateNewBreakpoint(); |
| constexpr uint64_t kAddress = 0x12345678; |
| BreakpointSettings settings; |
| settings.enabled = true; |
| settings.hit_mult = 2; |
| settings.locations.emplace_back(kAddress); |
| bp->SetSettings(settings); |
| |
| debug_ipc::NotifyException notify; |
| notify.type = debug_ipc::ExceptionType::kSoftwareBreakpoint; |
| notify.thread.process_koid = kProcessKoid; |
| notify.thread.thread_koid = kThreadKoid; |
| notify.thread.state = debug_ipc::ThreadRecord::State::kBlocked; |
| // Don't need stack pointers for this test. |
| notify.thread.frames.emplace_back(kAddress, 0); |
| sink()->PopulateNotificationWithBreakpoints(¬ify); |
| |
| // Notify of the breakpoint hit, it should continue the execution without notifying the thread. |
| notify.hit_breakpoints[0].hit_count = 1; |
| InjectException(notify); |
| EXPECT_EQ(1u, bp->GetStats().hit_count); |
| EXPECT_EQ(1u, sink()->resume_count()); |
| EXPECT_EQ(0u, thread_observer.stop_count()); |
| |
| // Notifying again should stop the thread. |
| notify.hit_breakpoints[0].hit_count = 2; |
| InjectException(notify); |
| EXPECT_EQ(2u, bp->GetStats().hit_count); |
| EXPECT_EQ(1u, sink()->resume_count()); |
| EXPECT_EQ(1u, thread_observer.stop_count()); |
| |
| // Cleanup. |
| session().thread_observers().RemoveObserver(&thread_observer); |
| } |
| |
| TEST_F(SessionTest, FilterExistingProcesses) { |
| constexpr uint64_t kJobKoid = 3333; |
| constexpr uint64_t kProcessKoid1 = 1111; |
| constexpr uint64_t kProcessKoid2 = 2222; |
| |
| Job job(&session(), false); |
| job.AttachForTesting(kJobKoid, "job-name"); |
| |
| AddExistingProcess("test_1", kProcessKoid1); |
| AddExistingProcess("test_2", kProcessKoid2); |
| |
| // First, we should have only one unused target. |
| ASSERT_EQ(session().system().GetTargets().size(), 1UL); |
| ASSERT_EQ(session().system().GetTargets()[0]->GetState(), zxdb::Target::State::kNone); |
| |
| Filter* filter = session().system().CreateNewFilter(); |
| filter->SetPattern("test"); |
| filter->SetJob(session().system().GetJobs()[0]); |
| |
| loop().RunUntilNoTasks(); |
| |
| // If both existing processes have been detected, we should have two used targets. |
| ASSERT_EQ(session().system().GetTargets().size(), 2UL); |
| ASSERT_NE(session().system().GetTargets()[0]->GetState(), zxdb::Target::State::kNone); |
| ASSERT_NE(session().system().GetTargets()[1]->GetState(), zxdb::Target::State::kNone); |
| } |
| |
| TEST_F(SessionTest, StatusRequest) { |
| constexpr uint64_t kProcessKoid1 = 1; |
| constexpr uint64_t kProcessKoid2 = 2; |
| const std::string kProcessName1 = "process-1"; |
| const std::string kProcessName2 = "process-2"; |
| |
| sink()->AppendProcessRecord({kProcessKoid1, kProcessName1, {}}); |
| sink()->AppendProcessRecord({kProcessKoid2, kProcessName2, {}}); |
| |
| bool called = false; |
| debug_ipc::StatusReply status = {}; |
| sink()->Status({}, [&called, &status](const Err& err, debug_ipc::StatusReply reply) { |
| called = true; |
| status = std::move(reply); |
| }); |
| |
| ASSERT_TRUE(called); |
| ASSERT_EQ(status.processes.size(), 2u); |
| EXPECT_EQ(status.processes[0].process_koid, kProcessKoid1); |
| EXPECT_EQ(status.processes[0].process_name, kProcessName1); |
| EXPECT_EQ(status.processes[1].process_koid, kProcessKoid2); |
| EXPECT_EQ(status.processes[1].process_name, kProcessName2); |
| } |
| |
| } // namespace zxdb |