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