xref: /llvm-project/mlir/unittests/Tools/lsp-server-support/Transport.cpp (revision 11bda17254d00cde5b84585f7c7a870d6793ad92)
1b811ad6fSBrian Gesiak //===- Transport.cpp - LSP JSON transport unit tests ----------------------===//
2b811ad6fSBrian Gesiak //
3b811ad6fSBrian Gesiak // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4b811ad6fSBrian Gesiak // See https://llvm.org/LICENSE.txt for license information.
5b811ad6fSBrian Gesiak // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6b811ad6fSBrian Gesiak //
7b811ad6fSBrian Gesiak //===----------------------------------------------------------------------===//
8b811ad6fSBrian Gesiak 
9b811ad6fSBrian Gesiak #include "mlir/Tools/lsp-server-support/Transport.h"
10b811ad6fSBrian Gesiak #include "mlir/Tools/lsp-server-support/Logging.h"
11b811ad6fSBrian Gesiak #include "mlir/Tools/lsp-server-support/Protocol.h"
12b811ad6fSBrian Gesiak #include "llvm/Support/FileSystem.h"
13b811ad6fSBrian Gesiak #include "gmock/gmock.h"
14b811ad6fSBrian Gesiak #include "gtest/gtest.h"
15b811ad6fSBrian Gesiak 
16b811ad6fSBrian Gesiak using namespace mlir;
17b811ad6fSBrian Gesiak using namespace mlir::lsp;
18b811ad6fSBrian Gesiak using namespace testing;
19b811ad6fSBrian Gesiak 
20b811ad6fSBrian Gesiak namespace {
21b811ad6fSBrian Gesiak 
TEST(TransportTest,SendReply)22b811ad6fSBrian Gesiak TEST(TransportTest, SendReply) {
23b811ad6fSBrian Gesiak   std::string out;
24b811ad6fSBrian Gesiak   llvm::raw_string_ostream os(out);
25b811ad6fSBrian Gesiak   JSONTransport transport(nullptr, os);
26b811ad6fSBrian Gesiak   MessageHandler handler(transport);
27b811ad6fSBrian Gesiak 
28b811ad6fSBrian Gesiak   transport.reply(1989, nullptr);
29b811ad6fSBrian Gesiak   EXPECT_THAT(out, HasSubstr("\"id\":1989"));
30b811ad6fSBrian Gesiak   EXPECT_THAT(out, HasSubstr("\"result\":null"));
31b811ad6fSBrian Gesiak }
32b811ad6fSBrian Gesiak 
33b811ad6fSBrian Gesiak class TransportInputTest : public Test {
34b811ad6fSBrian Gesiak   llvm::SmallVector<char> inputPath;
35b811ad6fSBrian Gesiak   std::FILE *in = nullptr;
36b811ad6fSBrian Gesiak   std::string output = "";
37b811ad6fSBrian Gesiak   llvm::raw_string_ostream os;
38b811ad6fSBrian Gesiak   std::optional<JSONTransport> transport = std::nullopt;
39b811ad6fSBrian Gesiak   std::optional<MessageHandler> messageHandler = std::nullopt;
40b811ad6fSBrian Gesiak 
41b811ad6fSBrian Gesiak protected:
TransportInputTest()42b811ad6fSBrian Gesiak   TransportInputTest() : os(output) {}
43b811ad6fSBrian Gesiak 
SetUp()44b811ad6fSBrian Gesiak   void SetUp() override {
45b811ad6fSBrian Gesiak     std::error_code ec =
46b811ad6fSBrian Gesiak         llvm::sys::fs::createTemporaryFile("lsp-unittest", "json", inputPath);
47b811ad6fSBrian Gesiak     ASSERT_FALSE(ec) << "Could not create temporary file: " << ec.message();
48b811ad6fSBrian Gesiak 
49b811ad6fSBrian Gesiak     in = std::fopen(inputPath.data(), "r");
50b811ad6fSBrian Gesiak     ASSERT_TRUE(in) << "Could not open temporary file: "
51b811ad6fSBrian Gesiak                     << std::strerror(errno);
52b811ad6fSBrian Gesiak     transport.emplace(in, os, JSONStreamStyle::Delimited);
53b811ad6fSBrian Gesiak     messageHandler.emplace(*transport);
54b811ad6fSBrian Gesiak   }
55b811ad6fSBrian Gesiak 
TearDown()56b811ad6fSBrian Gesiak   void TearDown() override {
57b811ad6fSBrian Gesiak     EXPECT_EQ(std::fclose(in), 0)
58b811ad6fSBrian Gesiak         << "Could not close temporary file FD: " << std::strerror(errno);
59b811ad6fSBrian Gesiak     std::error_code ec =
60b811ad6fSBrian Gesiak         llvm::sys::fs::remove(inputPath, /*IgnoreNonExisting=*/false);
61b811ad6fSBrian Gesiak     EXPECT_FALSE(ec) << "Could not remove temporary file '" << inputPath.data()
62b811ad6fSBrian Gesiak                      << "': " << ec.message();
63b811ad6fSBrian Gesiak   }
64b811ad6fSBrian Gesiak 
writeInput(StringRef buffer)65b811ad6fSBrian Gesiak   void writeInput(StringRef buffer) {
66b811ad6fSBrian Gesiak     std::error_code ec;
67b811ad6fSBrian Gesiak     llvm::raw_fd_ostream os(inputPath.data(), ec);
68b811ad6fSBrian Gesiak     ASSERT_FALSE(ec) << "Could not write to '" << inputPath.data()
69b811ad6fSBrian Gesiak                      << "': " << ec.message();
70b811ad6fSBrian Gesiak     os << buffer;
71b811ad6fSBrian Gesiak     os.close();
72b811ad6fSBrian Gesiak   }
73b811ad6fSBrian Gesiak 
getOutput() const74b811ad6fSBrian Gesiak   StringRef getOutput() const { return output; }
getMessageHandler()75b811ad6fSBrian Gesiak   MessageHandler &getMessageHandler() { return *messageHandler; }
76b811ad6fSBrian Gesiak 
runTransport()77b811ad6fSBrian Gesiak   void runTransport() {
78b811ad6fSBrian Gesiak     bool gotEOF = false;
79b811ad6fSBrian Gesiak     llvm::Error err = llvm::handleErrors(
80b811ad6fSBrian Gesiak         transport->run(*messageHandler), [&](const llvm::ECError &ecErr) {
81b811ad6fSBrian Gesiak           gotEOF = ecErr.convertToErrorCode() == std::errc::io_error;
82b811ad6fSBrian Gesiak         });
83b811ad6fSBrian Gesiak     llvm::consumeError(std::move(err));
84b811ad6fSBrian Gesiak     EXPECT_TRUE(gotEOF);
85b811ad6fSBrian Gesiak   }
86b811ad6fSBrian Gesiak };
87b811ad6fSBrian Gesiak 
TEST_F(TransportInputTest,RequestWithInvalidParams)88b811ad6fSBrian Gesiak TEST_F(TransportInputTest, RequestWithInvalidParams) {
89b811ad6fSBrian Gesiak   struct Handler {
90b811ad6fSBrian Gesiak     void onMethod(const TextDocumentItem &params,
91b811ad6fSBrian Gesiak                   mlir::lsp::Callback<TextDocumentIdentifier> callback) {}
92b811ad6fSBrian Gesiak   } handler;
93b811ad6fSBrian Gesiak   getMessageHandler().method("invalid-params-request", &handler,
94b811ad6fSBrian Gesiak                              &Handler::onMethod);
95b811ad6fSBrian Gesiak 
96b811ad6fSBrian Gesiak   writeInput("{\"jsonrpc\":\"2.0\",\"id\":92,"
97b811ad6fSBrian Gesiak              "\"method\":\"invalid-params-request\",\"params\":{}}\n");
98b811ad6fSBrian Gesiak   runTransport();
99b811ad6fSBrian Gesiak   EXPECT_THAT(getOutput(), HasSubstr("error"));
100b811ad6fSBrian Gesiak   EXPECT_THAT(getOutput(), HasSubstr("missing value at (root).uri"));
101b811ad6fSBrian Gesiak }
102b811ad6fSBrian Gesiak 
TEST_F(TransportInputTest,NotificationWithInvalidParams)103b811ad6fSBrian Gesiak TEST_F(TransportInputTest, NotificationWithInvalidParams) {
104b811ad6fSBrian Gesiak   // JSON parsing errors are only reported via error logging. As a result, this
105b811ad6fSBrian Gesiak   // test can't make any expectations -- but it prints the output anyway, by way
106b811ad6fSBrian Gesiak   // of demonstration.
107b811ad6fSBrian Gesiak   Logger::setLogLevel(Logger::Level::Error);
108b811ad6fSBrian Gesiak 
109b811ad6fSBrian Gesiak   struct Handler {
110b811ad6fSBrian Gesiak     void onNotification(const TextDocumentItem &params) {}
111b811ad6fSBrian Gesiak   } handler;
112b811ad6fSBrian Gesiak   getMessageHandler().notification("invalid-params-notification", &handler,
113b811ad6fSBrian Gesiak                                    &Handler::onNotification);
114b811ad6fSBrian Gesiak 
115b811ad6fSBrian Gesiak   writeInput("{\"jsonrpc\":\"2.0\",\"method\":\"invalid-params-notification\","
116b811ad6fSBrian Gesiak              "\"params\":{}}\n");
117b811ad6fSBrian Gesiak   runTransport();
118b811ad6fSBrian Gesiak }
119b811ad6fSBrian Gesiak 
TEST_F(TransportInputTest,MethodNotFound)120b811ad6fSBrian Gesiak TEST_F(TransportInputTest, MethodNotFound) {
121b811ad6fSBrian Gesiak   writeInput("{\"jsonrpc\":\"2.0\",\"id\":29,\"method\":\"ack\"}\n");
122b811ad6fSBrian Gesiak   runTransport();
123b811ad6fSBrian Gesiak   EXPECT_THAT(getOutput(), HasSubstr("\"id\":29"));
124b811ad6fSBrian Gesiak   EXPECT_THAT(getOutput(), HasSubstr("\"error\""));
125b811ad6fSBrian Gesiak   EXPECT_THAT(getOutput(), HasSubstr("\"message\":\"method not found: ack\""));
126b811ad6fSBrian Gesiak }
127b811ad6fSBrian Gesiak 
TEST_F(TransportInputTest,OutgoingNotification)128b811ad6fSBrian Gesiak TEST_F(TransportInputTest, OutgoingNotification) {
129b811ad6fSBrian Gesiak   auto notifyFn = getMessageHandler().outgoingNotification<CompletionList>(
130b811ad6fSBrian Gesiak       "outgoing-notification");
131b811ad6fSBrian Gesiak   notifyFn(CompletionList{});
132b811ad6fSBrian Gesiak   EXPECT_THAT(getOutput(), HasSubstr("\"method\":\"outgoing-notification\""));
133b811ad6fSBrian Gesiak }
134e24a7bbfSBrian Gesiak 
TEST_F(TransportInputTest,ResponseHandlerNotFound)135e24a7bbfSBrian Gesiak TEST_F(TransportInputTest, ResponseHandlerNotFound) {
136e24a7bbfSBrian Gesiak   // Unhandled responses are only reported via error logging. As a result, this
137e24a7bbfSBrian Gesiak   // test can't make any expectations -- but it prints the output anyway, by way
138e24a7bbfSBrian Gesiak   // of demonstration.
139e24a7bbfSBrian Gesiak   Logger::setLogLevel(Logger::Level::Error);
140e24a7bbfSBrian Gesiak   writeInput("{\"jsonrpc\":\"2.0\",\"id\":81,\"result\":null}\n");
141e24a7bbfSBrian Gesiak   runTransport();
142e24a7bbfSBrian Gesiak }
143e24a7bbfSBrian Gesiak 
TEST_F(TransportInputTest,OutgoingRequest)144e24a7bbfSBrian Gesiak TEST_F(TransportInputTest, OutgoingRequest) {
145e24a7bbfSBrian Gesiak   // Make some outgoing requests.
146e24a7bbfSBrian Gesiak   int responseCallbackInvoked = 0;
147*11bda172SBrian Gesiak   auto callFn =
148*11bda172SBrian Gesiak       getMessageHandler().outgoingRequest<CompletionList, CompletionContext>(
149e24a7bbfSBrian Gesiak           "outgoing-request",
150e24a7bbfSBrian Gesiak           [&responseCallbackInvoked](llvm::json::Value id,
151*11bda172SBrian Gesiak                                      llvm::Expected<CompletionContext> result) {
152e24a7bbfSBrian Gesiak             // Make expectations on the expected response.
153e24a7bbfSBrian Gesiak             EXPECT_EQ(id, 83);
154*11bda172SBrian Gesiak             ASSERT_TRUE((bool)result);
155*11bda172SBrian Gesiak             EXPECT_EQ(result->triggerKind, CompletionTriggerKind::Invoked);
156e24a7bbfSBrian Gesiak             responseCallbackInvoked += 1;
157e24a7bbfSBrian Gesiak           });
158e24a7bbfSBrian Gesiak   callFn({}, 82);
159e24a7bbfSBrian Gesiak   callFn({}, 83);
160e24a7bbfSBrian Gesiak   callFn({}, 84);
161e24a7bbfSBrian Gesiak   EXPECT_THAT(getOutput(), HasSubstr("\"method\":\"outgoing-request\""));
162e24a7bbfSBrian Gesiak   EXPECT_EQ(responseCallbackInvoked, 0);
163e24a7bbfSBrian Gesiak 
164e24a7bbfSBrian Gesiak   // One of the requests receives a response. The message handler handles this
165e24a7bbfSBrian Gesiak   // response by invoking the callback from above. Subsequent responses with the
166e24a7bbfSBrian Gesiak   // same ID are ignored.
167*11bda172SBrian Gesiak   writeInput(
168*11bda172SBrian Gesiak       "{\"jsonrpc\":\"2.0\",\"id\":83,\"result\":{\"triggerKind\":1}}\n"
169e24a7bbfSBrian Gesiak       "// -----\n"
170*11bda172SBrian Gesiak       "{\"jsonrpc\":\"2.0\",\"id\":83,\"result\":{\"triggerKind\":3}}\n");
171*11bda172SBrian Gesiak   runTransport();
172*11bda172SBrian Gesiak   EXPECT_EQ(responseCallbackInvoked, 1);
173*11bda172SBrian Gesiak }
174*11bda172SBrian Gesiak 
TEST_F(TransportInputTest,OutgoingRequestJSONParseFailure)175*11bda172SBrian Gesiak TEST_F(TransportInputTest, OutgoingRequestJSONParseFailure) {
176*11bda172SBrian Gesiak   // Make an outgoing request that expects a failure response.
177*11bda172SBrian Gesiak   bool responseCallbackInvoked = 0;
178*11bda172SBrian Gesiak   auto callFn = getMessageHandler().outgoingRequest<CompletionList, Position>(
179*11bda172SBrian Gesiak       "outgoing-request-json-parse-failure",
180*11bda172SBrian Gesiak       [&responseCallbackInvoked](llvm::json::Value id,
181*11bda172SBrian Gesiak                                  llvm::Expected<Position> result) {
182*11bda172SBrian Gesiak         llvm::Error err = result.takeError();
183*11bda172SBrian Gesiak         EXPECT_EQ(id, 109);
184*11bda172SBrian Gesiak         ASSERT_TRUE((bool)err);
185*11bda172SBrian Gesiak         EXPECT_THAT(debugString(err),
186*11bda172SBrian Gesiak                     HasSubstr("failed to decode "
187*11bda172SBrian Gesiak                               "reply:outgoing-request-json-parse-failure(109) "
188*11bda172SBrian Gesiak                               "response: missing value at (root).character"));
189*11bda172SBrian Gesiak         llvm::consumeError(std::move(err));
190*11bda172SBrian Gesiak         responseCallbackInvoked += 1;
191*11bda172SBrian Gesiak       });
192*11bda172SBrian Gesiak   callFn({}, 109);
193*11bda172SBrian Gesiak   EXPECT_EQ(responseCallbackInvoked, 0);
194*11bda172SBrian Gesiak 
195*11bda172SBrian Gesiak   // The request receives multiple responses, but only the first one triggers
196*11bda172SBrian Gesiak   // the response callback. The first response has erroneous JSON that causes a
197*11bda172SBrian Gesiak   // parse failure.
198*11bda172SBrian Gesiak   writeInput("{\"jsonrpc\":\"2.0\",\"id\":109,\"result\":{\"line\":7}}\n"
199*11bda172SBrian Gesiak              "// -----\n"
200*11bda172SBrian Gesiak              "{\"jsonrpc\":\"2.0\",\"id\":109,\"result\":{\"line\":3,"
201*11bda172SBrian Gesiak              "\"character\":2}}\n");
202e24a7bbfSBrian Gesiak   runTransport();
203e24a7bbfSBrian Gesiak   EXPECT_EQ(responseCallbackInvoked, 1);
204e24a7bbfSBrian Gesiak }
205b811ad6fSBrian Gesiak } // namespace
206