xref: /llvm-project/mlir/unittests/Tools/lsp-server-support/Transport.cpp (revision 11bda17254d00cde5b84585f7c7a870d6793ad92)
1 //===- Transport.cpp - LSP JSON transport unit tests ----------------------===//
2 //
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
6 //
7 //===----------------------------------------------------------------------===//
8 
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"
15 
16 using namespace mlir;
17 using namespace mlir::lsp;
18 using namespace testing;
19 
20 namespace {
21 
TEST(TransportTest,SendReply)22 TEST(TransportTest, SendReply) {
23   std::string out;
24   llvm::raw_string_ostream os(out);
25   JSONTransport transport(nullptr, os);
26   MessageHandler handler(transport);
27 
28   transport.reply(1989, nullptr);
29   EXPECT_THAT(out, HasSubstr("\"id\":1989"));
30   EXPECT_THAT(out, HasSubstr("\"result\":null"));
31 }
32 
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;
40 
41 protected:
TransportInputTest()42   TransportInputTest() : os(output) {}
43 
SetUp()44   void SetUp() override {
45     std::error_code ec =
46         llvm::sys::fs::createTemporaryFile("lsp-unittest", "json", inputPath);
47     ASSERT_FALSE(ec) << "Could not create temporary file: " << ec.message();
48 
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);
54   }
55 
TearDown()56   void TearDown() override {
57     EXPECT_EQ(std::fclose(in), 0)
58         << "Could not close temporary file FD: " << std::strerror(errno);
59     std::error_code ec =
60         llvm::sys::fs::remove(inputPath, /*IgnoreNonExisting=*/false);
61     EXPECT_FALSE(ec) << "Could not remove temporary file '" << inputPath.data()
62                      << "': " << ec.message();
63   }
64 
writeInput(StringRef buffer)65   void writeInput(StringRef buffer) {
66     std::error_code ec;
67     llvm::raw_fd_ostream os(inputPath.data(), ec);
68     ASSERT_FALSE(ec) << "Could not write to '" << inputPath.data()
69                      << "': " << ec.message();
70     os << buffer;
71     os.close();
72   }
73 
getOutput() const74   StringRef getOutput() const { return output; }
getMessageHandler()75   MessageHandler &getMessageHandler() { return *messageHandler; }
76 
runTransport()77   void runTransport() {
78     bool gotEOF = false;
79     llvm::Error err = llvm::handleErrors(
80         transport->run(*messageHandler), [&](const llvm::ECError &ecErr) {
81           gotEOF = ecErr.convertToErrorCode() == std::errc::io_error;
82         });
83     llvm::consumeError(std::move(err));
84     EXPECT_TRUE(gotEOF);
85   }
86 };
87 
TEST_F(TransportInputTest,RequestWithInvalidParams)88 TEST_F(TransportInputTest, RequestWithInvalidParams) {
89   struct Handler {
90     void onMethod(const TextDocumentItem &params,
91                   mlir::lsp::Callback<TextDocumentIdentifier> callback) {}
92   } handler;
93   getMessageHandler().method("invalid-params-request", &handler,
94                              &Handler::onMethod);
95 
96   writeInput("{\"jsonrpc\":\"2.0\",\"id\":92,"
97              "\"method\":\"invalid-params-request\",\"params\":{}}\n");
98   runTransport();
99   EXPECT_THAT(getOutput(), HasSubstr("error"));
100   EXPECT_THAT(getOutput(), HasSubstr("missing value at (root).uri"));
101 }
102 
TEST_F(TransportInputTest,NotificationWithInvalidParams)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
106   // of demonstration.
107   Logger::setLogLevel(Logger::Level::Error);
108 
109   struct Handler {
110     void onNotification(const TextDocumentItem &params) {}
111   } handler;
112   getMessageHandler().notification("invalid-params-notification", &handler,
113                                    &Handler::onNotification);
114 
115   writeInput("{\"jsonrpc\":\"2.0\",\"method\":\"invalid-params-notification\","
116              "\"params\":{}}\n");
117   runTransport();
118 }
119 
TEST_F(TransportInputTest,MethodNotFound)120 TEST_F(TransportInputTest, MethodNotFound) {
121   writeInput("{\"jsonrpc\":\"2.0\",\"id\":29,\"method\":\"ack\"}\n");
122   runTransport();
123   EXPECT_THAT(getOutput(), HasSubstr("\"id\":29"));
124   EXPECT_THAT(getOutput(), HasSubstr("\"error\""));
125   EXPECT_THAT(getOutput(), HasSubstr("\"message\":\"method not found: ack\""));
126 }
127 
TEST_F(TransportInputTest,OutgoingNotification)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\""));
133 }
134 
TEST_F(TransportInputTest,ResponseHandlerNotFound)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
138   // of demonstration.
139   Logger::setLogLevel(Logger::Level::Error);
140   writeInput("{\"jsonrpc\":\"2.0\",\"id\":81,\"result\":null}\n");
141   runTransport();
142 }
143 
TEST_F(TransportInputTest,OutgoingRequest)144 TEST_F(TransportInputTest, OutgoingRequest) {
145   // Make some outgoing requests.
146   int responseCallbackInvoked = 0;
147   auto callFn =
148       getMessageHandler().outgoingRequest<CompletionList, CompletionContext>(
149           "outgoing-request",
150           [&responseCallbackInvoked](llvm::json::Value id,
151                                      llvm::Expected<CompletionContext> result) {
152             // Make expectations on the expected response.
153             EXPECT_EQ(id, 83);
154             ASSERT_TRUE((bool)result);
155             EXPECT_EQ(result->triggerKind, CompletionTriggerKind::Invoked);
156             responseCallbackInvoked += 1;
157           });
158   callFn({}, 82);
159   callFn({}, 83);
160   callFn({}, 84);
161   EXPECT_THAT(getOutput(), HasSubstr("\"method\":\"outgoing-request\""));
162   EXPECT_EQ(responseCallbackInvoked, 0);
163 
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.
167   writeInput(
168       "{\"jsonrpc\":\"2.0\",\"id\":83,\"result\":{\"triggerKind\":1}}\n"
169       "// -----\n"
170       "{\"jsonrpc\":\"2.0\",\"id\":83,\"result\":{\"triggerKind\":3}}\n");
171   runTransport();
172   EXPECT_EQ(responseCallbackInvoked, 1);
173 }
174 
TEST_F(TransportInputTest,OutgoingRequestJSONParseFailure)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();
183         EXPECT_EQ(id, 109);
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;
191       });
192   callFn({}, 109);
193   EXPECT_EQ(responseCallbackInvoked, 0);
194 
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
197   // parse failure.
198   writeInput("{\"jsonrpc\":\"2.0\",\"id\":109,\"result\":{\"line\":7}}\n"
199              "// -----\n"
200              "{\"jsonrpc\":\"2.0\",\"id\":109,\"result\":{\"line\":3,"
201              "\"character\":2}}\n");
202   runTransport();
203   EXPECT_EQ(responseCallbackInvoked, 1);
204 }
205 } // namespace
206