1 //===- Transport.cpp - LSP JSON transport unit tests ----------------------===//
3 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4 // See https://llvm.org/LICENSE.txt for license information.
5 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
7 //===----------------------------------------------------------------------===//
9 #include "mlir/Tools/lsp-server-support/Transport.h"
10 #include "mlir/Tools/lsp-server-support/Logging.h"
11 #include "mlir/Tools/lsp-server-support/Protocol.h"
12 #include "llvm/Support/FileSystem.h"
13 #include "gmock/gmock.h"
14 #include "gtest/gtest.h"
17 using namespace mlir::lsp
;
18 using namespace testing
;
22 TEST(TransportTest
, SendReply
) {
24 llvm::raw_string_ostream
os(out
);
25 JSONTransport
transport(nullptr, os
);
26 MessageHandler
handler(transport
);
28 transport
.reply(1989, nullptr);
29 EXPECT_THAT(out
, HasSubstr("\"id\":1989"));
30 EXPECT_THAT(out
, HasSubstr("\"result\":null"));
33 class TransportInputTest
: public Test
{
34 llvm::SmallVector
<char> inputPath
;
35 std::FILE *in
= nullptr;
36 std::string output
= "";
37 llvm::raw_string_ostream os
;
38 std::optional
<JSONTransport
> transport
= std::nullopt
;
39 std::optional
<MessageHandler
> messageHandler
= std::nullopt
;
42 TransportInputTest() : os(output
) {}
44 void SetUp() override
{
46 llvm::sys::fs::createTemporaryFile("lsp-unittest", "json", inputPath
);
47 ASSERT_FALSE(ec
) << "Could not create temporary file: " << ec
.message();
49 in
= std::fopen(inputPath
.data(), "r");
50 ASSERT_TRUE(in
) << "Could not open temporary file: "
51 << std::strerror(errno
);
52 transport
.emplace(in
, os
, JSONStreamStyle::Delimited
);
53 messageHandler
.emplace(*transport
);
56 void TearDown() override
{
57 EXPECT_EQ(std::fclose(in
), 0)
58 << "Could not close temporary file FD: " << std::strerror(errno
);
60 llvm::sys::fs::remove(inputPath
, /*IgnoreNonExisting=*/false);
61 EXPECT_FALSE(ec
) << "Could not remove temporary file '" << inputPath
.data()
62 << "': " << ec
.message();
65 void writeInput(StringRef buffer
) {
67 llvm::raw_fd_ostream
os(inputPath
.data(), ec
);
68 ASSERT_FALSE(ec
) << "Could not write to '" << inputPath
.data()
69 << "': " << ec
.message();
74 StringRef
getOutput() const { return output
; }
75 MessageHandler
&getMessageHandler() { return *messageHandler
; }
79 llvm::Error err
= llvm::handleErrors(
80 transport
->run(*messageHandler
), [&](const llvm::ECError
&ecErr
) {
81 gotEOF
= ecErr
.convertToErrorCode() == std::errc::io_error
;
83 llvm::consumeError(std::move(err
));
88 TEST_F(TransportInputTest
, RequestWithInvalidParams
) {
90 void onMethod(const TextDocumentItem
¶ms
,
91 mlir::lsp::Callback
<TextDocumentIdentifier
> callback
) {}
93 getMessageHandler().method("invalid-params-request", &handler
,
96 writeInput("{\"jsonrpc\":\"2.0\",\"id\":92,"
97 "\"method\":\"invalid-params-request\",\"params\":{}}\n");
99 EXPECT_THAT(getOutput(), HasSubstr("error"));
100 EXPECT_THAT(getOutput(), HasSubstr("missing value at (root).uri"));
103 TEST_F(TransportInputTest
, NotificationWithInvalidParams
) {
104 // JSON parsing errors are only reported via error logging. As a result, this
105 // test can't make any expectations -- but it prints the output anyway, by way
107 Logger::setLogLevel(Logger::Level::Error
);
110 void onNotification(const TextDocumentItem
¶ms
) {}
112 getMessageHandler().notification("invalid-params-notification", &handler
,
113 &Handler::onNotification
);
115 writeInput("{\"jsonrpc\":\"2.0\",\"method\":\"invalid-params-notification\","
120 TEST_F(TransportInputTest
, MethodNotFound
) {
121 writeInput("{\"jsonrpc\":\"2.0\",\"id\":29,\"method\":\"ack\"}\n");
123 EXPECT_THAT(getOutput(), HasSubstr("\"id\":29"));
124 EXPECT_THAT(getOutput(), HasSubstr("\"error\""));
125 EXPECT_THAT(getOutput(), HasSubstr("\"message\":\"method not found: ack\""));
128 TEST_F(TransportInputTest
, OutgoingNotification
) {
129 auto notifyFn
= getMessageHandler().outgoingNotification
<CompletionList
>(
130 "outgoing-notification");
131 notifyFn(CompletionList
{});
132 EXPECT_THAT(getOutput(), HasSubstr("\"method\":\"outgoing-notification\""));
135 TEST_F(TransportInputTest
, ResponseHandlerNotFound
) {
136 // Unhandled responses are only reported via error logging. As a result, this
137 // test can't make any expectations -- but it prints the output anyway, by way
139 Logger::setLogLevel(Logger::Level::Error
);
140 writeInput("{\"jsonrpc\":\"2.0\",\"id\":81,\"result\":null}\n");
144 TEST_F(TransportInputTest
, OutgoingRequest
) {
145 // Make some outgoing requests.
146 int responseCallbackInvoked
= 0;
148 getMessageHandler().outgoingRequest
<CompletionList
, CompletionContext
>(
150 [&responseCallbackInvoked
](llvm::json::Value id
,
151 llvm::Expected
<CompletionContext
> result
) {
152 // Make expectations on the expected response.
154 ASSERT_TRUE((bool)result
);
155 EXPECT_EQ(result
->triggerKind
, CompletionTriggerKind::Invoked
);
156 responseCallbackInvoked
+= 1;
161 EXPECT_THAT(getOutput(), HasSubstr("\"method\":\"outgoing-request\""));
162 EXPECT_EQ(responseCallbackInvoked
, 0);
164 // One of the requests receives a response. The message handler handles this
165 // response by invoking the callback from above. Subsequent responses with the
166 // same ID are ignored.
168 "{\"jsonrpc\":\"2.0\",\"id\":83,\"result\":{\"triggerKind\":1}}\n"
170 "{\"jsonrpc\":\"2.0\",\"id\":83,\"result\":{\"triggerKind\":3}}\n");
172 EXPECT_EQ(responseCallbackInvoked
, 1);
175 TEST_F(TransportInputTest
, OutgoingRequestJSONParseFailure
) {
176 // Make an outgoing request that expects a failure response.
177 bool responseCallbackInvoked
= 0;
178 auto callFn
= getMessageHandler().outgoingRequest
<CompletionList
, Position
>(
179 "outgoing-request-json-parse-failure",
180 [&responseCallbackInvoked
](llvm::json::Value id
,
181 llvm::Expected
<Position
> result
) {
182 llvm::Error err
= result
.takeError();
184 ASSERT_TRUE((bool)err
);
185 EXPECT_THAT(debugString(err
),
186 HasSubstr("failed to decode "
187 "reply:outgoing-request-json-parse-failure(109) "
188 "response: missing value at (root).character"));
189 llvm::consumeError(std::move(err
));
190 responseCallbackInvoked
+= 1;
193 EXPECT_EQ(responseCallbackInvoked
, 0);
195 // The request receives multiple responses, but only the first one triggers
196 // the response callback. The first response has erroneous JSON that causes a
198 writeInput("{\"jsonrpc\":\"2.0\",\"id\":109,\"result\":{\"line\":7}}\n"
200 "{\"jsonrpc\":\"2.0\",\"id\":109,\"result\":{\"line\":3,"
201 "\"character\":2}}\n");
203 EXPECT_EQ(responseCallbackInvoked
, 1);