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