1 //===-- DraftStoreTests.cpp -------------------------------------*- C++ -*-===//
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 "Annotations.h"
10 #include "DraftStore.h"
11 #include "SourceCode.h"
12 #include "gmock/gmock.h"
13 #include "gtest/gtest.h"
14
15 namespace clang {
16 namespace clangd {
17 namespace {
18
19 struct IncrementalTestStep {
20 llvm::StringRef Src;
21 llvm::StringRef Contents;
22 };
23
rangeLength(llvm::StringRef Code,const Range & Rng)24 int rangeLength(llvm::StringRef Code, const Range &Rng) {
25 llvm::Expected<size_t> Start = positionToOffset(Code, Rng.start);
26 llvm::Expected<size_t> End = positionToOffset(Code, Rng.end);
27 assert(Start);
28 assert(End);
29 return *End - *Start;
30 }
31
32 /// Send the changes one by one to updateDraft, verify the intermediate results.
stepByStep(llvm::ArrayRef<IncrementalTestStep> Steps)33 void stepByStep(llvm::ArrayRef<IncrementalTestStep> Steps) {
34 DraftStore DS;
35 Annotations InitialSrc(Steps.front().Src);
36 constexpr llvm::StringLiteral Path("/hello.cpp");
37
38 // Set the initial content.
39 DS.addDraft(Path, InitialSrc.code());
40
41 for (size_t i = 1; i < Steps.size(); i++) {
42 Annotations SrcBefore(Steps[i - 1].Src);
43 Annotations SrcAfter(Steps[i].Src);
44 llvm::StringRef Contents = Steps[i - 1].Contents;
45 TextDocumentContentChangeEvent Event{
46 SrcBefore.range(),
47 rangeLength(SrcBefore.code(), SrcBefore.range()),
48 Contents.str(),
49 };
50
51 llvm::Expected<std::string> Result = DS.updateDraft(Path, {Event});
52 ASSERT_TRUE(!!Result);
53 EXPECT_EQ(*Result, SrcAfter.code());
54 EXPECT_EQ(*DS.getDraft(Path), SrcAfter.code());
55 }
56 }
57
58 /// Send all the changes at once to updateDraft, check only the final result.
allAtOnce(llvm::ArrayRef<IncrementalTestStep> Steps)59 void allAtOnce(llvm::ArrayRef<IncrementalTestStep> Steps) {
60 DraftStore DS;
61 Annotations InitialSrc(Steps.front().Src);
62 Annotations FinalSrc(Steps.back().Src);
63 constexpr llvm::StringLiteral Path("/hello.cpp");
64 std::vector<TextDocumentContentChangeEvent> Changes;
65
66 for (size_t i = 0; i < Steps.size() - 1; i++) {
67 Annotations Src(Steps[i].Src);
68 llvm::StringRef Contents = Steps[i].Contents;
69
70 Changes.push_back({
71 Src.range(),
72 rangeLength(Src.code(), Src.range()),
73 Contents.str(),
74 });
75 }
76
77 // Set the initial content.
78 DS.addDraft(Path, InitialSrc.code());
79
80 llvm::Expected<std::string> Result = DS.updateDraft(Path, Changes);
81
82 ASSERT_TRUE(!!Result) << llvm::toString(Result.takeError());
83 EXPECT_EQ(*Result, FinalSrc.code());
84 EXPECT_EQ(*DS.getDraft(Path), FinalSrc.code());
85 }
86
TEST(DraftStoreIncrementalUpdateTest,Simple)87 TEST(DraftStoreIncrementalUpdateTest, Simple) {
88 // clang-format off
89 IncrementalTestStep Steps[] =
90 {
91 // Replace a range
92 {
93 R"cpp(static int
94 hello[[World]]()
95 {})cpp",
96 "Universe"
97 },
98 // Delete a range
99 {
100 R"cpp(static int
101 hello[[Universe]]()
102 {})cpp",
103 ""
104 },
105 // Add a range
106 {
107 R"cpp(static int
108 hello[[]]()
109 {})cpp",
110 "Monde"
111 },
112 {
113 R"cpp(static int
114 helloMonde()
115 {})cpp",
116 ""
117 }
118 };
119 // clang-format on
120
121 stepByStep(Steps);
122 allAtOnce(Steps);
123 }
124
TEST(DraftStoreIncrementalUpdateTest,MultiLine)125 TEST(DraftStoreIncrementalUpdateTest, MultiLine) {
126 // clang-format off
127 IncrementalTestStep Steps[] =
128 {
129 // Replace a range
130 {
131 R"cpp(static [[int
132 helloWorld]]()
133 {})cpp",
134 R"cpp(char
135 welcome)cpp"
136 },
137 // Delete a range
138 {
139 R"cpp(static char[[
140 welcome]]()
141 {})cpp",
142 ""
143 },
144 // Add a range
145 {
146 R"cpp(static char[[]]()
147 {})cpp",
148 R"cpp(
149 cookies)cpp"
150 },
151 // Replace the whole file
152 {
153 R"cpp([[static char
154 cookies()
155 {}]])cpp",
156 R"cpp(#include <stdio.h>
157 )cpp"
158 },
159 // Delete the whole file
160 {
161 R"cpp([[#include <stdio.h>
162 ]])cpp",
163 "",
164 },
165 // Add something to an empty file
166 {
167 "[[]]",
168 R"cpp(int main() {
169 )cpp",
170 },
171 {
172 R"cpp(int main() {
173 )cpp",
174 ""
175 }
176 };
177 // clang-format on
178
179 stepByStep(Steps);
180 allAtOnce(Steps);
181 }
182
TEST(DraftStoreIncrementalUpdateTest,WrongRangeLength)183 TEST(DraftStoreIncrementalUpdateTest, WrongRangeLength) {
184 DraftStore DS;
185 Path File = "foo.cpp";
186
187 DS.addDraft(File, "int main() {}\n");
188
189 TextDocumentContentChangeEvent Change;
190 Change.range.emplace();
191 Change.range->start.line = 0;
192 Change.range->start.character = 0;
193 Change.range->end.line = 0;
194 Change.range->end.character = 2;
195 Change.rangeLength = 10;
196
197 Expected<std::string> Result = DS.updateDraft(File, {Change});
198
199 EXPECT_TRUE(!Result);
200 EXPECT_EQ(
201 toString(Result.takeError()),
202 "Change's rangeLength (10) doesn't match the computed range length (2).");
203 }
204
TEST(DraftStoreIncrementalUpdateTest,EndBeforeStart)205 TEST(DraftStoreIncrementalUpdateTest, EndBeforeStart) {
206 DraftStore DS;
207 Path File = "foo.cpp";
208
209 DS.addDraft(File, "int main() {}\n");
210
211 TextDocumentContentChangeEvent Change;
212 Change.range.emplace();
213 Change.range->start.line = 0;
214 Change.range->start.character = 5;
215 Change.range->end.line = 0;
216 Change.range->end.character = 3;
217
218 Expected<std::string> Result = DS.updateDraft(File, {Change});
219
220 EXPECT_TRUE(!Result);
221 EXPECT_EQ(toString(Result.takeError()),
222 "Range's end position (0:3) is before start position (0:5)");
223 }
224
TEST(DraftStoreIncrementalUpdateTest,StartCharOutOfRange)225 TEST(DraftStoreIncrementalUpdateTest, StartCharOutOfRange) {
226 DraftStore DS;
227 Path File = "foo.cpp";
228
229 DS.addDraft(File, "int main() {}\n");
230
231 TextDocumentContentChangeEvent Change;
232 Change.range.emplace();
233 Change.range->start.line = 0;
234 Change.range->start.character = 100;
235 Change.range->end.line = 0;
236 Change.range->end.character = 100;
237 Change.text = "foo";
238
239 Expected<std::string> Result = DS.updateDraft(File, {Change});
240
241 EXPECT_TRUE(!Result);
242 EXPECT_EQ(toString(Result.takeError()),
243 "utf-16 offset 100 is invalid for line 0");
244 }
245
TEST(DraftStoreIncrementalUpdateTest,EndCharOutOfRange)246 TEST(DraftStoreIncrementalUpdateTest, EndCharOutOfRange) {
247 DraftStore DS;
248 Path File = "foo.cpp";
249
250 DS.addDraft(File, "int main() {}\n");
251
252 TextDocumentContentChangeEvent Change;
253 Change.range.emplace();
254 Change.range->start.line = 0;
255 Change.range->start.character = 0;
256 Change.range->end.line = 0;
257 Change.range->end.character = 100;
258 Change.text = "foo";
259
260 Expected<std::string> Result = DS.updateDraft(File, {Change});
261
262 EXPECT_TRUE(!Result);
263 EXPECT_EQ(toString(Result.takeError()),
264 "utf-16 offset 100 is invalid for line 0");
265 }
266
TEST(DraftStoreIncrementalUpdateTest,StartLineOutOfRange)267 TEST(DraftStoreIncrementalUpdateTest, StartLineOutOfRange) {
268 DraftStore DS;
269 Path File = "foo.cpp";
270
271 DS.addDraft(File, "int main() {}\n");
272
273 TextDocumentContentChangeEvent Change;
274 Change.range.emplace();
275 Change.range->start.line = 100;
276 Change.range->start.character = 0;
277 Change.range->end.line = 100;
278 Change.range->end.character = 0;
279 Change.text = "foo";
280
281 Expected<std::string> Result = DS.updateDraft(File, {Change});
282
283 EXPECT_TRUE(!Result);
284 EXPECT_EQ(toString(Result.takeError()), "Line value is out of range (100)");
285 }
286
TEST(DraftStoreIncrementalUpdateTest,EndLineOutOfRange)287 TEST(DraftStoreIncrementalUpdateTest, EndLineOutOfRange) {
288 DraftStore DS;
289 Path File = "foo.cpp";
290
291 DS.addDraft(File, "int main() {}\n");
292
293 TextDocumentContentChangeEvent Change;
294 Change.range.emplace();
295 Change.range->start.line = 0;
296 Change.range->start.character = 0;
297 Change.range->end.line = 100;
298 Change.range->end.character = 0;
299 Change.text = "foo";
300
301 Expected<std::string> Result = DS.updateDraft(File, {Change});
302
303 EXPECT_TRUE(!Result);
304 EXPECT_EQ(toString(Result.takeError()), "Line value is out of range (100)");
305 }
306
307 /// Check that if a valid change is followed by an invalid change, the original
308 /// version of the document (prior to all changes) is kept.
TEST(DraftStoreIncrementalUpdateTest,InvalidRangeInASequence)309 TEST(DraftStoreIncrementalUpdateTest, InvalidRangeInASequence) {
310 DraftStore DS;
311 Path File = "foo.cpp";
312
313 StringRef OriginalContents = "int main() {}\n";
314 DS.addDraft(File, OriginalContents);
315
316 // The valid change
317 TextDocumentContentChangeEvent Change1;
318 Change1.range.emplace();
319 Change1.range->start.line = 0;
320 Change1.range->start.character = 0;
321 Change1.range->end.line = 0;
322 Change1.range->end.character = 0;
323 Change1.text = "Hello ";
324
325 // The invalid change
326 TextDocumentContentChangeEvent Change2;
327 Change2.range.emplace();
328 Change2.range->start.line = 0;
329 Change2.range->start.character = 5;
330 Change2.range->end.line = 0;
331 Change2.range->end.character = 100;
332 Change2.text = "something";
333
334 Expected<std::string> Result = DS.updateDraft(File, {Change1, Change2});
335
336 EXPECT_TRUE(!Result);
337 EXPECT_EQ(toString(Result.takeError()),
338 "utf-16 offset 100 is invalid for line 0");
339
340 Optional<std::string> Contents = DS.getDraft(File);
341 EXPECT_TRUE(Contents);
342 EXPECT_EQ(*Contents, OriginalContents);
343 }
344
345 } // namespace
346 } // namespace clangd
347 } // namespace clang
348