https://github.com/JDevlieghere created https://github.com/llvm/llvm-project/pull/202752
Add unit-test coverage for the MCP protocol types and server under source/Protocol/MCP and the MCP plugin under source/Plugins/Protocol/MCP. The Server handlers run over the in-memory TestTransport, which gains SimulateError/SimulateClosed/SetRegisterMessageHandlerShouldFail helpers to drive the handler lifecycle without a real socket. Code that touches the filesystem or otherwise requires mucking with the test environment are deliberately left uncovered until those layers can be mocked. Assisted-by: Claude >From e40ae165f9c8668fb154eaf7feea47a8e5d10316 Mon Sep 17 00:00:00 2001 From: Jonas Devlieghere <[email protected]> Date: Tue, 9 Jun 2026 12:37:58 -0700 Subject: [PATCH] [lldb] Add unit tests for the MCP server Add unit-test coverage for the MCP protocol types and server under source/Protocol/MCP and the MCP plugin under source/Plugins/Protocol/MCP. The Server handlers run over the in-memory TestTransport, which gains SimulateError/SimulateClosed/SetRegisterMessageHandlerShouldFail helpers to drive the handler lifecycle without a real socket. Code that touches the filesystem or otherwise requires mucking with the test environment are deliberately left uncovered until those layers can be mocked. Assisted-by: Claude --- lldb/unittests/Protocol/CMakeLists.txt | 16 + lldb/unittests/Protocol/MCPErrorTest.cpp | 73 +++ lldb/unittests/Protocol/MCPPluginTest.cpp | 414 ++++++++++++++++++ lldb/unittests/Protocol/MCPServerInfoTest.cpp | 40 ++ lldb/unittests/Protocol/MCPTransportTest.cpp | 40 ++ .../Protocol/ProtocolMCPServerTest.cpp | 161 +++++++ lldb/unittests/Protocol/ProtocolMCPTest.cpp | 392 +++++++++++++++++ .../Host/JSONTransportTestUtilities.h | 25 ++ 8 files changed, 1161 insertions(+) create mode 100644 lldb/unittests/Protocol/MCPErrorTest.cpp create mode 100644 lldb/unittests/Protocol/MCPPluginTest.cpp create mode 100644 lldb/unittests/Protocol/MCPServerInfoTest.cpp create mode 100644 lldb/unittests/Protocol/MCPTransportTest.cpp diff --git a/lldb/unittests/Protocol/CMakeLists.txt b/lldb/unittests/Protocol/CMakeLists.txt index f877517ea233d..78953b1b95ae8 100644 --- a/lldb/unittests/Protocol/CMakeLists.txt +++ b/lldb/unittests/Protocol/CMakeLists.txt @@ -1,10 +1,26 @@ add_lldb_unittest(ProtocolTests + MCPErrorTest.cpp + MCPPluginTest.cpp + MCPServerInfoTest.cpp + MCPTransportTest.cpp ProtocolMCPTest.cpp ProtocolMCPServerTest.cpp + LINK_COMPONENTS + Support LINK_LIBS + lldbCore + lldbCommands lldbHost + lldbInterpreter lldbProtocolMCP + lldbPluginPlatformMacOSX + lldbPluginProtocolServerMCP + lldbPluginScriptInterpreterNone + lldbSymbol + lldbTarget lldbUtility + lldbUtilityHelpers + lldbHostHelpers LLVMTestingSupport ) diff --git a/lldb/unittests/Protocol/MCPErrorTest.cpp b/lldb/unittests/Protocol/MCPErrorTest.cpp new file mode 100644 index 0000000000000..7bf01f1042913 --- /dev/null +++ b/lldb/unittests/Protocol/MCPErrorTest.cpp @@ -0,0 +1,73 @@ +//===----------------------------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "lldb/Protocol/MCP/MCPError.h" +#include "llvm/Support/Error.h" +#include "llvm/Support/raw_ostream.h" +#include "llvm/Testing/Support/Error.h" +#include "gtest/gtest.h" +#include <system_error> + +using namespace llvm; +using namespace lldb_protocol::mcp; + +static std::string Log(const ErrorInfoBase &info) { + std::string message; + raw_string_ostream os(message); + info.log(os); + return message; +} + +TEST(MCPErrorTest, DefaultErrorCode) { + MCPError error("something went wrong"); + EXPECT_EQ(error.getMessage(), "something went wrong"); + EXPECT_EQ(Log(error), "something went wrong"); + + std::error_code ec = error.convertToErrorCode(); + EXPECT_EQ(ec.value(), MCPError::kInternalError); + EXPECT_EQ(ec.category(), std::generic_category()); +} + +TEST(MCPErrorTest, CustomErrorCode) { + MCPError error("not found", MCPError::kResourceNotFound); + EXPECT_EQ(error.getMessage(), "not found"); + + std::error_code ec = error.convertToErrorCode(); + EXPECT_EQ(ec.value(), MCPError::kResourceNotFound); + EXPECT_EQ(ec.category(), std::generic_category()); +} + +TEST(MCPErrorTest, AsLLVMError) { + Error error = make_error<MCPError>("boom", 42); + EXPECT_TRUE(error.isA<MCPError>()); + + std::error_code ec = errorToErrorCode(std::move(error)); + EXPECT_EQ(ec.value(), 42); +} + +TEST(MCPErrorTest, FailedWithMessage) { + EXPECT_THAT_ERROR(make_error<MCPError>("explosion"), + FailedWithMessage("explosion")); +} + +TEST(MCPErrorTest, UnsupportedURILog) { + UnsupportedURI error("lldb://debugger/0"); + EXPECT_EQ(Log(error), "unsupported uri: lldb://debugger/0"); +} + +TEST(MCPErrorTest, UnsupportedURIErrorCode) { + UnsupportedURI error("lldb://debugger/0"); + EXPECT_EQ(error.convertToErrorCode(), inconvertibleErrorCode()); +} + +TEST(MCPErrorTest, UnsupportedURIAsLLVMError) { + Error error = make_error<UnsupportedURI>("lldb://foo"); + EXPECT_TRUE(error.isA<UnsupportedURI>()); + EXPECT_FALSE(error.isA<MCPError>()); + consumeError(std::move(error)); +} diff --git a/lldb/unittests/Protocol/MCPPluginTest.cpp b/lldb/unittests/Protocol/MCPPluginTest.cpp new file mode 100644 index 0000000000000..97850d173719c --- /dev/null +++ b/lldb/unittests/Protocol/MCPPluginTest.cpp @@ -0,0 +1,414 @@ +//===----------------------------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "Plugins/Platform/MacOSX/PlatformMacOSX.h" +#include "Plugins/Platform/MacOSX/PlatformRemoteMacOSX.h" +#include "Plugins/Protocol/MCP/ProtocolServerMCP.h" +#include "Plugins/Protocol/MCP/Resource.h" +#include "Plugins/Protocol/MCP/Tool.h" +#include "Plugins/ScriptInterpreter/None/ScriptInterpreterNone.h" +#include "TestingSupport/SubsystemRAII.h" +#include "TestingSupport/TestUtilities.h" +#include "lldb/Core/Debugger.h" +#include "lldb/Core/Module.h" +#include "lldb/Core/ProtocolServer.h" +#include "lldb/Host/FileSystem.h" +#include "lldb/Host/HostInfo.h" +#include "lldb/Host/Socket.h" +#include "lldb/Protocol/MCP/MCPError.h" +#include "lldb/Target/Platform.h" +#include "lldb/Target/Target.h" +#include "lldb/Utility/ArchSpec.h" +#include "lldb/Utility/FileSpec.h" +#include "llvm/Support/Error.h" +#include "llvm/Support/FormatVariadic.h" +#include "llvm/Support/JSON.h" +#include "llvm/Testing/Support/Error.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include <memory> +#include <mutex> +#include <optional> +#include <string> +#include <vector> + +using namespace llvm; +using namespace lldb; +using namespace lldb_private; +using namespace lldb_private::mcp; +using namespace lldb_protocol::mcp; + +#ifndef _WIN32 + +namespace { +class MCPPluginTest : public testing::Test { +public: + SubsystemRAII<FileSystem, HostInfo, PlatformMacOSX, ScriptInterpreterNone, + Socket> + subsystems; + DebuggerSP m_debugger_sp; + + void SetUp() override { + std::call_once(TestUtilities::g_debugger_initialize_flag, + []() { Debugger::Initialize(nullptr); }); + ArchSpec arch("x86_64-apple-macosx-"); + Platform::SetHostPlatform( + PlatformRemoteMacOSX::CreateInstance(true, &arch)); + m_debugger_sp = Debugger::CreateInstance(); + } + + void TearDown() override { + Debugger::Destroy(m_debugger_sp); + m_debugger_sp.reset(); + } + + TargetSP CreateTarget() { + ArchSpec arch("x86_64-apple-macosx-"); + PlatformSP platform_sp; + TargetSP target_sp; + m_debugger_sp->GetTargetList().CreateTarget( + *m_debugger_sp, "", arch, eLoadDependentsNo, platform_sp, target_sp); + return target_sp; + } + + TargetSP CreateTargetWithExecutable(StringRef path) { + TargetSP target_sp = CreateTarget(); + ArchSpec arch("x86_64-apple-macosx-"); + // Use the FileSpec/ArchSpec constructor so the module keeps its file spec + // even though the path doesn't point at a real object file. + ModuleSP module_sp = std::make_shared<Module>(FileSpec(path), arch); + target_sp->SetExecutableModule(module_sp, eLoadDependentsNo); + return target_sp; + } +}; + +void ExpectUnsupportedURI(Expected<ReadResourceResult> result) { + ASSERT_FALSE(static_cast<bool>(result)); + llvm::Error err = result.takeError(); + EXPECT_TRUE(err.isA<UnsupportedURI>()); + consumeError(std::move(err)); +} +} // namespace + +//===----------------------------------------------------------------------===// +// DebuggerResourceProvider +//===----------------------------------------------------------------------===// + +TEST_F(MCPPluginTest, GetResources) { + CreateTarget(); + CreateTargetWithExecutable("/tmp/lldb-mcp-test/my_executable"); + + DebuggerResourceProvider provider; + std::vector<Resource> resources = provider.GetResources(); + + std::string debugger_uri = + formatv("lldb://debugger/{0}", m_debugger_sp->GetID()).str(); + std::string target0_uri = + formatv("lldb://debugger/{0}/target/0", m_debugger_sp->GetID()).str(); + std::string target1_uri = + formatv("lldb://debugger/{0}/target/1", m_debugger_sp->GetID()).str(); + + std::vector<std::string> uris; + std::string exe_target_name; + for (const Resource &resource : resources) { + uris.push_back(resource.uri); + if (resource.uri == target1_uri) + exe_target_name = resource.name; + } + + EXPECT_THAT(uris, testing::Contains(debugger_uri)); + EXPECT_THAT(uris, testing::Contains(target0_uri)); + EXPECT_THAT(uris, testing::Contains(target1_uri)); + // The target with an executable module is named after the executable. + EXPECT_EQ(exe_target_name, "my_executable"); +} + +TEST_F(MCPPluginTest, ReadResourceUnsupportedScheme) { + DebuggerResourceProvider provider; + ExpectUnsupportedURI(provider.ReadResource("http://example.com")); +} + +TEST_F(MCPPluginTest, ReadResourceTooFewComponents) { + DebuggerResourceProvider provider; + ExpectUnsupportedURI(provider.ReadResource("lldb://x")); +} + +TEST_F(MCPPluginTest, ReadResourceNotDebugger) { + DebuggerResourceProvider provider; + ExpectUnsupportedURI(provider.ReadResource("lldb://session/0")); +} + +TEST_F(MCPPluginTest, ReadResourceInvalidDebuggerId) { + DebuggerResourceProvider provider; + EXPECT_THAT_EXPECTED( + provider.ReadResource("lldb://debugger/abc"), + FailedWithMessage("invalid debugger id 'abc': debugger/abc")); +} + +TEST_F(MCPPluginTest, ReadResourceUnknownDebugger) { + DebuggerResourceProvider provider; + EXPECT_THAT_EXPECTED(provider.ReadResource("lldb://debugger/999999"), + FailedWithMessage("invalid debugger id: 999999")); +} + +TEST_F(MCPPluginTest, ReadResourceDebugger) { + CreateTarget(); + + DebuggerResourceProvider provider; + std::string uri = + formatv("lldb://debugger/{0}", m_debugger_sp->GetID()).str(); + Expected<ReadResourceResult> result = provider.ReadResource(uri); + ASSERT_THAT_EXPECTED(result, Succeeded()); + ASSERT_EQ(result->contents.size(), 1u); + EXPECT_EQ(result->contents[0].uri, uri); + EXPECT_EQ(result->contents[0].mimeType, "application/json"); + + Expected<json::Value> json = json::parse(result->contents[0].text); + ASSERT_THAT_EXPECTED(json, Succeeded()); + const json::Object *obj = json->getAsObject(); + ASSERT_NE(obj, nullptr); + EXPECT_EQ(obj->getInteger("debugger_id"), + static_cast<int64_t>(m_debugger_sp->GetID())); + EXPECT_EQ(obj->getInteger("num_targets"), 1); +} + +TEST_F(MCPPluginTest, ReadResourceTargetNotTarget) { + DebuggerResourceProvider provider; + std::string uri = + formatv("lldb://debugger/{0}/session/0", m_debugger_sp->GetID()).str(); + ExpectUnsupportedURI(provider.ReadResource(uri)); +} + +TEST_F(MCPPluginTest, ReadResourceInvalidTargetId) { + DebuggerResourceProvider provider; + std::string uri = + formatv("lldb://debugger/{0}/target/abc", m_debugger_sp->GetID()).str(); + std::string path = + formatv("debugger/{0}/target/abc", m_debugger_sp->GetID()).str(); + EXPECT_THAT_EXPECTED( + provider.ReadResource(uri), + FailedWithMessage(formatv("invalid target id 'abc': {0}", path).str())); +} + +TEST_F(MCPPluginTest, ReadResourceTargetUnknownDebugger) { + DebuggerResourceProvider provider; + EXPECT_THAT_EXPECTED(provider.ReadResource("lldb://debugger/999999/target/0"), + FailedWithMessage("invalid debugger id: 999999")); +} + +TEST_F(MCPPluginTest, ReadResourceUnknownTarget) { + DebuggerResourceProvider provider; + std::string uri = + formatv("lldb://debugger/{0}/target/999", m_debugger_sp->GetID()).str(); + EXPECT_THAT_EXPECTED(provider.ReadResource(uri), + FailedWithMessage("invalid target idx: 999")); +} + +TEST_F(MCPPluginTest, ReadResourceTarget) { + CreateTarget(); + + DebuggerResourceProvider provider; + std::string uri = + formatv("lldb://debugger/{0}/target/0", m_debugger_sp->GetID()).str(); + Expected<ReadResourceResult> result = provider.ReadResource(uri); + ASSERT_THAT_EXPECTED(result, Succeeded()); + ASSERT_EQ(result->contents.size(), 1u); + + Expected<json::Value> json = json::parse(result->contents[0].text); + ASSERT_THAT_EXPECTED(json, Succeeded()); + const json::Object *obj = json->getAsObject(); + ASSERT_NE(obj, nullptr); + EXPECT_EQ(obj->getInteger("debugger_id"), + static_cast<int64_t>(m_debugger_sp->GetID())); + EXPECT_EQ(obj->getInteger("target_idx"), 0); + // No executable module: there is no "path" entry. + EXPECT_EQ(obj->get("path"), nullptr); +} + +TEST_F(MCPPluginTest, ReadResourceTargetWithExecutable) { + CreateTargetWithExecutable("/tmp/lldb-mcp-test/my_executable"); + + DebuggerResourceProvider provider; + std::string uri = + formatv("lldb://debugger/{0}/target/0", m_debugger_sp->GetID()).str(); + Expected<ReadResourceResult> result = provider.ReadResource(uri); + ASSERT_THAT_EXPECTED(result, Succeeded()); + ASSERT_EQ(result->contents.size(), 1u); + + Expected<json::Value> json = json::parse(result->contents[0].text); + ASSERT_THAT_EXPECTED(json, Succeeded()); + const json::Object *obj = json->getAsObject(); + ASSERT_NE(obj, nullptr); + EXPECT_EQ(obj->getString("path"), "/tmp/lldb-mcp-test/my_executable"); +} + +//===----------------------------------------------------------------------===// +// CommandTool +//===----------------------------------------------------------------------===// + +TEST_F(MCPPluginTest, CommandToolRequiresArguments) { + CommandTool tool("command", "Run an lldb command."); + ToolArguments args; // std::monostate + EXPECT_THAT_EXPECTED(tool.Call(args), + FailedWithMessage("CommandTool requires arguments")); +} + +TEST_F(MCPPluginTest, CommandToolInvalidArguments) { + CommandTool tool("command", "Run an lldb command."); + ToolArguments args = json::Value(42); + EXPECT_THAT_EXPECTED(tool.Call(args), Failed()); +} + +TEST_F(MCPPluginTest, CommandToolMalformedDebugger) { + CommandTool tool("command", "Run an lldb command."); + ToolArguments args = json::Value( + json::Object{{"debugger", "notanumber"}, {"command", "version"}}); + EXPECT_THAT_EXPECTED( + tool.Call(args), + FailedWithMessage("malformed debugger specifier notanumber")); +} + +TEST_F(MCPPluginTest, CommandToolNoDebuggerFound) { + CommandTool tool("command", "Run an lldb command."); + ToolArguments args = + json::Value(json::Object{{"debugger", "999999"}, {"command", "version"}}); + EXPECT_THAT_EXPECTED(tool.Call(args), FailedWithMessage("no debugger found")); +} + +TEST_F(MCPPluginTest, CommandToolRunsCommandForDebuggerId) { + CommandTool tool("command", "Run an lldb command."); + ToolArguments args = json::Value( + json::Object{{"debugger", std::to_string(m_debugger_sp->GetID())}, + {"command", "version"}}); + + Expected<CallToolResult> result = tool.Call(args); + ASSERT_THAT_EXPECTED(result, Succeeded()); + EXPECT_FALSE(result->isError); + ASSERT_EQ(result->content.size(), 1u); + EXPECT_THAT(result->content[0].text, testing::HasSubstr("lldb")); +} + +TEST_F(MCPPluginTest, CommandToolRunsCommandForDebuggerURI) { + CommandTool tool("command", "Run an lldb command."); + ToolArguments args = json::Value(json::Object{ + {"debugger", + formatv("lldb-mcp://debugger/{0}", m_debugger_sp->GetID()).str()}, + {"command", "version"}}); + + Expected<CallToolResult> result = tool.Call(args); + ASSERT_THAT_EXPECTED(result, Succeeded()); + EXPECT_FALSE(result->isError); +} + +TEST_F(MCPPluginTest, CommandToolUsesFirstDebuggerWhenUnspecified) { + CommandTool tool("command", "Run an lldb command."); + ToolArguments args = json::Value(json::Object{{"command", "version"}}); + + Expected<CallToolResult> result = tool.Call(args); + ASSERT_THAT_EXPECTED(result, Succeeded()); + EXPECT_FALSE(result->isError); +} + +TEST_F(MCPPluginTest, CommandToolReportsCommandError) { + CommandTool tool("command", "Run an lldb command."); + ToolArguments args = json::Value( + json::Object{{"debugger", std::to_string(m_debugger_sp->GetID())}, + {"command", "this-is-not-a-real-command"}}); + + Expected<CallToolResult> result = tool.Call(args); + ASSERT_THAT_EXPECTED(result, Succeeded()); + EXPECT_TRUE(result->isError); + ASSERT_EQ(result->content.size(), 1u); + EXPECT_FALSE(result->content[0].text.empty()); +} + +TEST_F(MCPPluginTest, CommandToolSchema) { + CommandTool tool("command", "Run an lldb command."); + std::optional<json::Value> schema = tool.GetSchema(); + ASSERT_TRUE(schema.has_value()); + const json::Object *obj = schema->getAsObject(); + ASSERT_NE(obj, nullptr); + EXPECT_EQ(obj->getString("type"), "object"); + EXPECT_NE(obj->getObject("properties"), nullptr); +} + +//===----------------------------------------------------------------------===// +// DebuggerListTool +//===----------------------------------------------------------------------===// + +TEST_F(MCPPluginTest, DebuggerListTool) { + DebuggerListTool tool("debugger_list", "List debugger instances."); + ToolArguments args; + Expected<CallToolResult> result = tool.Call(args); + ASSERT_THAT_EXPECTED(result, Succeeded()); + EXPECT_FALSE(result->isError); + ASSERT_EQ(result->content.size(), 1u); + EXPECT_THAT( + result->content[0].text, + testing::HasSubstr( + formatv("lldb-mcp://debugger/{0}", m_debugger_sp->GetID()).str())); +} + +//===----------------------------------------------------------------------===// +// ProtocolServerMCP +//===----------------------------------------------------------------------===// + +namespace { +/// Exposes the protected Extend hook for testing. +class ExtendableProtocolServerMCP : public ProtocolServerMCP { +public: + using ProtocolServerMCP::Extend; +}; +} // namespace + +TEST_F(MCPPluginTest, ProtocolServerExtend) { + ExtendableProtocolServerMCP server; + lldb_protocol::mcp::Server mcp_server("lldb-mcp", "0.1.0"); + // Extend registers the "command" and "debugger_list" tools and the debugger + // resource provider on the server. + server.Extend(mcp_server); +} + +TEST_F(MCPPluginTest, ProtocolServerStaticPluginInfo) { + EXPECT_EQ(ProtocolServerMCP::GetPluginNameStatic(), "MCP"); + EXPECT_EQ(ProtocolServerMCP::GetPluginDescriptionStatic(), "MCP Server."); +} + +TEST_F(MCPPluginTest, ProtocolServerCreateInstance) { + ProtocolServerUP server = ProtocolServerMCP::CreateInstance(); + ASSERT_TRUE(server); + EXPECT_EQ(server->GetPluginName(), "MCP"); + EXPECT_EQ(server->GetSocket(), nullptr); +} + +TEST_F(MCPPluginTest, ProtocolServerInitializeAndTerminate) { + ProtocolServerMCP::Initialize(); + + ProtocolServer *server = ProtocolServer::GetOrCreate("MCP"); + EXPECT_NE(server, nullptr); + + std::vector<StringRef> protocols = ProtocolServer::GetSupportedProtocols(); + EXPECT_THAT(protocols, testing::Contains(StringRef("MCP"))); + + ProtocolServerMCP::Terminate(); +} + +TEST_F(MCPPluginTest, ProtocolServerStopNotRunning) { + ProtocolServerUP server = ProtocolServerMCP::CreateInstance(); + ASSERT_TRUE(server); + // Stopping a server that was never started is an error. + EXPECT_THAT_ERROR(server->Stop(), Failed()); +} + +// NOTE: ProtocolServerMCP::Start (and therefore AcceptCallback) binds a +// listening socket and writes the server info into the real `~/.lldb` +// directory, so it can't be exercised hermetically yet. It is covered by the +// API test (TestMCPUnixSocket.py) and should get unit coverage once the +// socket and filesystem layers can be mocked. + +#endif diff --git a/lldb/unittests/Protocol/MCPServerInfoTest.cpp b/lldb/unittests/Protocol/MCPServerInfoTest.cpp new file mode 100644 index 0000000000000..d78c158e3ca7d --- /dev/null +++ b/lldb/unittests/Protocol/MCPServerInfoTest.cpp @@ -0,0 +1,40 @@ +//===----------------------------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "TestingSupport/TestUtilities.h" +#include "lldb/Protocol/MCP/Server.h" +#include "llvm/Support/Error.h" +#include "llvm/Testing/Support/Error.h" +#include "gtest/gtest.h" + +using namespace llvm; +using namespace lldb_private; +using namespace lldb_protocol::mcp; + +// NOTE: ServerInfo::Write/Load and the non-empty ServerInfoHandle paths read +// and write the real `~/.lldb` directory (via HostInfo::GetUserLLDBDir) and so +// can't be exercised hermetically yet. They should be covered once the +// filesystem can be mocked. + +TEST(MCPServerInfoTest, JSONRoundtrip) { + ServerInfo info; + info.connection_uri = "unix:///tmp/test.sock"; + + Expected<ServerInfo> deserialized = roundtripJSON(info); + ASSERT_THAT_EXPECTED(deserialized, Succeeded()); + EXPECT_EQ(deserialized->connection_uri, info.connection_uri); +} + +TEST(MCPServerInfoTest, EmptyHandleRemoveIsNoOp) { + // A default-constructed handle tracks no file, so Remove is a no-op and does + // not touch the filesystem. + ServerInfoHandle handle; + handle.Remove(); + // Calling Remove again must be safe (idempotent). + handle.Remove(); +} diff --git a/lldb/unittests/Protocol/MCPTransportTest.cpp b/lldb/unittests/Protocol/MCPTransportTest.cpp new file mode 100644 index 0000000000000..570dbee3fbc3f --- /dev/null +++ b/lldb/unittests/Protocol/MCPTransportTest.cpp @@ -0,0 +1,40 @@ +//===----------------------------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "lldb/Host/MainLoop.h" +#include "lldb/Protocol/MCP/Transport.h" +#include "llvm/ADT/StringRef.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include <string> +#include <vector> + +using namespace llvm; +using namespace lldb_private; +using namespace lldb_protocol::mcp; + +TEST(MCPTransportTest, LogWithCallback) { + MainLoop loop; + std::vector<std::string> messages; + Transport transport( + loop, /*in=*/nullptr, /*out=*/nullptr, + [&](StringRef message) { messages.push_back(message.str()); }); + + transport.Log("hello"); + transport.Log("world"); + + EXPECT_THAT(messages, testing::ElementsAre("hello", "world")); +} + +TEST(MCPTransportTest, LogWithoutCallback) { + MainLoop loop; + Transport transport(loop, /*in=*/nullptr, /*out=*/nullptr); + + // Without a log callback, logging is a no-op and must not crash. + transport.Log("ignored"); +} diff --git a/lldb/unittests/Protocol/ProtocolMCPServerTest.cpp b/lldb/unittests/Protocol/ProtocolMCPServerTest.cpp index 9a5b75edeeb9d..d83cbfd7c036d 100644 --- a/lldb/unittests/Protocol/ProtocolMCPServerTest.cpp +++ b/lldb/unittests/Protocol/ProtocolMCPServerTest.cpp @@ -124,6 +124,33 @@ class FailTool : public Tool { } }; +/// Test resource provider that fails with a non-UnsupportedURI error. +class ErrorResourceProvider : public ResourceProvider { +public: + using ResourceProvider::ResourceProvider; + + std::vector<Resource> GetResources() const override { return {}; } + + llvm::Expected<ReadResourceResult> + ReadResource(llvm::StringRef uri) const override { + return llvm::createStringError("resource boom"); + } +}; + +/// Test tool that omits its input schema. +class NoSchemaTool : public Tool { +public: + using Tool::Tool; + + llvm::Expected<CallToolResult> Call(const ToolArguments &args) override { + return CallToolResult{}; + } + + std::optional<llvm::json::Value> GetSchema() const override { + return std::nullopt; + } +}; + class TestServer : public Server { public: using Server::Bind; @@ -326,4 +353,138 @@ TEST_F(ProtocolServerMCPTest, NotificationInitialized) { testing::Contains("MCP initialization complete")); } +TEST_F(ProtocolServerMCPTest, ToolsCallNoName) { + EXPECT_THAT_EXPECTED( + (Call<CallToolResult, CallToolParams>( + "tools/call", + CallToolParams{/*name=*/"", /*arguments=*/std::nullopt})), + HasValue(make_response( + lldb_protocol::mcp::Error{eErrorCodeInternalError, "no tool name"}))); +} + +TEST_F(ProtocolServerMCPTest, ToolsCallUnknownTool) { + EXPECT_THAT_EXPECTED( + (Call<CallToolResult, CallToolParams>( + "tools/call", + CallToolParams{/*name=*/"missing", /*arguments=*/std::nullopt})), + HasValue(make_response(lldb_protocol::mcp::Error{ + eErrorCodeInternalError, "no tool \"missing\""}))); +} + +TEST_F(ProtocolServerMCPTest, ToolsListWithoutSchema) { + server_up->AddTool( + std::make_unique<NoSchemaTool>("noschema", "no schema tool")); + + ToolDefinition no_schema_tool; + no_schema_tool.name = "noschema"; + no_schema_tool.description = "no schema tool"; + + EXPECT_THAT_EXPECTED( + Call<ListToolsResult>("tools/list", Void{}), + HasValue(make_response(ListToolsResult{{no_schema_tool}}))); +} + +TEST_F(ProtocolServerMCPTest, ResourcesRead) { + server_up->AddResourceProvider(std::make_unique<TestResourceProvider>()); + + EXPECT_THAT_EXPECTED( + (Call<ReadResourceResult, ReadResourceParams>( + "resources/read", ReadResourceParams{/*uri=*/"lldb://foo/bar"})), + HasValue(make_response(ReadResourceResult{{ + { + /*uri=*/"lldb://foo/bar", + /*text=*/"foobar", + /*mimeType=*/"application/json", + }, + }}))); +} + +TEST_F(ProtocolServerMCPTest, ResourcesReadNoURI) { + server_up->AddResourceProvider(std::make_unique<TestResourceProvider>()); + + EXPECT_THAT_EXPECTED((Call<ReadResourceResult, ReadResourceParams>( + "resources/read", ReadResourceParams{/*uri=*/""})), + HasValue(make_response(lldb_protocol::mcp::Error{ + eErrorCodeInternalError, "no resource uri"}))); +} + +TEST_F(ProtocolServerMCPTest, ResourcesReadNotFound) { + server_up->AddResourceProvider(std::make_unique<TestResourceProvider>()); + + EXPECT_THAT_EXPECTED( + (Call<ReadResourceResult, ReadResourceParams>( + "resources/read", ReadResourceParams{/*uri=*/"lldb://unknown"})), + HasValue(make_response(lldb_protocol::mcp::Error{ + MCPError::kResourceNotFound, + "no resource handler for uri: lldb://unknown"}))); +} + +TEST(MCPToolTest, GetDefinitionWithSchema) { + TestTool tool("test", "test tool"); + EXPECT_EQ(tool.GetName(), "test"); + + ToolDefinition definition = tool.GetDefinition(); + EXPECT_EQ(definition.name, "test"); + EXPECT_EQ(definition.description, "test tool"); + ASSERT_TRUE(definition.inputSchema.has_value()); +} + +TEST(MCPToolTest, GetDefinitionWithoutSchema) { + NoSchemaTool tool("noschema", "no schema tool"); + + ToolDefinition definition = tool.GetDefinition(); + EXPECT_EQ(definition.name, "noschema"); + EXPECT_EQ(definition.description, "no schema tool"); + EXPECT_FALSE(definition.inputSchema.has_value()); +} + +TEST_F(ProtocolServerMCPTest, AddNullToolAndResourceProvider) { + // Null tools and resource providers are silently ignored. + server_up->AddTool(nullptr); + server_up->AddResourceProvider(nullptr); + + EXPECT_THAT_EXPECTED(Call<ListToolsResult>("tools/list", Void{}), + HasValue(make_response(ListToolsResult{}))); + EXPECT_THAT_EXPECTED(Call<ListResourcesResult>("resources/list", Void{}), + HasValue(make_response(ListResourcesResult{}))); +} + +TEST_F(ProtocolServerMCPTest, ResourcesReadError) { + server_up->AddResourceProvider(std::make_unique<ErrorResourceProvider>()); + + EXPECT_THAT_EXPECTED( + (Call<ReadResourceResult, ReadResourceParams>( + "resources/read", ReadResourceParams{/*uri=*/"lldb://x"})), + HasValue(make_response(lldb_protocol::mcp::Error{eErrorCodeInternalError, + "resource boom"}))); +} + +TEST_F(ProtocolServerMCPTest, Accept) { + auto transports = TestTransport<ProtocolDescriptor>::createPair(loop); + EXPECT_THAT_ERROR(server_up->Accept(std::move(transports.second)), + Succeeded()); +} + +TEST_F(ProtocolServerMCPTest, AcceptErrorAndDisconnect) { + auto transports = TestTransport<ProtocolDescriptor>::createPair(loop); + TestTransport<ProtocolDescriptor> *server_transport = transports.second.get(); + EXPECT_THAT_ERROR(server_up->Accept(std::move(transports.second)), + Succeeded()); + + // A transport error is logged by the server. + server_transport->SimulateError(llvm::createStringError("boom")); + EXPECT_THAT(logged_messages, + testing::Contains(testing::HasSubstr("Transport error: boom"))); + + // Closing the transport removes the client from the server. This destroys + // the transport, so it must not be used afterwards. + server_transport->SimulateClosed(); +} + +TEST_F(ProtocolServerMCPTest, AcceptRegisterFailure) { + auto transport = std::make_unique<TestTransport<ProtocolDescriptor>>(loop); + transport->SetRegisterMessageHandlerShouldFail(true); + EXPECT_THAT_ERROR(server_up->Accept(std::move(transport)), Failed()); +} + #endif diff --git a/lldb/unittests/Protocol/ProtocolMCPTest.cpp b/lldb/unittests/Protocol/ProtocolMCPTest.cpp index 5f7391e43fe34..efed243912c00 100644 --- a/lldb/unittests/Protocol/ProtocolMCPTest.cpp +++ b/lldb/unittests/Protocol/ProtocolMCPTest.cpp @@ -296,4 +296,396 @@ TEST(ProtocolMCPTest, ReadResourceResultEmpty) { EXPECT_TRUE(deserialized_result->contents.empty()); } +TEST(ProtocolMCPTest, RequestWithStringId) { + Request request; + request.id = "request-1"; + request.method = "foo"; + + llvm::Expected<Request> deserialized = roundtripJSON(request); + ASSERT_THAT_EXPECTED(deserialized, llvm::Succeeded()); + EXPECT_EQ(request, *deserialized); +} + +TEST(ProtocolMCPTest, RequestWithoutParams) { + Request request; + request.id = 7; + request.method = "bar"; + + llvm::Expected<Request> deserialized = roundtripJSON(request); + ASSERT_THAT_EXPECTED(deserialized, llvm::Succeeded()); + EXPECT_EQ(request, *deserialized); + EXPECT_FALSE(deserialized->params.has_value()); +} + +TEST(ProtocolMCPTest, RequestMissingId) { + llvm::json::Value value = + llvm::json::Object{{"jsonrpc", "2.0"}, {"method", "foo"}}; + Request request; + llvm::json::Path::Root root; + EXPECT_FALSE(fromJSON(value, request, root)); +} + +TEST(ProtocolMCPTest, RequestInvalidId) { + llvm::json::Value value = + llvm::json::Object{{"jsonrpc", "2.0"}, {"id", true}, {"method", "foo"}}; + Request request; + llvm::json::Path::Root root; + EXPECT_FALSE(fromJSON(value, request, root)); +} + +TEST(ProtocolMCPTest, ResponseWithStringId) { + Response response; + response.id = "resp-1"; + response.result = llvm::json::Value("ok"); + + llvm::Expected<Response> deserialized = roundtripJSON(response); + ASSERT_THAT_EXPECTED(deserialized, llvm::Succeeded()); + EXPECT_EQ(response, *deserialized); +} + +TEST(ProtocolMCPTest, ResponseResultAndErrorMutuallyExclusive) { + llvm::json::Value value = llvm::json::Object{ + {"jsonrpc", "2.0"}, + {"id", 1}, + {"result", 1}, + {"error", llvm::json::Object{{"code", 1}, {"message", "m"}}}}; + Response response; + llvm::json::Path::Root root; + EXPECT_FALSE(fromJSON(value, response, root)); +} + +TEST(ProtocolMCPTest, ResponseRequiresResultOrError) { + llvm::json::Value value = llvm::json::Object{{"jsonrpc", "2.0"}, {"id", 1}}; + Response response; + llvm::json::Path::Root root; + EXPECT_FALSE(fromJSON(value, response, root)); +} + +TEST(ProtocolMCPTest, ResponseExpectsObject) { + llvm::json::Value value(42); + Response response; + llvm::json::Path::Root root; + EXPECT_FALSE(fromJSON(value, response, root)); +} + +TEST(ProtocolMCPTest, ResponseInvalidError) { + llvm::json::Value value = llvm::json::Object{ + {"jsonrpc", "2.0"}, {"id", 1}, {"error", "not-an-object"}}; + Response response; + llvm::json::Path::Root root; + EXPECT_FALSE(fromJSON(value, response, root)); +} + +TEST(ProtocolMCPTest, ErrorWithData) { + Error error; + error.code = -32000; + error.message = "boom"; + error.data = llvm::json::Object{{"detail", "stack"}}; + + llvm::Expected<Error> deserialized = roundtripJSON(error); + ASSERT_THAT_EXPECTED(deserialized, llvm::Succeeded()); + EXPECT_EQ(error, *deserialized); + EXPECT_TRUE(deserialized->data.has_value()); +} + +TEST(ProtocolMCPTest, NotificationWithoutParams) { + Notification notification; + notification.method = "ping"; + + llvm::Expected<Notification> deserialized = roundtripJSON(notification); + ASSERT_THAT_EXPECTED(deserialized, llvm::Succeeded()); + EXPECT_EQ(notification, *deserialized); + EXPECT_FALSE(deserialized->params.has_value()); +} + +TEST(ProtocolMCPTest, NotificationExpectsObject) { + llvm::json::Value value(42); + Notification notification; + llvm::json::Path::Root root; + EXPECT_FALSE(fromJSON(value, notification, root)); +} + +TEST(ProtocolMCPTest, NotificationMissingMethod) { + llvm::json::Value value = llvm::json::Object{{"jsonrpc", "2.0"}}; + Notification notification; + llvm::json::Path::Root root; + EXPECT_FALSE(fromJSON(value, notification, root)); +} + +TEST(ProtocolMCPTest, ToolDefinitionMinimal) { + ToolDefinition tool_definition; + tool_definition.name = "tool"; + + llvm::Expected<ToolDefinition> deserialized = roundtripJSON(tool_definition); + ASSERT_THAT_EXPECTED(deserialized, llvm::Succeeded()); + EXPECT_EQ(tool_definition.name, deserialized->name); + EXPECT_TRUE(deserialized->description.empty()); + EXPECT_FALSE(deserialized->inputSchema.has_value()); +} + +TEST(ProtocolMCPTest, ToolDefinitionMissingName) { + llvm::json::Value value = llvm::json::Object{{"description", "d"}}; + ToolDefinition tool_definition; + llvm::json::Path::Root root; + EXPECT_FALSE(fromJSON(value, tool_definition, root)); +} + +TEST(ProtocolMCPTest, MessageExpectsObject) { + llvm::json::Value value(42); + Message message; + llvm::json::Path::Root root; + EXPECT_FALSE(fromJSON(value, message, root)); +} + +TEST(ProtocolMCPTest, MessageRequiresJSONRPC) { + llvm::json::Value value = llvm::json::Object{{"id", 1}, {"method", "m"}}; + Message message; + llvm::json::Path::Root root; + EXPECT_FALSE(fromJSON(value, message, root)); +} + +TEST(ProtocolMCPTest, MessageUnsupportedJSONRPCVersion) { + llvm::json::Value value = + llvm::json::Object{{"jsonrpc", "1.0"}, {"id", 1}, {"method", "m"}}; + Message message; + llvm::json::Path::Root root; + EXPECT_FALSE(fromJSON(value, message, root)); +} + +TEST(ProtocolMCPTest, MessageUnrecognized) { + llvm::json::Value value = llvm::json::Object{{"jsonrpc", "2.0"}, {"id", 1}}; + Message message; + llvm::json::Path::Root root; + EXPECT_FALSE(fromJSON(value, message, root)); +} + +TEST(ProtocolMCPTest, MessageInvalidNotification) { + // No "id" routes to a Notification, but a missing "method" is invalid. + llvm::json::Value value = llvm::json::Object{{"jsonrpc", "2.0"}}; + Message message; + llvm::json::Path::Root root; + EXPECT_FALSE(fromJSON(value, message, root)); +} + +TEST(ProtocolMCPTest, MessageInvalidRequest) { + // Routed to a Request (it has a "method"), but the id is invalid. + llvm::json::Value value = + llvm::json::Object{{"jsonrpc", "2.0"}, {"id", true}, {"method", "m"}}; + Message message; + llvm::json::Path::Root root; + EXPECT_FALSE(fromJSON(value, message, root)); +} + +TEST(ProtocolMCPTest, MessageInvalidResponse) { + // Routed to a Response (it has "result"/"error" but no "method"), but + // 'result' and 'error' are mutually exclusive. + llvm::json::Value value = llvm::json::Object{ + {"jsonrpc", "2.0"}, + {"id", 1}, + {"result", 1}, + {"error", llvm::json::Object{{"code", 1}, {"message", "m"}}}}; + Message message; + llvm::json::Path::Root root; + EXPECT_FALSE(fromJSON(value, message, root)); +} + +TEST(ProtocolMCPTest, ImplementationWithTitle) { + Implementation impl; + impl.name = "lldb-mcp"; + impl.version = "0.1.0"; + impl.title = "LLDB MCP"; + + llvm::Expected<Implementation> deserialized = roundtripJSON(impl); + ASSERT_THAT_EXPECTED(deserialized, llvm::Succeeded()); + EXPECT_EQ(impl.name, deserialized->name); + EXPECT_EQ(impl.version, deserialized->version); + EXPECT_EQ(impl.title, deserialized->title); +} + +TEST(ProtocolMCPTest, ImplementationWithoutTitle) { + Implementation impl; + impl.name = "lldb-mcp"; + impl.version = "0.1.0"; + + llvm::Expected<Implementation> deserialized = roundtripJSON(impl); + ASSERT_THAT_EXPECTED(deserialized, llvm::Succeeded()); + EXPECT_EQ(impl.name, deserialized->name); + EXPECT_TRUE(deserialized->title.empty()); +} + +TEST(ProtocolMCPTest, ServerCapabilitiesAllFields) { + ServerCapabilities caps; + caps.supportsToolsList = true; + caps.supportsResourcesList = true; + caps.supportsResourcesSubscribe = true; + caps.supportsCompletions = true; + caps.supportsLogging = true; + + llvm::json::Value value = toJSON(caps); + const llvm::json::Object *obj = value.getAsObject(); + ASSERT_NE(obj, nullptr); + EXPECT_NE(obj->get("tools"), nullptr); + EXPECT_NE(obj->get("completions"), nullptr); + EXPECT_NE(obj->get("logging"), nullptr); + + const llvm::json::Object *resources = obj->getObject("resources"); + ASSERT_NE(resources, nullptr); + EXPECT_EQ(resources->getBoolean("listChanged"), true); + EXPECT_EQ(resources->getBoolean("subscribe"), true); +} + +TEST(ProtocolMCPTest, ServerCapabilitiesSubscribeOnly) { + ServerCapabilities caps; + caps.supportsResourcesSubscribe = true; + + llvm::json::Value value = toJSON(caps); + const llvm::json::Object *resources = + value.getAsObject()->getObject("resources"); + ASSERT_NE(resources, nullptr); + EXPECT_EQ(resources->getBoolean("subscribe"), true); + EXPECT_EQ(resources->get("listChanged"), nullptr); +} + +TEST(ProtocolMCPTest, ServerCapabilitiesFromJSONWithoutTools) { + ServerCapabilities caps; + llvm::json::Value value = llvm::json::Object{}; + llvm::json::Path::Root root; + ASSERT_TRUE(fromJSON(value, caps, root)); + EXPECT_FALSE(caps.supportsToolsList); +} + +TEST(ProtocolMCPTest, ServerCapabilitiesFromJSONExpectsObject) { + ServerCapabilities caps; + llvm::json::Value value(42); + llvm::json::Path::Root root; + EXPECT_FALSE(fromJSON(value, caps, root)); +} + +TEST(ProtocolMCPTest, ClientCapabilities) { + ClientCapabilities caps; + EXPECT_EQ(toJSON(caps), llvm::json::Value(llvm::json::Object{})); + + llvm::json::Value value = llvm::json::Object{}; + llvm::json::Path::Root root; + EXPECT_TRUE(fromJSON(value, caps, root)); +} + +TEST(ProtocolMCPTest, InitializeParams) { + InitializeParams params; + params.protocolVersion = "2024-11-05"; + params.clientInfo.name = "client"; + params.clientInfo.version = "1.0"; + + llvm::Expected<InitializeParams> deserialized = roundtripJSON(params); + ASSERT_THAT_EXPECTED(deserialized, llvm::Succeeded()); + EXPECT_EQ(params.protocolVersion, deserialized->protocolVersion); + EXPECT_EQ(params.clientInfo.name, deserialized->clientInfo.name); + EXPECT_EQ(params.clientInfo.version, deserialized->clientInfo.version); +} + +TEST(ProtocolMCPTest, InitializeResultWithInstructions) { + InitializeResult result; + result.protocolVersion = "2024-11-05"; + result.capabilities.supportsToolsList = true; + result.serverInfo.name = "lldb-mcp"; + result.serverInfo.version = "0.1.0"; + result.instructions = "Use the tools wisely."; + + llvm::Expected<InitializeResult> deserialized = roundtripJSON(result); + ASSERT_THAT_EXPECTED(deserialized, llvm::Succeeded()); + EXPECT_EQ(result.protocolVersion, deserialized->protocolVersion); + EXPECT_EQ(result.instructions, deserialized->instructions); + EXPECT_TRUE(deserialized->capabilities.supportsToolsList); +} + +TEST(ProtocolMCPTest, InitializeResultWithoutInstructions) { + InitializeResult result; + result.protocolVersion = "2024-11-05"; + result.serverInfo.name = "lldb-mcp"; + result.serverInfo.version = "0.1.0"; + + llvm::Expected<InitializeResult> deserialized = roundtripJSON(result); + ASSERT_THAT_EXPECTED(deserialized, llvm::Succeeded()); + EXPECT_TRUE(deserialized->instructions.empty()); +} + +TEST(ProtocolMCPTest, ListToolsResult) { + ToolDefinition tool_definition; + tool_definition.name = "a"; + tool_definition.description = "d"; + + ListToolsResult result; + result.tools = {tool_definition}; + + llvm::Expected<ListToolsResult> deserialized = roundtripJSON(result); + ASSERT_THAT_EXPECTED(deserialized, llvm::Succeeded()); + ASSERT_EQ(deserialized->tools.size(), 1u); + EXPECT_EQ(deserialized->tools[0].name, "a"); +} + +TEST(ProtocolMCPTest, CallToolResultStructuredContent) { + CallToolResult result; + result.content = {TextContent{"text"}}; + result.structuredContent = llvm::json::Object{{"k", "v"}}; + + llvm::Expected<CallToolResult> deserialized = roundtripJSON(result); + ASSERT_THAT_EXPECTED(deserialized, llvm::Succeeded()); + ASSERT_TRUE(deserialized->structuredContent.has_value()); + ASSERT_EQ(deserialized->content.size(), 1u); + EXPECT_EQ(deserialized->content[0].text, "text"); +} + +TEST(ProtocolMCPTest, CallToolParamsWithArguments) { + CallToolParams params; + params.name = "tool"; + params.arguments = llvm::json::Object{{"a", 1}}; + + llvm::Expected<CallToolParams> deserialized = roundtripJSON(params); + ASSERT_THAT_EXPECTED(deserialized, llvm::Succeeded()); + EXPECT_EQ(deserialized->name, "tool"); + EXPECT_TRUE(deserialized->arguments.has_value()); +} + +TEST(ProtocolMCPTest, CallToolParamsWithoutArguments) { + CallToolParams params; + params.name = "tool"; + + llvm::Expected<CallToolParams> deserialized = roundtripJSON(params); + ASSERT_THAT_EXPECTED(deserialized, llvm::Succeeded()); + EXPECT_EQ(deserialized->name, "tool"); + EXPECT_FALSE(deserialized->arguments.has_value()); +} + +TEST(ProtocolMCPTest, ReadResourceParams) { + ReadResourceParams params; + params.uri = "lldb://debugger/0"; + + llvm::Expected<ReadResourceParams> deserialized = roundtripJSON(params); + ASSERT_THAT_EXPECTED(deserialized, llvm::Succeeded()); + EXPECT_EQ(deserialized->uri, params.uri); +} + +TEST(ProtocolMCPTest, ListResourcesResult) { + Resource resource; + resource.uri = "lldb://x"; + resource.name = "x"; + + ListResourcesResult result; + result.resources = {resource}; + + llvm::Expected<ListResourcesResult> deserialized = roundtripJSON(result); + ASSERT_THAT_EXPECTED(deserialized, llvm::Succeeded()); + ASSERT_EQ(deserialized->resources.size(), 1u); + EXPECT_EQ(deserialized->resources[0].uri, "lldb://x"); +} + +TEST(ProtocolMCPTest, Void) { + EXPECT_EQ(toJSON(Void{}), llvm::json::Value(llvm::json::Object{})); + + Void value; + llvm::json::Value json = llvm::json::Object{}; + llvm::json::Path::Root root; + EXPECT_TRUE(fromJSON(json, value, root)); +} + #endif diff --git a/lldb/unittests/TestingSupport/Host/JSONTransportTestUtilities.h b/lldb/unittests/TestingSupport/Host/JSONTransportTestUtilities.h index 4623c365c960f..93845fc82d173 100644 --- a/lldb/unittests/TestingSupport/Host/JSONTransportTestUtilities.h +++ b/lldb/unittests/TestingSupport/Host/JSONTransportTestUtilities.h @@ -64,17 +64,42 @@ class TestTransport final } llvm::Error RegisterMessageHandler(MessageHandler &handler) override { + if (m_register_should_fail) + return llvm::createStringError("RegisterMessageHandler failed"); if (!m_handler) m_handler = &handler; return llvm::Error::success(); } + /// Makes the next RegisterMessageHandler call fail, to exercise error paths. + void SetRegisterMessageHandlerShouldFail(bool fail) { + m_register_should_fail = fail; + } + + /// Drives the registered handler's error callback, as the real transport + /// would on a read or parse failure. + void SimulateError(llvm::Error error) { + EXPECT_TRUE(m_handler) + << "SimulateError called before RegisterMessageHandler"; + m_handler->OnError(std::move(error)); + } + + /// Drives the registered handler's close callback, as the real transport + /// would on EOF. Mirrors IOTransport::OnRead: the handler may destroy this + /// transport, so members must not be accessed after this returns. + void SimulateClosed() { + EXPECT_TRUE(m_handler) + << "SimulateClosed called before RegisterMessageHandler"; + m_handler->OnClosed(); + } + protected: void Log(llvm::StringRef message) override {}; private: lldb_private::MainLoop &m_loop; MessageHandler *m_handler = nullptr; + bool m_register_should_fail = false; }; template <typename Proto> _______________________________________________ lldb-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits
