Index: unittests/CMakeLists.txt =================================================================== --- unittests/CMakeLists.txt +++ unittests/CMakeLists.txt @@ -68,9 +68,10 @@ add_subdirectory(Symbol) add_subdirectory(SymbolFile) add_subdirectory(Target) +add_subdirectory(tools) add_subdirectory(UnwindAssembly) add_subdirectory(Utility) if(LLDB_CAN_USE_DEBUGSERVER) add_subdirectory(debugserver) -endif() \ No newline at end of file +endif() Index: unittests/tools/CMakeLists.txt =================================================================== --- /dev/null +++ unittests/tools/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(lldb-server) Index: unittests/tools/lldb-server/CMakeLists.txt =================================================================== --- /dev/null +++ unittests/tools/lldb-server/CMakeLists.txt @@ -0,0 +1,13 @@ +function(add_lldb_test_executable test_name) + set(EXCLUDE_FROM_ALL ON) + add_llvm_executable(${test_name} NO_INSTALL_RPATH ${ARGN}) + set(outdir ${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}) + set_output_directory(${test_name} BINARY_DIR ${outdir} LIBRARY_DIR ${outdir}) +endfunction() + +add_lldb_test_executable(thread_inferior inferior/thread_inferior.cpp) + +add_definitions(-DLLDB_SERVER="$") +add_definitions(-DTHREAD_INFERIOR="${CMAKE_CURRENT_BINARY_DIR}/thread_inferior") +add_subdirectory(tests) +add_dependencies(LLDBServerTests thread_inferior) Index: unittests/tools/lldb-server/inferior/thread_inferior.cpp =================================================================== --- /dev/null +++ unittests/tools/lldb-server/inferior/thread_inferior.cpp @@ -0,0 +1,36 @@ +//===-- thread_inferior.cpp -------------------------------------*- C++ -*-===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// + +#include +#include +#include + +#include "llvm/Support/Compiler.h" + +int main(int argc, char* argv[]) +{ + int thread_count = 2; + if (argc > 1) { + thread_count = std::stoi(argv[1], nullptr, 10); + } + + bool delay = true; + std::vector threads; + for (int i = 0; i < thread_count; i++) { + threads.push_back(std::thread([&delay]{while(delay);})); + } + + LLVM_BUILTIN_DEBUGTRAP; + delay = false; + for (std::thread& t : threads) { + t.join(); + } + + return 0; +} Index: unittests/tools/lldb-server/tests/CMakeLists.txt =================================================================== --- /dev/null +++ unittests/tools/lldb-server/tests/CMakeLists.txt @@ -0,0 +1,15 @@ +add_lldb_unittest(LLDBServerTests + TestClient.cpp + MessageObjects.cpp + ThreadIdsInJstopinfoTest.cpp + + LINK_LIBS + lldbHost + lldbCore + lldbInterpreter + lldbTarget + lldbPluginPlatformLinux + lldbPluginProcessGDBRemote + LINK_COMPONENTS + Support + ) Index: unittests/tools/lldb-server/tests/MessageObjects.h =================================================================== --- /dev/null +++ unittests/tools/lldb-server/tests/MessageObjects.h @@ -0,0 +1,117 @@ +//===-- MessageObjects.h ----------------------------------------*- C++ -*-===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// + +#include +#include +#include +#include + +#include "lldb/lldb-types.h" +#include "llvm/Support/Endian.h" +#include "llvm/Support/Error.h" +#include "llvm/ADT/DenseMap.h" + +namespace CommunicationTests { + class ThreadInfo; + typedef llvm::DenseMap ThreadInfoMap; + typedef llvm::DenseMap ULongMap; + +class ParsingError : public llvm::ErrorInfo { +public: + static char ID; + + ParsingError(llvm::StringRef source) : source(source) { } + const std::string& GetSource() { return source; } + + void log(llvm::raw_ostream &OS) const override { + OS << source << " argument was invalid"; + } + + std::error_code convertToErrorCode() const override { + return std::make_error_code(std::errc::invalid_argument); + } + +private: + std::string source; +}; + +class ProcessInfo { +public: + static llvm::Expected> Create(llvm::StringRef response); + pid_t GetPid() const; + llvm::support::endianness GetEndian() const; + +protected: + ProcessInfo(); + +private: + pid_t pid; + pid_t parent_pid; + uid_t real_uid; + gid_t real_gid; + uid_t effective_uid; + gid_t effective_gid; + std::string triple; + llvm::SmallString<16> ostype; + llvm::support::endianness endian; + unsigned int ptrsize; +}; + +class ThreadInfo { +public: + ThreadInfo(); + explicit ThreadInfo(llvm::StringRef name, llvm::StringRef reason, + const ULongMap& registers, unsigned int signal); + + unsigned long ReadRegister(unsigned long register_id) const; + +private: + llvm::StringRef name; + llvm::StringRef reason; + ULongMap registers; + unsigned int signal; +}; + +class JThreadsInfo { +public: + static llvm::Expected> + Create(llvm::StringRef response, llvm::support::endianness endian); + + const ThreadInfoMap& GetThreadInfos() const; + +protected: + JThreadsInfo(); + +private: + ThreadInfoMap thread_infos; +}; + +class StopReply { +public: + static llvm::Expected> + Create(llvm::StringRef response, llvm::support::endianness endian); + const ULongMap& GetThreadPcs() const; + +protected: + StopReply(); + +private: + void ParseResponse(llvm::StringRef response, + llvm::support::endianness endian); + unsigned int signal; + lldb::tid_t thread; + llvm::StringRef name; + ULongMap thread_pcs; + ULongMap registers; + llvm::StringRef reason; +}; + +// Common functions for parsing packet data. +llvm::StringMap SplitPairList(llvm::StringRef s); +} Index: unittests/tools/lldb-server/tests/MessageObjects.cpp =================================================================== --- /dev/null +++ unittests/tools/lldb-server/tests/MessageObjects.cpp @@ -0,0 +1,246 @@ +//===-- MessageObjects.cpp --------------------------------------*- C++ -*-===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// + +#include +#include + +#include "lldb/Core/StructuredData.h" +#include "llvm/ADT/StringExtras.h" + +#include "gtest/gtest.h" +#include "MessageObjects.h" + +using namespace lldb_private; +using namespace llvm; +using namespace llvm::support; +namespace CommunicationTests { +char ParsingError::ID = 1; + +Expected> ProcessInfo::Create(StringRef response) { + std::unique_ptr process_info = + std::unique_ptr(new ProcessInfo); + auto elements = SplitPairList(response); + if (elements["pid"].getAsInteger(16, process_info->pid)) { + return make_error("ProcessInfo: pid"); + } + if (elements["parent-pid"].getAsInteger(16, process_info->parent_pid)) { + return make_error("ProcessInfo: parent-pid"); + } + if (elements["real-uid"].getAsInteger(16, process_info->real_uid)) { + return make_error("ProcessInfo: real-uid"); + } + if (elements["real-gid"].getAsInteger(16, process_info->real_gid)) { + return make_error("ProcessInfo: real-uid"); + } + if (elements["effective-uid"].getAsInteger(16, process_info->effective_uid)) { + return make_error("ProcessInfo: effective-uid"); + } + if (elements["effective-gid"].getAsInteger(16, process_info->effective_gid)) { + return make_error("ProcessInfo: effective-gid"); + } + if (elements["ptrsize"].getAsInteger(10, process_info->ptrsize)) { + return make_error("ProcessInfo: ptrsize"); + } + + process_info->triple = fromHex(elements["triple"]); + process_info->endian = llvm::StringSwitch(elements["endian"]) + .Case("little", support::little) + .Case("big", support::big) + .Default(support::native); + + return std::move(process_info); +} + +pid_t ProcessInfo::GetPid() const { + return pid; +} + +endianness ProcessInfo::GetEndian() const { + return endian; +} + +ProcessInfo::ProcessInfo() { +} + +//====== ThreadInfo ============================================================ +ThreadInfo::ThreadInfo() { + // For map [] operators. +} + +ThreadInfo::ThreadInfo(StringRef name, StringRef reason, + const ULongMap& registers, unsigned int signal) : + name(name), reason(reason), registers(registers), signal(signal) { +} + +unsigned long ThreadInfo::ReadRegister(unsigned long register_id) const { + return registers.lookup(register_id); +} + +//====== JThreadsInfo ========================================================== +Expected> +JThreadsInfo::Create(StringRef response, endianness endian) { + auto jthreads_info = std::unique_ptr(new JThreadsInfo); + + StructuredData::ObjectSP json = StructuredData::ParseJSON(response); + StructuredData::Array* array = json->GetAsArray(); + if (!array) { + return make_error("JThreadsInfo: JSON array"); + } + + for (size_t i = 0; i < array->GetSize(); i++) { + StructuredData::Dictionary* thread_info; + array->GetItemAtIndexAsDictionary(i, thread_info); + if (!thread_info) { + return make_error(formatv("JThreadsInfo: JSON obj at {0}", + i).str()); + } + + StringRef name, reason; + thread_info->GetValueForKeyAsString("name", name); + thread_info->GetValueForKeyAsString("reason", reason); + unsigned long signal; + thread_info->GetValueForKeyAsInteger("signal", signal); + unsigned long tid; + thread_info->GetValueForKeyAsInteger("tid", tid); + + StructuredData::Dictionary* register_dict; + thread_info->GetValueForKeyAsDictionary("registers", register_dict); + if (!register_dict) { + return make_error("JThreadsInfo: registers JSON obj"); + } + + ULongMap registers; + + auto keys_obj = register_dict->GetKeys(); + auto keys = keys_obj->GetAsArray(); + for (size_t i = 0; i < keys->GetSize(); i++) { + StringRef key_str, value_str; + keys->GetItemAtIndexAsString(i, key_str); + register_dict->GetValueForKeyAsString(key_str, value_str); + unsigned int register_id; + unsigned long register_value; + if (key_str.getAsInteger(10, register_id)) { + return make_error( + formatv("JThreadsInfo: register key[{0}]", i).str()); + } + if (value_str.getAsInteger(16, register_value)) { + return make_error( + formatv("JThreadsInfo: register value[{0}]", i).str()); + } + if (endian == little) { + sys::swapByteOrder(register_value); + } + + registers[register_id] = register_value; + } + + jthreads_info->thread_infos[tid] = + ThreadInfo(name, reason, registers, signal); + } + + return std::move(jthreads_info); +} + +const ThreadInfoMap& JThreadsInfo::GetThreadInfos() const { + return thread_infos; +} + +JThreadsInfo::JThreadsInfo() { +} + +//====== StopReply ============================================================= +StopReply::StopReply() { +} + +const ULongMap& StopReply::GetThreadPcs() const { + return thread_pcs; +} + +Expected> +StopReply::Create(StringRef response, llvm::support::endianness endian) { + auto stop_reply = std::unique_ptr(new StopReply); + + auto elements = SplitPairList(response); + stop_reply->name = elements["name"]; + stop_reply->reason = elements["reason"]; + + SmallVector threads; + SmallVector pcs; + elements["threads"].split(threads, ','); + elements["thread-pcs"].split(pcs, ','); + if (threads.size() != pcs.size()) { + return make_error("StopReply: thread/PC count mismatch"); + } + + for (unsigned int i = 0; i < threads.size(); i++) { + lldb::tid_t thread_id; + unsigned long pc; + if (threads[i].getAsInteger(16, thread_id)) { + return make_error( + formatv("StopReply: thread ID at [{0}].", i).str()); + } + if (pcs[i].getAsInteger(16, pc)) { + return make_error( + formatv("StopReply: thread PC at [{0}].", i).str()); + } + + stop_reply->thread_pcs[thread_id] = pc; + } + + unsigned int register_id = 0; + while (true) { + std::stringstream ss; + ss << std::hex << std::setw(2) << std::setfill('0') << register_id; + std::string hex_id = ss.str(); + if (elements.find(hex_id) != elements.end()) { + unsigned long register_value; + if (elements[hex_id].getAsInteger(16, register_value)) { + return make_error( + formatv("StopReply: Register value at [{0}].", register_id).str()); + } + if (endian == little) { + sys::swapByteOrder(register_value); + } + + stop_reply->registers[register_id++] = register_value; + } + else { + break; + } + } + + for (auto i = elements.begin(); i != elements.end(); i++) { + StringRef key = i->getKey(); + StringRef val = i->getValue(); + if (key.size() >= 9 && key[0] == 'T' && key.substr(3, 6) == "thread") { + if (val.getAsInteger(16, stop_reply->thread)) { + return make_error("StopReply: thread id"); + } + if (key.substr(1, 2).getAsInteger(16, stop_reply->signal)) { + return make_error("StopReply: stop signal"); + } + } + } + + return std::move(stop_reply); +} + +//====== Globals =============================================================== +StringMap SplitPairList(StringRef str) { + SmallVector elements; + str.split(elements, ';'); + + StringMap pairs; + for (StringRef s : elements) { + pairs.insert(s.split(':')); + } + + return pairs; +} +} Index: unittests/tools/lldb-server/tests/TestClient.h =================================================================== --- /dev/null +++ unittests/tools/lldb-server/tests/TestClient.h @@ -0,0 +1,57 @@ +//===-- TestClient.h --------------------------------------------*- C++ -*-===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// + +#include +#include + +#include "lldb/Core/ArchSpec.h" +#include "lldb/Target/ProcessLaunchInfo.h" + +#include "Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.h" + +#include "MessageObjects.h" + +namespace CommunicationTests { +// TODO: Make the test client an abstract base class, with different children +// for different types of connections: llgs v. debugserver +class TestClient : + public lldb_private::process_gdb_remote::GDBRemoteCommunicationClient { +public: + TestClient(const std::string& test_name, const std::string& test_case_name); + virtual ~TestClient(); + void StartDebugger(); + void StopDebugger(); + void SetInferior(llvm::ArrayRef inferior_args); + void ListThreadsInStopReply(); + void SetBreakpoint(unsigned long address); + void Continue(unsigned long thread_id = 0); + const ProcessInfo& GetProcessInfo(); + std::unique_ptr GetJThreadsInfo(); + const StopReply& GetLatestStopReply(); + void SendMessage(const std::string& message); + void SendMessage(const std::string& message, std::string& response_string); + void SendMessage(const std::string& message, std::string& response_string, + PacketResult expected_result); + unsigned int GetPcRegisterId(); + +private: + void GenerateConnectionAddress(std::string& address); + std::string GenerateLogFileName(const lldb_private::ArchSpec& arch) const; + std::string FormatFailedResult(const std::string& message, + lldb_private::process_gdb_remote::GDBRemoteCommunication::PacketResult + result); + + std::unique_ptr process_info; + std::unique_ptr stop_reply; + lldb_private::ProcessLaunchInfo server_process_info; + std::string test_name; + std::string test_case_name; + unsigned int pc_register; +}; +} Index: unittests/tools/lldb-server/tests/TestClient.cpp =================================================================== --- /dev/null +++ unittests/tools/lldb-server/tests/TestClient.cpp @@ -0,0 +1,250 @@ +//===-- TestClient.cpp ------------------------------------------*- C++ -*-===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// + +#include +#include +#include +#include +#include + +#include "gtest/gtest.h" +#include "TestClient.h" + +#include "lldb/Core/ArchSpec.h" +#include "lldb/Host/common/TCPSocket.h" +#include "lldb/Host/HostInfo.h" +#include "lldb/Host/posix/ConnectionFileDescriptorPosix.h" +#include "lldb/Host/posix/ProcessLauncherPosix.h" +#include "lldb/Interpreter/Args.h" +#include "lldb/Target/ProcessLaunchInfo.h" +#include "llvm/ADT/StringExtras.h" + +using namespace lldb; +using namespace lldb_private; +using namespace llvm; + +namespace CommunicationTests { +const char* LOCALHOST = "127.0.0.1"; + +TestClient::TestClient(const std::string& test_name, + const std::string& test_case_name) +: test_name(test_name), test_case_name(test_case_name), pc_register(UINT_MAX) { + HostInfo::Initialize(); +} + +TestClient::~TestClient() { +} + +void TestClient::StartDebugger() { + const ArchSpec& arch_spec = HostInfo::GetArchitecture(); + Args args; + args.AppendArgument(LLDB_SERVER); + args.AppendArgument("gdbserver"); + args.AppendArgument("--log-file=" + GenerateLogFileName(arch_spec)); + args.AppendArgument("--log-channels=gdb-remote packets"); + args.AppendArgument("--reverse-connect"); + std::string connectionAddress; + GenerateConnectionAddress(connectionAddress); + + args.AppendArgument(connectionAddress); + server_process_info.SetArchitecture(arch_spec); + server_process_info.SetArguments(args, true); + Status status = Host::LaunchProcess(server_process_info); + // TODO: Move ASSERTS to test, return an Error. + ASSERT_FALSE(status.Fail()) + << "Failure to launch lldb server: " << status.AsCString(); + + sleep(5); // TODO: Sleep is bad. Can I wait for it to start? + SendAck(); // Send this as a handshake. +} + +void TestClient::StopDebugger() { + std::string response; + SendMessage("k", response, PacketResult::ErrorDisconnected); +} + +void TestClient::SetInferior(llvm::ArrayRef inferior_args) { + std::stringstream command; + command << "A"; + for (size_t i = 0; i < inferior_args.size(); i++) { + if (i > 0) command << ','; + std::string hex_encoded = toHex(inferior_args[i]); + command << hex_encoded.size() << ',' << i << ',' << hex_encoded; + } + + SendMessage(command.str()); + SendMessage("qLaunchSuccess"); + std::string response; + SendMessage("qProcessInfo", response); + auto create_or_error = ProcessInfo::Create(response); + if (auto create_error = create_or_error.takeError()) { + FAIL() << toString(std::move(create_error)); + } + + process_info = std::move(*create_or_error); +} + +void TestClient::ListThreadsInStopReply() { + SendMessage("QListThreadsInStopReply"); +} + +void TestClient::SetBreakpoint(unsigned long address) { + std::stringstream command; + command << "Z0," << std::hex << address << ",1"; + SendMessage(command.str()); +} + +void TestClient::Continue(unsigned long thread_id) { + // TODO!!!! Move ASSERTs to test. Return an Error + // ASSERT_TRUE(process_info.get()) + // << "Continue() called before process_info initialized."; + + if (thread_id == 0) { + thread_id = process_info->GetPid(); + } + + std::string message = formatv("vCont;c:{0:x-}", thread_id).str(); + std::string response; + SendMessage(message, response); + auto creation = StopReply::Create(response, process_info->GetEndian()); + if (auto create_error = creation.takeError()) { + FAIL() << toString(std::move(create_error)); + } + + stop_reply = std::move(*creation); +} + +const ProcessInfo& TestClient::GetProcessInfo() { + return *(process_info.get()); +} + +std::unique_ptr TestClient::GetJThreadsInfo() { + std::string response; + SendMessage("jThreadsInfo", response); + auto creation = JThreadsInfo::Create(response, process_info->GetEndian()); + // TODO: Move all ASSERT/FAIL calls out. + // Replace with returning an Error or Expected + if (auto create_error = creation.takeError()) { + // FAIL() << toString(std::move(create_error)); + return std::unique_ptr(nullptr); + } + + return std::move(*creation); +} + +const StopReply& TestClient::GetLatestStopReply() { + return *(stop_reply.get()); +} + +void TestClient::SendMessage(const std::string& message) { + std::string dummy_string; + SendMessage(message, dummy_string); +} + +void TestClient::SendMessage(const std::string& message, + std::string& response_string) { + SendMessage(message, response_string, PacketResult::Success); + // TODO: Move ASSERTS to test, return an Error. + ASSERT_NE(response_string[0], 'E') + << "Error " << response_string << " while sending message: " << message; +} + +void TestClient::SendMessage(const std::string& message, + std::string& response_string, + PacketResult expected_result) { + StringExtractorGDBRemote response; + PacketResult result = SendPacketAndWaitForResponse(message, response, false); + response.GetEscapedBinaryData(response_string); + // TODO: Move ASSERTS to test, return an Error. + ASSERT_EQ(result, expected_result) + << FormatFailedResult(message, result); +} + +unsigned int TestClient::GetPcRegisterId() { + if (pc_register != UINT_MAX) { + return pc_register; + } + + for (unsigned int register_id = 0; ; register_id++) { + std::string message = formatv("qRegisterInfo{0:x-}", register_id).str(); + // This will throw if we scan all registers and never get the + // expected result. Is that okay? Or is there a situation where + // no result is valid? + std::string response; + SendMessage(message, response); + auto elements = SplitPairList(response); + if (elements["alt-name"] == "pc" || elements["generic"] == "pc") { + pc_register = register_id; + break; + } + } + + return pc_register; +} + +void TestClient::GenerateConnectionAddress(std::string& address) { + StartListenThread(LOCALHOST, 0); + auto connection = (ConnectionFileDescriptor*)GetConnection(); + uint16_t listening_port = connection->GetListeningPort(UINT32_MAX); + // TODO: Move ASSERTS to test, return an Error. + ASSERT_GT(listening_port, 0) << "GetListeningPort failed."; + + raw_string_ostream address_stream(address); + address_stream << LOCALHOST << ":" << listening_port; + address_stream.str(); +} + +std::string TestClient::GenerateLogFileName(const ArchSpec& arch) const { + std::string log_file_name; + raw_string_ostream log_file(log_file_name); + log_file << "lldb-test-traces/lldb-" << test_case_name << '-' << test_name + << '-' << arch.GetArchitectureName() << ".log"; + log_file.str(); + return log_file_name; +} + +std::string TestClient::FormatFailedResult(const std::string& message, + PacketResult result) { + std::string formatted_error; + raw_string_ostream error_stream(formatted_error); + error_stream << "Failure sending message: " << message << " Result: "; + + switch (result) { + case PacketResult::ErrorSendFailed: + error_stream << "ErrorSendFailed"; + break; + case PacketResult::ErrorSendAck: + error_stream << "ErrorSendAck"; + break; + case PacketResult::ErrorReplyFailed: + error_stream << "ErrorReplyFailed"; + break; + case PacketResult::ErrorReplyTimeout: + error_stream << "ErrorReplyTimeout"; + break; + case PacketResult::ErrorReplyInvalid: + error_stream << "ErrorReplyInvalid"; + break; + case PacketResult::ErrorReplyAck: + error_stream << "ErrorReplyAck"; + break; + case PacketResult::ErrorDisconnected: + error_stream << "ErrorDisconnected"; + break; + case PacketResult::ErrorNoSequenceLock: + error_stream << "ErrorNoSequenceLock"; + break; + default: + error_stream << "Unknown Error"; + } + + error_stream.str(); + return formatted_error; +} +} Index: unittests/tools/lldb-server/tests/ThreadIdsInJstopinfoTest.cpp =================================================================== --- /dev/null +++ unittests/tools/lldb-server/tests/ThreadIdsInJstopinfoTest.cpp @@ -0,0 +1,50 @@ +//===-- ThreadsInJstopinfoTest.cpp ------------------------------*- C++ -*-===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// + +#include + +#include "gtest/gtest.h" +#include "TestClient.h" + +using namespace CommunicationTests; + +TEST(ThreadsInJstopinfoTest, TestStopReplyContainsThreadPcsLlgs) { + std::vector inferior_args; + // This inferior spawns N threads, then forces a break. + inferior_args.push_back(THREAD_INFERIOR); + inferior_args.push_back("4"); + + auto test_info = ::testing::UnitTest::GetInstance()->current_test_info(); + + TestClient client(test_info->name(), test_info->test_case_name()); + client.StartDebugger(); + client.SetInferior(inferior_args); + client.ListThreadsInStopReply(); + client.Continue(); + unsigned int pc_reg = client.GetPcRegisterId(); + + auto jthreads_info = client.GetJThreadsInfo(); + ASSERT_TRUE(jthreads_info.get()) << "Error reading JThreadInfo."; + + auto stop_reply = client.GetLatestStopReply(); + auto stop_reply_pcs = stop_reply.GetThreadPcs(); + auto thread_infos = jthreads_info->GetThreadInfos(); + ASSERT_EQ(stop_reply_pcs.size(), thread_infos.size()) + << "Thread count mismatch."; + + for (auto stop_reply_pc : stop_reply_pcs) { + unsigned long tid = stop_reply_pc.first; + ASSERT_TRUE(thread_infos.find(tid) != thread_infos.end()) + << "Thread ID: " << tid << " not in JThreadsInfo."; + ASSERT_EQ(stop_reply_pcs[tid], thread_infos[tid].ReadRegister(pc_reg)) + << "Mismatched PC for thread: " << tid; + } + + client.StopDebugger(); +}