206 lines · cpp
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-exception6//7//===----------------------------------------------------------------------===//8 9#include "llvm/Support/LSP/Transport.h"10#include "llvm/Support/FileSystem.h"11#include "llvm/Support/LSP/Logging.h"12#include "llvm/Support/LSP/Protocol.h"13#include "gmock/gmock.h"14#include "gtest/gtest.h"15 16using namespace llvm;17using namespace llvm::lsp;18using namespace testing;19 20namespace {21 22TEST(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 33class 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 41protected:42 TransportInputTest() : os(output) {}43 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 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 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 74 StringRef getOutput() const { return output; }75 MessageHandler &getMessageHandler() { return *messageHandler; }76 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 88TEST_F(TransportInputTest, RequestWithInvalidParams) {89 struct Handler {90 void onMethod(const TextDocumentItem ¶ms,91 llvm::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 103TEST_F(TransportInputTest, NotificationWithInvalidParams) {104 // JSON parsing errors are only reported via error logging. As a result, this105 // test can't make any expectations -- but it prints the output anyway, by way106 // of demonstration.107 Logger::setLogLevel(Logger::Level::Error);108 109 struct Handler {110 void onNotification(const TextDocumentItem ¶ms) {}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 120TEST_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 128TEST_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 135TEST_F(TransportInputTest, ResponseHandlerNotFound) {136 // Unhandled responses are only reported via error logging. As a result, this137 // test can't make any expectations -- but it prints the output anyway, by way138 // of demonstration.139 Logger::setLogLevel(Logger::Level::Error);140 writeInput("{\"jsonrpc\":\"2.0\",\"id\":81,\"result\":null}\n");141 runTransport();142}143 144TEST_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](const 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 this165 // response by invoking the callback from above. Subsequent responses with the166 // 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 175TEST_F(TransportInputTest, OutgoingRequestJSONParseFailure) {176 // Make an outgoing request that expects a failure response.177 unsigned responseCallbackInvoked = 0;178 auto callFn = getMessageHandler().outgoingRequest<CompletionList, Position>(179 "outgoing-request-json-parse-failure",180 [&responseCallbackInvoked](const 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, 0u);194 195 // The request receives multiple responses, but only the first one triggers196 // the response callback. The first response has erroneous JSON that causes a197 // 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, 1u);204}205} // namespace206