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