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