1//===-- JSONTransportTests.cpp -------------------------------------------===//
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#include "Protocol.h"
9#include "Transport.h"
10#include "support/Cancellation.h"
11#include "gmock/gmock.h"
12#include "gtest/gtest.h"
13#include <cstdio>
14
15namespace clang {
16namespace clangd {
17namespace {
18
19// No fmemopen on windows or on versions of MacOS X earlier than 10.13, so we
20// can't easily run this test.
21#if !(defined(_WIN32) || (defined(__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__) && \
22 __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ < 101300))
23
24// Fixture takes care of managing the input/output buffers for the transport.
25class JSONTransportTest : public ::testing::Test {
26 std::string InBuf, OutBuf, MirrorBuf;
27 llvm::raw_string_ostream Out, Mirror;
28 std::unique_ptr<FILE, int (*)(FILE *)> In;
29
30protected:
31 JSONTransportTest() : Out(OutBuf), Mirror(MirrorBuf), In(nullptr, nullptr) {}
32
33 template <typename... Args>
34 std::unique_ptr<Transport> transport(std::string InData, bool Pretty,
35 JSONStreamStyle Style) {
36 InBuf = std::move(InData);
37 In = {fmemopen(s: &InBuf[0], len: InBuf.size(), modes: "r"), &fclose};
38 return newJSONTransport(In: In.get(), Out, InMirror: &Mirror, Pretty, Style);
39 }
40
41 std::string input() const { return InBuf; }
42 std::string output() { return Out.str(); }
43 std::string inputMirror() { return Mirror.str(); }
44};
45
46// Echo is a simple server running on a transport:
47// - logs each message it gets.
48// - when it gets a call, replies to it
49// - when it gets a notification for method "call", makes a call on Target
50// Hangs up when it gets an exit notification.
51class Echo : public Transport::MessageHandler {
52 Transport &Target;
53 std::string LogBuf;
54 llvm::raw_string_ostream Log;
55
56public:
57 Echo(Transport &Target) : Target(Target), Log(LogBuf) {}
58
59 std::string log() { return Log.str(); }
60
61 bool onNotify(llvm::StringRef Method, llvm::json::Value Params) override {
62 Log << "Notification " << Method << ": " << Params << "\n";
63 if (Method == "call")
64 Target.call(Method: "echo call", Params: std::move(Params), ID: 42);
65 return Method != "exit";
66 }
67
68 bool onCall(llvm::StringRef Method, llvm::json::Value Params,
69 llvm::json::Value ID) override {
70 Log << "Call " << Method << "(" << ID << "): " << Params << "\n";
71 if (Method == "err")
72 Target.reply(
73 ID, Result: llvm::make_error<LSPError>(Args: "trouble at mill", Args: ErrorCode(88)));
74 else if (Method == "invalidated") // gone out skew on treadle
75 Target.reply(ID, Result: llvm::make_error<CancelledError>(
76 Args: static_cast<int>(ErrorCode::ContentModified)));
77 else
78 Target.reply(ID, Result: std::move(Params));
79 return true;
80 }
81
82 bool onReply(llvm::json::Value ID,
83 llvm::Expected<llvm::json::Value> Params) override {
84 if (Params)
85 Log << "Reply(" << ID << "): " << *Params << "\n";
86 else
87 Log << "Reply(" << ID
88 << "): error = " << llvm::toString(E: Params.takeError()) << "\n";
89 return true;
90 }
91};
92
93std::string trim(llvm::StringRef S) { return S.trim().str(); }
94
95// Runs an Echo session using the standard JSON-RPC format we use in production.
96TEST_F(JSONTransportTest, StandardDense) {
97 auto T = transport(
98 InData: "Content-Length: 52\r\n\r\n"
99 R"({"jsonrpc": "2.0", "method": "call", "params": 1234})"
100 "Content-Length: 46\r\n\r\n"
101 R"({"jsonrpc": "2.0", "id": 1234, "result": 5678})"
102 "Content-Length: 67\r\n\r\n"
103 R"({"jsonrpc": "2.0", "method": "foo", "id": "abcd", "params": "efgh"})"
104 "Content-Length: 73\r\n\r\n"
105 R"({"jsonrpc": "2.0", "id": "xyz", "error": {"code": 99, "message": "bad!"}})"
106 "Content-Length: 68\r\n\r\n"
107 R"({"jsonrpc": "2.0", "method": "err", "id": "wxyz", "params": "boom!"})"
108 "Content-Length: 36\r\n\r\n"
109 R"({"jsonrpc": "2.0", "method": "exit"})",
110 /*Pretty=*/false, Style: JSONStreamStyle::Standard);
111 Echo E(*T);
112 auto Err = T->loop(E);
113 EXPECT_FALSE(bool(Err)) << toString(E: std::move(Err));
114
115 const char *WantLog = R"(
116Notification call: 1234
117Reply(1234): 5678
118Call foo("abcd"): "efgh"
119Reply("xyz"): error = 99: bad!
120Call err("wxyz"): "boom!"
121Notification exit: null
122 )";
123 EXPECT_EQ(trim(E.log()), trim(WantLog));
124 const char *WantOutput =
125 "Content-Length: 60\r\n\r\n"
126 R"({"id":42,"jsonrpc":"2.0","method":"echo call","params":1234})"
127 "Content-Length: 45\r\n\r\n"
128 R"({"id":"abcd","jsonrpc":"2.0","result":"efgh"})"
129 "Content-Length: 77\r\n\r\n"
130 R"({"error":{"code":88,"message":"trouble at mill"},"id":"wxyz","jsonrpc":"2.0"})";
131 EXPECT_EQ(output(), WantOutput);
132 EXPECT_EQ(trim(inputMirror()), trim(input()));
133}
134
135// Runs an Echo session using the "delimited" input and pretty-printed output
136// that we use in lit tests.
137TEST_F(JSONTransportTest, DelimitedPretty) {
138 auto T = transport(InData: R"jsonrpc(
139{"jsonrpc": "2.0", "method": "call", "params": 1234}
140---
141{"jsonrpc": "2.0", "id": 1234, "result": 5678}
142---
143{"jsonrpc": "2.0", "method": "foo", "id": "abcd", "params": "efgh"}
144---
145{"jsonrpc": "2.0", "id": "xyz", "error": {"code": 99, "message": "bad!"}}
146---
147{"jsonrpc": "2.0", "method": "invalidated", "id": "wxyz", "params": "boom!"}
148---
149{"jsonrpc": "2.0", "method": "exit"}
150 )jsonrpc",
151 /*Pretty=*/true, Style: JSONStreamStyle::Delimited);
152 Echo E(*T);
153 auto Err = T->loop(E);
154 EXPECT_FALSE(bool(Err)) << toString(E: std::move(Err));
155
156 const char *WantLog = R"(
157Notification call: 1234
158Reply(1234): 5678
159Call foo("abcd"): "efgh"
160Reply("xyz"): error = 99: bad!
161Call invalidated("wxyz"): "boom!"
162Notification exit: null
163 )";
164 EXPECT_EQ(trim(E.log()), trim(WantLog));
165 const char *WantOutput = "Content-Length: 77\r\n\r\n"
166 R"({
167 "id": 42,
168 "jsonrpc": "2.0",
169 "method": "echo call",
170 "params": 1234
171})"
172 "Content-Length: 58\r\n\r\n"
173 R"({
174 "id": "abcd",
175 "jsonrpc": "2.0",
176 "result": "efgh"
177})"
178 "Content-Length: 145\r\n\r\n"
179 R"({
180 "error": {
181 "code": -32801,
182 "message": "Request cancelled because the document was modified"
183 },
184 "id": "wxyz",
185 "jsonrpc": "2.0"
186})";
187 EXPECT_EQ(output(), WantOutput);
188 EXPECT_EQ(trim(inputMirror()), trim(input()));
189}
190
191// IO errors such as EOF ane reported.
192// The only successful return from loop() is if a handler returned false.
193TEST_F(JSONTransportTest, EndOfFile) {
194 auto T = transport(InData: "Content-Length: 52\r\n\r\n"
195 R"({"jsonrpc": "2.0", "method": "call", "params": 1234})",
196 /*Pretty=*/false, Style: JSONStreamStyle::Standard);
197 Echo E(*T);
198 auto Err = T->loop(E);
199 EXPECT_EQ(trim(E.log()), "Notification call: 1234");
200 EXPECT_TRUE(bool(Err)); // Ran into EOF with no handler signalling done.
201 consumeError(Err: std::move(Err));
202 EXPECT_EQ(trim(inputMirror()), trim(input()));
203}
204
205#endif
206
207} // namespace
208} // namespace clangd
209} // namespace clang
210

source code of clang-tools-extra/clangd/unittests/JSONTransportTests.cpp