//===-- DraftStoreTests.cpp -------------------------------------*- C++ -*-===// // // The LLVM Compiler Infrastructure // // This file is distributed under the University of Illinois Open Source // License. See LICENSE.TXT for details. // //===----------------------------------------------------------------------===// #include "Annotations.h" #include "DraftStore.h" #include "SourceCode.h" #include "gmock/gmock.h" #include "gtest/gtest.h" namespace clang { namespace clangd { namespace { struct IncrementalTestStep { llvm::StringRef Src; llvm::StringRef Contents; }; int rangeLength(llvm::StringRef Code, const Range &Rng) { llvm::Expected Start = positionToOffset(Code, Rng.start); llvm::Expected End = positionToOffset(Code, Rng.end); assert(Start); assert(End); return *End - *Start; } /// Send the changes one by one to updateDraft, verify the intermediate results. void stepByStep(llvm::ArrayRef Steps) { DraftStore DS; Annotations InitialSrc(Steps.front().Src); constexpr llvm::StringLiteral Path("/hello.cpp"); // Set the initial content. DS.addDraft(Path, InitialSrc.code()); for (size_t i = 1; i < Steps.size(); i++) { Annotations SrcBefore(Steps[i - 1].Src); Annotations SrcAfter(Steps[i].Src); llvm::StringRef Contents = Steps[i - 1].Contents; TextDocumentContentChangeEvent Event{ SrcBefore.range(), rangeLength(SrcBefore.code(), SrcBefore.range()), Contents.str(), }; llvm::Expected Result = DS.updateDraft(Path, {Event}); ASSERT_TRUE(!!Result); EXPECT_EQ(*Result, SrcAfter.code()); EXPECT_EQ(*DS.getDraft(Path), SrcAfter.code()); } } /// Send all the changes at once to updateDraft, check only the final result. void allAtOnce(llvm::ArrayRef Steps) { DraftStore DS; Annotations InitialSrc(Steps.front().Src); Annotations FinalSrc(Steps.back().Src); constexpr llvm::StringLiteral Path("/hello.cpp"); std::vector Changes; for (size_t i = 0; i < Steps.size() - 1; i++) { Annotations Src(Steps[i].Src); llvm::StringRef Contents = Steps[i].Contents; Changes.push_back({ Src.range(), rangeLength(Src.code(), Src.range()), Contents.str(), }); } // Set the initial content. DS.addDraft(Path, InitialSrc.code()); llvm::Expected Result = DS.updateDraft(Path, Changes); ASSERT_TRUE(!!Result) << llvm::toString(Result.takeError()); EXPECT_EQ(*Result, FinalSrc.code()); EXPECT_EQ(*DS.getDraft(Path), FinalSrc.code()); } TEST(DraftStoreIncrementalUpdateTest, Simple) { // clang-format off IncrementalTestStep Steps[] = { // Replace a range { R"cpp(static int hello[[World]]() {})cpp", "Universe" }, // Delete a range { R"cpp(static int hello[[Universe]]() {})cpp", "" }, // Add a range { R"cpp(static int hello[[]]() {})cpp", "Monde" }, { R"cpp(static int helloMonde() {})cpp", "" } }; // clang-format on stepByStep(Steps); allAtOnce(Steps); } TEST(DraftStoreIncrementalUpdateTest, MultiLine) { // clang-format off IncrementalTestStep Steps[] = { // Replace a range { R"cpp(static [[int helloWorld]]() {})cpp", R"cpp(char welcome)cpp" }, // Delete a range { R"cpp(static char[[ welcome]]() {})cpp", "" }, // Add a range { R"cpp(static char[[]]() {})cpp", R"cpp( cookies)cpp" }, // Replace the whole file { R"cpp([[static char cookies() {}]])cpp", R"cpp(#include )cpp" }, // Delete the whole file { R"cpp([[#include ]])cpp", "", }, // Add something to an empty file { "[[]]", R"cpp(int main() { )cpp", }, { R"cpp(int main() { )cpp", "" } }; // clang-format on stepByStep(Steps); allAtOnce(Steps); } TEST(DraftStoreIncrementalUpdateTest, WrongRangeLength) { DraftStore DS; Path File = "foo.cpp"; DS.addDraft(File, "int main() {}\n"); TextDocumentContentChangeEvent Change; Change.range.emplace(); Change.range->start.line = 0; Change.range->start.character = 0; Change.range->end.line = 0; Change.range->end.character = 2; Change.rangeLength = 10; Expected Result = DS.updateDraft(File, {Change}); EXPECT_TRUE(!Result); EXPECT_EQ( toString(Result.takeError()), "Change's rangeLength (10) doesn't match the computed range length (2)."); } TEST(DraftStoreIncrementalUpdateTest, EndBeforeStart) { DraftStore DS; Path File = "foo.cpp"; DS.addDraft(File, "int main() {}\n"); TextDocumentContentChangeEvent Change; Change.range.emplace(); Change.range->start.line = 0; Change.range->start.character = 5; Change.range->end.line = 0; Change.range->end.character = 3; Expected Result = DS.updateDraft(File, {Change}); EXPECT_TRUE(!Result); EXPECT_EQ(toString(Result.takeError()), "Range's end position (0:3) is before start position (0:5)"); } TEST(DraftStoreIncrementalUpdateTest, StartCharOutOfRange) { DraftStore DS; Path File = "foo.cpp"; DS.addDraft(File, "int main() {}\n"); TextDocumentContentChangeEvent Change; Change.range.emplace(); Change.range->start.line = 0; Change.range->start.character = 100; Change.range->end.line = 0; Change.range->end.character = 100; Change.text = "foo"; Expected Result = DS.updateDraft(File, {Change}); EXPECT_TRUE(!Result); EXPECT_EQ(toString(Result.takeError()), "UTF-16 offset 100 is invalid for line 0"); } TEST(DraftStoreIncrementalUpdateTest, EndCharOutOfRange) { DraftStore DS; Path File = "foo.cpp"; DS.addDraft(File, "int main() {}\n"); TextDocumentContentChangeEvent Change; Change.range.emplace(); Change.range->start.line = 0; Change.range->start.character = 0; Change.range->end.line = 0; Change.range->end.character = 100; Change.text = "foo"; Expected Result = DS.updateDraft(File, {Change}); EXPECT_TRUE(!Result); EXPECT_EQ(toString(Result.takeError()), "UTF-16 offset 100 is invalid for line 0"); } TEST(DraftStoreIncrementalUpdateTest, StartLineOutOfRange) { DraftStore DS; Path File = "foo.cpp"; DS.addDraft(File, "int main() {}\n"); TextDocumentContentChangeEvent Change; Change.range.emplace(); Change.range->start.line = 100; Change.range->start.character = 0; Change.range->end.line = 100; Change.range->end.character = 0; Change.text = "foo"; Expected Result = DS.updateDraft(File, {Change}); EXPECT_TRUE(!Result); EXPECT_EQ(toString(Result.takeError()), "Line value is out of range (100)"); } TEST(DraftStoreIncrementalUpdateTest, EndLineOutOfRange) { DraftStore DS; Path File = "foo.cpp"; DS.addDraft(File, "int main() {}\n"); TextDocumentContentChangeEvent Change; Change.range.emplace(); Change.range->start.line = 0; Change.range->start.character = 0; Change.range->end.line = 100; Change.range->end.character = 0; Change.text = "foo"; Expected Result = DS.updateDraft(File, {Change}); EXPECT_TRUE(!Result); EXPECT_EQ(toString(Result.takeError()), "Line value is out of range (100)"); } /// Check that if a valid change is followed by an invalid change, the original /// version of the document (prior to all changes) is kept. TEST(DraftStoreIncrementalUpdateTest, InvalidRangeInASequence) { DraftStore DS; Path File = "foo.cpp"; StringRef OriginalContents = "int main() {}\n"; DS.addDraft(File, OriginalContents); // The valid change TextDocumentContentChangeEvent Change1; Change1.range.emplace(); Change1.range->start.line = 0; Change1.range->start.character = 0; Change1.range->end.line = 0; Change1.range->end.character = 0; Change1.text = "Hello "; // The invalid change TextDocumentContentChangeEvent Change2; Change2.range.emplace(); Change2.range->start.line = 0; Change2.range->start.character = 5; Change2.range->end.line = 0; Change2.range->end.character = 100; Change2.text = "something"; Expected Result = DS.updateDraft(File, {Change1, Change2}); EXPECT_TRUE(!Result); EXPECT_EQ(toString(Result.takeError()), "UTF-16 offset 100 is invalid for line 0"); Optional Contents = DS.getDraft(File); EXPECT_TRUE(Contents); EXPECT_EQ(*Contents, OriginalContents); } } // namespace } // namespace clangd } // namespace clang