Author: Jonas Devlieghere Date: 2025-06-12T14:52:43-07:00 New Revision: 8a2895ad89793591cd3f0114bc56cd345f651823
URL: https://github.com/llvm/llvm-project/commit/8a2895ad89793591cd3f0114bc56cd345f651823 DIFF: https://github.com/llvm/llvm-project/commit/8a2895ad89793591cd3f0114bc56cd345f651823.diff LOG: [lldb] Implement JSON RPC (newline delimited) Transport (#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. Added: lldb/unittests/Host/JSONTransportTest.cpp lldb/unittests/TestingSupport/Host/PipeTestUtilities.h Modified: lldb/include/lldb/Host/JSONTransport.h lldb/source/Host/common/JSONTransport.cpp lldb/unittests/DAP/CMakeLists.txt lldb/unittests/DAP/TestBase.cpp lldb/unittests/DAP/TestBase.h lldb/unittests/Host/CMakeLists.txt Removed: lldb/unittests/DAP/TransportTest.cpp ################################################################################ diff --git a/lldb/include/lldb/Host/JSONTransport.h b/lldb/include/lldb/Host/JSONTransport.h index 4db5e417ea852..4087cdf2b42f7 100644 --- a/lldb/include/lldb/Host/JSONTransport.h +++ b/lldb/include/lldb/Host/JSONTransport.h @@ -51,17 +51,17 @@ class TransportTimeoutError : public llvm::ErrorInfo<TransportTimeoutError> { } }; -class TransportClosedError : public llvm::ErrorInfo<TransportClosedError> { +class TransportInvalidError : public llvm::ErrorInfo<TransportInvalidError> { public: static char ID; - TransportClosedError() = default; + TransportInvalidError() = default; void log(llvm::raw_ostream &OS) const override { - OS << "transport is closed"; + OS << "transport IO object invalid"; } std::error_code convertToErrorCode() const override { - return llvm::inconvertibleErrorCode(); + return std::make_error_code(std::errc::not_connected); } }; @@ -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..1a0851d5c4365 100644 --- a/lldb/source/Host/common/JSONTransport.cpp +++ b/lldb/source/Host/common/JSONTransport.cpp @@ -31,7 +31,7 @@ static Expected<std::string> ReadFull(IOObject &descriptor, size_t length, std::optional<std::chrono::microseconds> timeout = std::nullopt) { if (!descriptor.IsValid()) - return llvm::make_error<TransportClosedError>(); + return llvm::make_error<TransportInvalidError>(); bool timeout_supported = true; // FIXME: SelectHelper does not work with NativeFile on Win32. @@ -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<TransportInvalidError>(); IOObject *input = m_input.get(); Expected<std::string> message_header = @@ -131,7 +131,7 @@ HTTPDelimitedJSONTransport::ReadImpl(const std::chrono::microseconds &timeout) { Error HTTPDelimitedJSONTransport::WriteImpl(const std::string &message) { if (!m_output || !m_output->IsValid()) - return llvm::make_error<TransportClosedError>(); + return llvm::make_error<TransportInvalidError>(); Log(llvm::formatv("<-- {0}", message).str()); @@ -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<TransportInvalidError>(); + + 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<TransportInvalidError>(); + + 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; +char TransportInvalidError::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..f1ec5e03bbeca --- /dev/null +++ b/lldb/unittests/Host/JSONTransportTest.cpp @@ -0,0 +1,176 @@ +//===-- 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, ReadWithEOF) { + input.CloseWriteFileDescriptor(); + ASSERT_THAT_EXPECTED( + transport->Read<JSONTestType>(std::chrono::milliseconds(1)), + Failed<TransportEOFError>()); +} + +TEST_F(HTTPDelimitedJSONTransportTest, ReadAfterClosed) { + input.CloseReadFileDescriptor(); + ASSERT_THAT_EXPECTED( + transport->Read<JSONTestType>(std::chrono::milliseconds(1)), + llvm::Failed()); +} + +TEST_F(HTTPDelimitedJSONTransportTest, InvalidTransport) { + transport = std::make_unique<HTTPDelimitedJSONTransport>(nullptr, nullptr); + ASSERT_THAT_EXPECTED( + transport->Read<JSONTestType>(std::chrono::milliseconds(1)), + Failed<TransportInvalidError>()); +} + +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, ReadWithEOF) { + input.CloseWriteFileDescriptor(); + ASSERT_THAT_EXPECTED( + transport->Read<JSONTestType>(std::chrono::milliseconds(1)), + Failed<TransportEOFError>()); +} + +TEST_F(JSONRPCTransportTest, ReadAfterClosed) { + input.CloseReadFileDescriptor(); + ASSERT_THAT_EXPECTED( + transport->Read<JSONTestType>(std::chrono::milliseconds(1)), + llvm::Failed()); +} + +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")); +} + +TEST_F(JSONRPCTransportTest, InvalidTransport) { + transport = std::make_unique<JSONRPCTransport>(nullptr, nullptr); + ASSERT_THAT_EXPECTED( + transport->Read<JSONTestType>(std::chrono::milliseconds(1)), + Failed<TransportInvalidError>()); +} + +#ifndef _WIN32 +TEST_F(HTTPDelimitedJSONTransportTest, ReadWithTimeout) { + ASSERT_THAT_EXPECTED( + transport->Read<JSONTestType>(std::chrono::milliseconds(1)), + Failed<TransportTimeoutError>()); +} + +TEST_F(JSONRPCTransportTest, ReadWithTimeout) { + ASSERT_THAT_EXPECTED( + transport->Read<JSONTestType>(std::chrono::milliseconds(1)), + Failed<TransportTimeoutError>()); +} +#endif 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 _______________________________________________ lldb-commits mailing list lldb-commits@lists.llvm.org https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits