[Lldb-commits] [lldb] [lldb] Implement JSON RPC (newline delimited) Transport (PR #143946)
Jonas Devlieghere via lldb-commits
lldb-commits at lists.llvm.org
Thu Jun 12 11:20:40 PDT 2025
https://github.com/JDevlieghere created https://github.com/llvm/llvm-project/pull/143946
This PR implements JSON RPC-style (i.e. newline delimited) JSON transport. I moved the existing transport tests from DAP to Host and moved the PipeTest base class into TestingSupport so it can be shared by both.
>From 9ebb3986e008cec978c0111557a02a9a8d720838 Mon Sep 17 00:00:00 2001
From: Jonas Devlieghere <jonas at devlieghere.com>
Date: Thu, 12 Jun 2025 11:19:15 -0700
Subject: [PATCH] [lldb] Implement JSON RPC (newline delimited) Transport
This PR implements JSON RPC-style (i.e. newline delimited) JSON
transport. I moved the existing transport tests from DAP to Host and
moved the PipeTest base class into TestingSupport so it can be shared by
both.
---
lldb/include/lldb/Host/JSONTransport.h | 15 ++
lldb/source/Host/common/JSONTransport.cpp | 31 +++-
lldb/unittests/DAP/CMakeLists.txt | 1 -
lldb/unittests/DAP/TestBase.cpp | 7 +-
lldb/unittests/DAP/TestBase.h | 13 +-
lldb/unittests/DAP/TransportTest.cpp | 98 ------------
lldb/unittests/Host/CMakeLists.txt | 1 +
lldb/unittests/Host/JSONTransportTest.cpp | 146 ++++++++++++++++++
.../TestingSupport/Host/PipeTestUtilities.h | 28 ++++
9 files changed, 223 insertions(+), 117 deletions(-)
delete mode 100644 lldb/unittests/DAP/TransportTest.cpp
create mode 100644 lldb/unittests/Host/JSONTransportTest.cpp
create mode 100644 lldb/unittests/TestingSupport/Host/PipeTestUtilities.h
diff --git a/lldb/include/lldb/Host/JSONTransport.h b/lldb/include/lldb/Host/JSONTransport.h
index 4db5e417ea852..f7593750669e1 100644
--- a/lldb/include/lldb/Host/JSONTransport.h
+++ b/lldb/include/lldb/Host/JSONTransport.h
@@ -121,6 +121,21 @@ class HTTPDelimitedJSONTransport : public JSONTransport {
static constexpr llvm::StringLiteral kHeaderSeparator = "\r\n\r\n";
};
+/// A transport class for JSON RPC.
+class JSONRPCTransport : public JSONTransport {
+public:
+ JSONRPCTransport(lldb::IOObjectSP input, lldb::IOObjectSP output)
+ : JSONTransport(input, output) {}
+ virtual ~JSONRPCTransport() = default;
+
+protected:
+ virtual llvm::Error WriteImpl(const std::string &message) override;
+ virtual llvm::Expected<std::string>
+ ReadImpl(const std::chrono::microseconds &timeout) override;
+
+ static constexpr llvm::StringLiteral kMessageSeparator = "\n";
+};
+
} // namespace lldb_private
#endif
diff --git a/lldb/source/Host/common/JSONTransport.cpp b/lldb/source/Host/common/JSONTransport.cpp
index 103c76d25daf7..7b925372e0b74 100644
--- a/lldb/source/Host/common/JSONTransport.cpp
+++ b/lldb/source/Host/common/JSONTransport.cpp
@@ -92,7 +92,7 @@ void JSONTransport::Log(llvm::StringRef message) {
Expected<std::string>
HTTPDelimitedJSONTransport::ReadImpl(const std::chrono::microseconds &timeout) {
if (!m_input || !m_input->IsValid())
- return createStringError("transport output is closed");
+ return llvm::make_error<TransportClosedError>();
IOObject *input = m_input.get();
Expected<std::string> message_header =
@@ -142,6 +142,35 @@ Error HTTPDelimitedJSONTransport::WriteImpl(const std::string &message) {
return m_output->Write(Output.data(), num_bytes).takeError();
}
+Expected<std::string>
+JSONRPCTransport::ReadImpl(const std::chrono::microseconds &timeout) {
+ if (!m_input || !m_input->IsValid())
+ return make_error<TransportClosedError>();
+
+ IOObject *input = m_input.get();
+ Expected<std::string> raw_json =
+ ReadUntil(*input, kMessageSeparator, timeout);
+ if (!raw_json)
+ return raw_json.takeError();
+
+ Log(llvm::formatv("--> {0}", *raw_json).str());
+
+ return *raw_json;
+}
+
+Error JSONRPCTransport::WriteImpl(const std::string &message) {
+ if (!m_output || !m_output->IsValid())
+ return llvm::make_error<TransportClosedError>();
+
+ Log(llvm::formatv("<-- {0}", message).str());
+
+ std::string Output;
+ llvm::raw_string_ostream OS(Output);
+ OS << message << kMessageSeparator;
+ size_t num_bytes = Output.size();
+ return m_output->Write(Output.data(), num_bytes).takeError();
+}
+
char TransportEOFError::ID;
char TransportTimeoutError::ID;
char TransportClosedError::ID;
diff --git a/lldb/unittests/DAP/CMakeLists.txt b/lldb/unittests/DAP/CMakeLists.txt
index 37a6a81ad12a0..ee623d341ec69 100644
--- a/lldb/unittests/DAP/CMakeLists.txt
+++ b/lldb/unittests/DAP/CMakeLists.txt
@@ -7,7 +7,6 @@ add_lldb_unittest(DAPTests
LLDBUtilsTest.cpp
ProtocolTypesTest.cpp
TestBase.cpp
- TransportTest.cpp
VariablesTest.cpp
LINK_COMPONENTS
diff --git a/lldb/unittests/DAP/TestBase.cpp b/lldb/unittests/DAP/TestBase.cpp
index 4063b34250312..27ad42686fbbf 100644
--- a/lldb/unittests/DAP/TestBase.cpp
+++ b/lldb/unittests/DAP/TestBase.cpp
@@ -28,13 +28,8 @@ using lldb_private::File;
using lldb_private::NativeFile;
using lldb_private::Pipe;
-void PipeBase::SetUp() {
- ASSERT_THAT_ERROR(input.CreateNew(false).ToError(), Succeeded());
- ASSERT_THAT_ERROR(output.CreateNew(false).ToError(), Succeeded());
-}
-
void TransportBase::SetUp() {
- PipeBase::SetUp();
+ PipeTest::SetUp();
to_dap = std::make_unique<Transport>(
"to_dap", nullptr,
std::make_shared<NativeFile>(input.GetReadFileDescriptor(),
diff --git a/lldb/unittests/DAP/TestBase.h b/lldb/unittests/DAP/TestBase.h
index 70b3985271a92..25d37013954d5 100644
--- a/lldb/unittests/DAP/TestBase.h
+++ b/lldb/unittests/DAP/TestBase.h
@@ -8,26 +8,17 @@
#include "DAP.h"
#include "Protocol/ProtocolBase.h"
+#include "TestingSupport/Host/PipeTestUtilities.h"
#include "Transport.h"
-#include "lldb/Host/Pipe.h"
#include "llvm/ADT/StringRef.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
namespace lldb_dap_tests {
-/// A base class for tests that need a pair of pipes for communication.
-class PipeBase : public testing::Test {
-protected:
- lldb_private::Pipe input;
- lldb_private::Pipe output;
-
- void SetUp() override;
-};
-
/// A base class for tests that need transport configured for communicating DAP
/// messages.
-class TransportBase : public PipeBase {
+class TransportBase : public PipeTest {
protected:
std::unique_ptr<lldb_dap::Transport> to_dap;
std::unique_ptr<lldb_dap::Transport> from_dap;
diff --git a/lldb/unittests/DAP/TransportTest.cpp b/lldb/unittests/DAP/TransportTest.cpp
deleted file mode 100644
index aaf257993af23..0000000000000
--- a/lldb/unittests/DAP/TransportTest.cpp
+++ /dev/null
@@ -1,98 +0,0 @@
-//===-- TransportTest.cpp -------------------------------------------------===//
-//
-// 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 "Transport.h"
-#include "Protocol/ProtocolBase.h"
-#include "TestBase.h"
-#include "lldb/Host/File.h"
-#include "lldb/Host/Pipe.h"
-#include "llvm/ADT/StringRef.h"
-#include "llvm/Testing/Support/Error.h"
-#include "gtest/gtest.h"
-#include <chrono>
-#include <memory>
-#include <optional>
-
-using namespace llvm;
-using namespace lldb;
-using namespace lldb_dap;
-using namespace lldb_dap_tests;
-using namespace lldb_dap::protocol;
-using lldb_private::File;
-using lldb_private::NativeFile;
-using lldb_private::Pipe;
-using lldb_private::TransportEOFError;
-using lldb_private::TransportTimeoutError;
-
-class TransportTest : public PipeBase {
-protected:
- std::unique_ptr<Transport> transport;
-
- void SetUp() override {
- PipeBase::SetUp();
- transport = std::make_unique<Transport>(
- "stdio", nullptr,
- std::make_shared<NativeFile>(input.GetReadFileDescriptor(),
- File::eOpenOptionReadOnly,
- NativeFile::Unowned),
- std::make_shared<NativeFile>(output.GetWriteFileDescriptor(),
- File::eOpenOptionWriteOnly,
- NativeFile::Unowned));
- }
-};
-
-TEST_F(TransportTest, MalformedRequests) {
- std::string malformed_header = "COnTent-LenGth: -1{}\r\n\r\nnotjosn";
- ASSERT_THAT_EXPECTED(
- input.Write(malformed_header.data(), malformed_header.size()),
- Succeeded());
- ASSERT_THAT_EXPECTED(
- transport->Read<protocol::Message>(std::chrono::milliseconds(1)),
- FailedWithMessage(
- "expected 'Content-Length: ' and got 'COnTent-LenGth: '"));
-}
-
-TEST_F(TransportTest, Read) {
- std::string json =
- R"json({"seq": 1, "type": "request", "command": "abc"})json";
- std::string message =
- formatv("Content-Length: {0}\r\n\r\n{1}", json.size(), json).str();
- ASSERT_THAT_EXPECTED(input.Write(message.data(), message.size()),
- Succeeded());
- ASSERT_THAT_EXPECTED(
- transport->Read<protocol::Message>(std::chrono::milliseconds(1)),
- HasValue(testing::VariantWith<Request>(testing::FieldsAre(
- /*seq=*/1, /*command=*/"abc", /*arguments=*/std::nullopt))));
-}
-
-TEST_F(TransportTest, ReadWithTimeout) {
- ASSERT_THAT_EXPECTED(
- transport->Read<protocol::Message>(std::chrono::milliseconds(1)),
- Failed<TransportTimeoutError>());
-}
-
-TEST_F(TransportTest, ReadWithEOF) {
- input.CloseWriteFileDescriptor();
- ASSERT_THAT_EXPECTED(
- transport->Read<protocol::Message>(std::chrono::milliseconds(1)),
- Failed<TransportEOFError>());
-}
-
-TEST_F(TransportTest, Write) {
- ASSERT_THAT_ERROR(transport->Write(Event{"my-event", std::nullopt}),
- Succeeded());
- output.CloseWriteFileDescriptor();
- char buf[1024];
- Expected<size_t> bytes_read =
- output.Read(buf, sizeof(buf), std::chrono::milliseconds(1));
- ASSERT_THAT_EXPECTED(bytes_read, Succeeded());
- ASSERT_EQ(
- StringRef(buf, *bytes_read),
- StringRef("Content-Length: 43\r\n\r\n"
- R"json({"event":"my-event","seq":0,"type":"event"})json"));
-}
diff --git a/lldb/unittests/Host/CMakeLists.txt b/lldb/unittests/Host/CMakeLists.txt
index 5b8deed00af88..3b20f1d723d18 100644
--- a/lldb/unittests/Host/CMakeLists.txt
+++ b/lldb/unittests/Host/CMakeLists.txt
@@ -6,6 +6,7 @@ set (FILES
HostInfoTest.cpp
HostTest.cpp
MainLoopTest.cpp
+ JSONTransportTest.cpp
NativeProcessProtocolTest.cpp
PipeTest.cpp
ProcessLaunchInfoTest.cpp
diff --git a/lldb/unittests/Host/JSONTransportTest.cpp b/lldb/unittests/Host/JSONTransportTest.cpp
new file mode 100644
index 0000000000000..bb7958b4e95b5
--- /dev/null
+++ b/lldb/unittests/Host/JSONTransportTest.cpp
@@ -0,0 +1,146 @@
+//===-- JSONTransportTest.cpp ---------------------------------------------===//
+//
+// 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/JSONTransport.h"
+#include "TestingSupport/Host/PipeTestUtilities.h"
+#include "lldb/Host/File.h"
+
+using namespace llvm;
+using namespace lldb_private;
+
+namespace {
+template <typename T> class JSONTransportTest : public PipeTest {
+protected:
+ std::unique_ptr<JSONTransport> transport;
+
+ void SetUp() override {
+ PipeTest::SetUp();
+ transport = std::make_unique<T>(
+ std::make_shared<NativeFile>(input.GetReadFileDescriptor(),
+ File::eOpenOptionReadOnly,
+ NativeFile::Unowned),
+ std::make_shared<NativeFile>(output.GetWriteFileDescriptor(),
+ File::eOpenOptionWriteOnly,
+ NativeFile::Unowned));
+ }
+};
+
+class HTTPDelimitedJSONTransportTest
+ : public JSONTransportTest<HTTPDelimitedJSONTransport> {
+public:
+ using JSONTransportTest::JSONTransportTest;
+};
+
+class JSONRPCTransportTest : public JSONTransportTest<JSONRPCTransport> {
+public:
+ using JSONTransportTest::JSONTransportTest;
+};
+
+struct JSONTestType {
+ std::string str;
+};
+
+llvm::json::Value toJSON(const JSONTestType &T) {
+ return llvm::json::Object{{"str", T.str}};
+}
+
+bool fromJSON(const llvm::json::Value &V, JSONTestType &T, llvm::json::Path P) {
+ llvm::json::ObjectMapper O(V, P);
+ return O && O.map("str", T.str);
+}
+} // namespace
+
+TEST_F(HTTPDelimitedJSONTransportTest, MalformedRequests) {
+ std::string malformed_header = "COnTent-LenGth: -1{}\r\n\r\nnotjosn";
+ ASSERT_THAT_EXPECTED(
+ input.Write(malformed_header.data(), malformed_header.size()),
+ Succeeded());
+ ASSERT_THAT_EXPECTED(
+ transport->Read<JSONTestType>(std::chrono::milliseconds(1)),
+ FailedWithMessage(
+ "expected 'Content-Length: ' and got 'COnTent-LenGth: '"));
+}
+
+TEST_F(HTTPDelimitedJSONTransportTest, Read) {
+ std::string json = R"json({"str": "foo"})json";
+ std::string message =
+ formatv("Content-Length: {0}\r\n\r\n{1}", json.size(), json).str();
+ ASSERT_THAT_EXPECTED(input.Write(message.data(), message.size()),
+ Succeeded());
+ ASSERT_THAT_EXPECTED(
+ transport->Read<JSONTestType>(std::chrono::milliseconds(1)),
+ HasValue(testing::FieldsAre(/*str=*/"foo")));
+}
+
+TEST_F(HTTPDelimitedJSONTransportTest, ReadWithTimeout) {
+ ASSERT_THAT_EXPECTED(
+ transport->Read<JSONTestType>(std::chrono::milliseconds(1)),
+ Failed<TransportTimeoutError>());
+}
+
+TEST_F(HTTPDelimitedJSONTransportTest, ReadWithEOF) {
+ input.CloseWriteFileDescriptor();
+ ASSERT_THAT_EXPECTED(
+ transport->Read<JSONTestType>(std::chrono::milliseconds(1)),
+ Failed<TransportEOFError>());
+}
+
+TEST_F(HTTPDelimitedJSONTransportTest, Write) {
+ ASSERT_THAT_ERROR(transport->Write(JSONTestType{"foo"}), Succeeded());
+ output.CloseWriteFileDescriptor();
+ char buf[1024];
+ Expected<size_t> bytes_read =
+ output.Read(buf, sizeof(buf), std::chrono::milliseconds(1));
+ ASSERT_THAT_EXPECTED(bytes_read, Succeeded());
+ ASSERT_EQ(StringRef(buf, *bytes_read), StringRef("Content-Length: 13\r\n\r\n"
+ R"json({"str":"foo"})json"));
+}
+
+TEST_F(JSONRPCTransportTest, MalformedRequests) {
+ std::string malformed_header = "notjson\n";
+ ASSERT_THAT_EXPECTED(
+ input.Write(malformed_header.data(), malformed_header.size()),
+ Succeeded());
+ ASSERT_THAT_EXPECTED(
+ transport->Read<JSONTestType>(std::chrono::milliseconds(1)),
+ llvm::Failed());
+}
+
+TEST_F(JSONRPCTransportTest, Read) {
+ std::string json = R"json({"str": "foo"})json";
+ std::string message = formatv("{0}\n", json).str();
+ ASSERT_THAT_EXPECTED(input.Write(message.data(), message.size()),
+ Succeeded());
+ ASSERT_THAT_EXPECTED(
+ transport->Read<JSONTestType>(std::chrono::milliseconds(1)),
+ HasValue(testing::FieldsAre(/*str=*/"foo")));
+}
+
+TEST_F(JSONRPCTransportTest, ReadWithTimeout) {
+ ASSERT_THAT_EXPECTED(
+ transport->Read<JSONTestType>(std::chrono::milliseconds(1)),
+ Failed<TransportTimeoutError>());
+}
+
+TEST_F(JSONRPCTransportTest, ReadWithEOF) {
+ input.CloseWriteFileDescriptor();
+ ASSERT_THAT_EXPECTED(
+ transport->Read<JSONTestType>(std::chrono::milliseconds(1)),
+ Failed<TransportEOFError>());
+}
+
+TEST_F(JSONRPCTransportTest, Write) {
+ ASSERT_THAT_ERROR(transport->Write(JSONTestType{"foo"}), Succeeded());
+ output.CloseWriteFileDescriptor();
+ char buf[1024];
+ Expected<size_t> bytes_read =
+ output.Read(buf, sizeof(buf), std::chrono::milliseconds(1));
+ ASSERT_THAT_EXPECTED(bytes_read, Succeeded());
+ ASSERT_EQ(StringRef(buf, *bytes_read), StringRef(R"json({"str":"foo"})json"
+ "\n"));
+}
diff --git a/lldb/unittests/TestingSupport/Host/PipeTestUtilities.h b/lldb/unittests/TestingSupport/Host/PipeTestUtilities.h
new file mode 100644
index 0000000000000..50d5d4117c898
--- /dev/null
+++ b/lldb/unittests/TestingSupport/Host/PipeTestUtilities.h
@@ -0,0 +1,28 @@
+//===-- PipeTestUtilities.cpp ---------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLDB_UNITTESTS_TESTINGSUPPORT_PIPETESTUTILITIES_H
+#define LLDB_UNITTESTS_TESTINGSUPPORT_PIPETESTUTILITIES_H
+
+#include "lldb/Host/Pipe.h"
+#include "llvm/Testing/Support/Error.h"
+#include "gtest/gtest.h"
+
+/// A base class for tests that need a pair of pipes for communication.
+class PipeTest : public testing::Test {
+protected:
+ lldb_private::Pipe input;
+ lldb_private::Pipe output;
+
+ void SetUp() override {
+ ASSERT_THAT_ERROR(input.CreateNew(false).ToError(), llvm::Succeeded());
+ ASSERT_THAT_ERROR(output.CreateNew(false).ToError(), llvm::Succeeded());
+ }
+};
+
+#endif
More information about the lldb-commits
mailing list