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 ¶ms,
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 ¶ms) {}
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