diff options
Diffstat (limited to 'clangd/unittests/ClangdTests.cpp')
-rw-r--r-- | clangd/unittests/ClangdTests.cpp | 1162 |
1 files changed, 1162 insertions, 0 deletions
diff --git a/clangd/unittests/ClangdTests.cpp b/clangd/unittests/ClangdTests.cpp new file mode 100644 index 00000000..5d98bdc2 --- /dev/null +++ b/clangd/unittests/ClangdTests.cpp @@ -0,0 +1,1162 @@ +//===-- ClangdTests.cpp - Clangd unit tests ---------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "Annotations.h" +#include "ClangdLSPServer.h" +#include "ClangdServer.h" +#include "GlobalCompilationDatabase.h" +#include "Matchers.h" +#include "SyncAPI.h" +#include "TestFS.h" +#include "Threading.h" +#include "URI.h" +#include "clang/Config/config.h" +#include "clang/Sema/CodeCompleteConsumer.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringMap.h" +#include "llvm/Support/Errc.h" +#include "llvm/Support/Path.h" +#include "llvm/Support/Regex.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include <algorithm> +#include <chrono> +#include <iostream> +#include <random> +#include <string> +#include <thread> +#include <vector> + +namespace clang { +namespace clangd { + +namespace { + +using ::testing::ElementsAre; +using ::testing::Field; +using ::testing::Gt; +using ::testing::IsEmpty; +using ::testing::Pair; +using ::testing::UnorderedElementsAre; + +MATCHER_P2(DeclAt, File, Range, "") { + return arg.PreferredDeclaration == + Location{URIForFile::canonicalize(File, testRoot()), Range}; +} + +bool diagsContainErrors(const std::vector<Diag> &Diagnostics) { + for (auto D : Diagnostics) { + if (D.Severity == DiagnosticsEngine::Error || + D.Severity == DiagnosticsEngine::Fatal) + return true; + } + return false; +} + +class ErrorCheckingDiagConsumer : public DiagnosticsConsumer { +public: + void onDiagnosticsReady(PathRef File, + std::vector<Diag> Diagnostics) override { + bool HadError = diagsContainErrors(Diagnostics); + std::lock_guard<std::mutex> Lock(Mutex); + HadErrorInLastDiags = HadError; + } + + bool hadErrorInLastDiags() { + std::lock_guard<std::mutex> Lock(Mutex); + return HadErrorInLastDiags; + } + +private: + std::mutex Mutex; + bool HadErrorInLastDiags = false; +}; + +/// For each file, record whether the last published diagnostics contained at +/// least one error. +class MultipleErrorCheckingDiagConsumer : public DiagnosticsConsumer { +public: + void onDiagnosticsReady(PathRef File, + std::vector<Diag> Diagnostics) override { + bool HadError = diagsContainErrors(Diagnostics); + + std::lock_guard<std::mutex> Lock(Mutex); + LastDiagsHadError[File] = HadError; + } + + /// Exposes all files consumed by onDiagnosticsReady in an unspecified order. + /// For each file, a bool value indicates whether the last diagnostics + /// contained an error. + std::vector<std::pair<Path, bool>> filesWithDiags() const { + std::vector<std::pair<Path, bool>> Result; + std::lock_guard<std::mutex> Lock(Mutex); + + for (const auto &it : LastDiagsHadError) { + Result.emplace_back(it.first(), it.second); + } + + return Result; + } + + void clear() { + std::lock_guard<std::mutex> Lock(Mutex); + LastDiagsHadError.clear(); + } + +private: + mutable std::mutex Mutex; + llvm::StringMap<bool> LastDiagsHadError; +}; + +/// Replaces all patterns of the form 0x123abc with spaces +std::string replacePtrsInDump(std::string const &Dump) { + llvm::Regex RE("0x[0-9a-fA-F]+"); + llvm::SmallVector<llvm::StringRef, 1> Matches; + llvm::StringRef Pending = Dump; + + std::string Result; + while (RE.match(Pending, &Matches)) { + assert(Matches.size() == 1 && "Exactly one match expected"); + auto MatchPos = Matches[0].data() - Pending.data(); + + Result += Pending.take_front(MatchPos); + Pending = Pending.drop_front(MatchPos + Matches[0].size()); + } + Result += Pending; + + return Result; +} + +std::string dumpASTWithoutMemoryLocs(ClangdServer &Server, PathRef File) { + auto DumpWithMemLocs = runDumpAST(Server, File); + return replacePtrsInDump(DumpWithMemLocs); +} + +class ClangdVFSTest : public ::testing::Test { +protected: + std::string parseSourceAndDumpAST( + PathRef SourceFileRelPath, llvm::StringRef SourceContents, + std::vector<std::pair<PathRef, llvm::StringRef>> ExtraFiles = {}, + bool ExpectErrors = false) { + MockFSProvider FS; + ErrorCheckingDiagConsumer DiagConsumer; + MockCompilationDatabase CDB; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + for (const auto &FileWithContents : ExtraFiles) + FS.Files[testPath(FileWithContents.first)] = FileWithContents.second; + + auto SourceFilename = testPath(SourceFileRelPath); + Server.addDocument(SourceFilename, SourceContents); + auto Result = dumpASTWithoutMemoryLocs(Server, SourceFilename); + EXPECT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for diagnostics"; + EXPECT_EQ(ExpectErrors, DiagConsumer.hadErrorInLastDiags()); + return Result; + } +}; + +TEST_F(ClangdVFSTest, Parse) { + // FIXME: figure out a stable format for AST dumps, so that we can check the + // output of the dump itself is equal to the expected one, not just that it's + // different. + auto Empty = parseSourceAndDumpAST("foo.cpp", "", {}); + auto OneDecl = parseSourceAndDumpAST("foo.cpp", "int a;", {}); + auto SomeDecls = parseSourceAndDumpAST("foo.cpp", "int a; int b; int c;", {}); + EXPECT_NE(Empty, OneDecl); + EXPECT_NE(Empty, SomeDecls); + EXPECT_NE(SomeDecls, OneDecl); + + auto Empty2 = parseSourceAndDumpAST("foo.cpp", ""); + auto OneDecl2 = parseSourceAndDumpAST("foo.cpp", "int a;"); + auto SomeDecls2 = parseSourceAndDumpAST("foo.cpp", "int a; int b; int c;"); + EXPECT_EQ(Empty, Empty2); + EXPECT_EQ(OneDecl, OneDecl2); + EXPECT_EQ(SomeDecls, SomeDecls2); +} + +TEST_F(ClangdVFSTest, ParseWithHeader) { + parseSourceAndDumpAST("foo.cpp", "#include \"foo.h\"", {}, + /*ExpectErrors=*/true); + parseSourceAndDumpAST("foo.cpp", "#include \"foo.h\"", {{"foo.h", ""}}, + /*ExpectErrors=*/false); + + const auto SourceContents = R"cpp( +#include "foo.h" +int b = a; +)cpp"; + parseSourceAndDumpAST("foo.cpp", SourceContents, {{"foo.h", ""}}, + /*ExpectErrors=*/true); + parseSourceAndDumpAST("foo.cpp", SourceContents, {{"foo.h", "int a;"}}, + /*ExpectErrors=*/false); +} + +TEST_F(ClangdVFSTest, Reparse) { + MockFSProvider FS; + ErrorCheckingDiagConsumer DiagConsumer; + MockCompilationDatabase CDB; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + const auto SourceContents = R"cpp( +#include "foo.h" +int b = a; +)cpp"; + + auto FooCpp = testPath("foo.cpp"); + + FS.Files[testPath("foo.h")] = "int a;"; + FS.Files[FooCpp] = SourceContents; + + Server.addDocument(FooCpp, SourceContents); + auto DumpParse1 = dumpASTWithoutMemoryLocs(Server, FooCpp); + ASSERT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for diagnostics"; + EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags()); + + Server.addDocument(FooCpp, ""); + auto DumpParseEmpty = dumpASTWithoutMemoryLocs(Server, FooCpp); + ASSERT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for diagnostics"; + EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags()); + + Server.addDocument(FooCpp, SourceContents); + auto DumpParse2 = dumpASTWithoutMemoryLocs(Server, FooCpp); + ASSERT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for diagnostics"; + EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags()); + + EXPECT_EQ(DumpParse1, DumpParse2); + EXPECT_NE(DumpParse1, DumpParseEmpty); +} + +TEST_F(ClangdVFSTest, ReparseOnHeaderChange) { + MockFSProvider FS; + ErrorCheckingDiagConsumer DiagConsumer; + MockCompilationDatabase CDB; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + const auto SourceContents = R"cpp( +#include "foo.h" +int b = a; +)cpp"; + + auto FooCpp = testPath("foo.cpp"); + auto FooH = testPath("foo.h"); + + FS.Files[FooH] = "int a;"; + FS.Files[FooCpp] = SourceContents; + + Server.addDocument(FooCpp, SourceContents); + auto DumpParse1 = dumpASTWithoutMemoryLocs(Server, FooCpp); + ASSERT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for diagnostics"; + EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags()); + + FS.Files[FooH] = ""; + Server.addDocument(FooCpp, SourceContents); + auto DumpParseDifferent = dumpASTWithoutMemoryLocs(Server, FooCpp); + ASSERT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for diagnostics"; + EXPECT_TRUE(DiagConsumer.hadErrorInLastDiags()); + + FS.Files[FooH] = "int a;"; + Server.addDocument(FooCpp, SourceContents); + auto DumpParse2 = dumpASTWithoutMemoryLocs(Server, FooCpp); + ASSERT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for diagnostics"; + EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags()); + + EXPECT_EQ(DumpParse1, DumpParse2); + EXPECT_NE(DumpParse1, DumpParseDifferent); +} + +TEST_F(ClangdVFSTest, PropagatesContexts) { + static Key<int> Secret; + struct FSProvider : public FileSystemProvider { + IntrusiveRefCntPtr<llvm::vfs::FileSystem> getFileSystem() const override { + Got = Context::current().getExisting(Secret); + return buildTestFS({}); + } + mutable int Got; + } FS; + struct DiagConsumer : public DiagnosticsConsumer { + void onDiagnosticsReady(PathRef File, + std::vector<Diag> Diagnostics) override { + Got = Context::current().getExisting(Secret); + } + int Got; + } DiagConsumer; + MockCompilationDatabase CDB; + + // Verify that the context is plumbed to the FS provider and diagnostics. + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + { + WithContextValue Entrypoint(Secret, 42); + Server.addDocument(testPath("foo.cpp"), "void main(){}"); + } + ASSERT_TRUE(Server.blockUntilIdleForTest()); + EXPECT_EQ(FS.Got, 42); + EXPECT_EQ(DiagConsumer.Got, 42); +} + +// Only enable this test on Unix +#ifdef LLVM_ON_UNIX +TEST_F(ClangdVFSTest, SearchLibDir) { + // Checks that searches for GCC installation is done through vfs. + MockFSProvider FS; + ErrorCheckingDiagConsumer DiagConsumer; + MockCompilationDatabase CDB; + CDB.ExtraClangFlags.insert(CDB.ExtraClangFlags.end(), + {"-xc++", "-target", "x86_64-linux-unknown", + "-m64", "--gcc-toolchain=/randomusr", + "-stdlib=libstdc++"}); + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + // Just a random gcc version string + SmallString<8> Version("4.9.3"); + + // A lib dir for gcc installation + SmallString<64> LibDir("/randomusr/lib/gcc/x86_64-linux-gnu"); + llvm::sys::path::append(LibDir, Version); + + // Put crtbegin.o into LibDir/64 to trick clang into thinking there's a gcc + // installation there. + SmallString<64> DummyLibFile; + llvm::sys::path::append(DummyLibFile, LibDir, "64", "crtbegin.o"); + FS.Files[DummyLibFile] = ""; + + SmallString<64> IncludeDir("/randomusr/include/c++"); + llvm::sys::path::append(IncludeDir, Version); + + SmallString<64> StringPath; + llvm::sys::path::append(StringPath, IncludeDir, "string"); + FS.Files[StringPath] = "class mock_string {};"; + + auto FooCpp = testPath("foo.cpp"); + const auto SourceContents = R"cpp( +#include <string> +mock_string x; +)cpp"; + FS.Files[FooCpp] = SourceContents; + + runAddDocument(Server, FooCpp, SourceContents); + EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags()); + + const auto SourceContentsWithError = R"cpp( +#include <string> +std::string x; +)cpp"; + runAddDocument(Server, FooCpp, SourceContentsWithError); + EXPECT_TRUE(DiagConsumer.hadErrorInLastDiags()); +} +#endif // LLVM_ON_UNIX + +TEST_F(ClangdVFSTest, ForceReparseCompileCommand) { + MockFSProvider FS; + ErrorCheckingDiagConsumer DiagConsumer; + MockCompilationDatabase CDB; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + auto FooCpp = testPath("foo.cpp"); + const auto SourceContents1 = R"cpp( +template <class T> +struct foo { T x; }; +)cpp"; + const auto SourceContents2 = R"cpp( +template <class T> +struct bar { T x; }; +)cpp"; + + FS.Files[FooCpp] = ""; + + // First parse files in C mode and check they produce errors. + CDB.ExtraClangFlags = {"-xc"}; + runAddDocument(Server, FooCpp, SourceContents1); + EXPECT_TRUE(DiagConsumer.hadErrorInLastDiags()); + runAddDocument(Server, FooCpp, SourceContents2); + EXPECT_TRUE(DiagConsumer.hadErrorInLastDiags()); + + // Now switch to C++ mode. + CDB.ExtraClangFlags = {"-xc++"}; + runAddDocument(Server, FooCpp, SourceContents2, WantDiagnostics::Auto); + EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags()); + // Subsequent addDocument calls should finish without errors too. + runAddDocument(Server, FooCpp, SourceContents1); + EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags()); + runAddDocument(Server, FooCpp, SourceContents2); + EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags()); +} + +TEST_F(ClangdVFSTest, ForceReparseCompileCommandDefines) { + MockFSProvider FS; + ErrorCheckingDiagConsumer DiagConsumer; + MockCompilationDatabase CDB; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + auto FooCpp = testPath("foo.cpp"); + const auto SourceContents = R"cpp( +#ifdef WITH_ERROR +this +#endif + +int main() { return 0; } +)cpp"; + FS.Files[FooCpp] = ""; + + // Parse with define, we expect to see the errors. + CDB.ExtraClangFlags = {"-DWITH_ERROR"}; + runAddDocument(Server, FooCpp, SourceContents); + EXPECT_TRUE(DiagConsumer.hadErrorInLastDiags()); + + // Parse without the define, no errors should be produced. + CDB.ExtraClangFlags = {}; + runAddDocument(Server, FooCpp, SourceContents, WantDiagnostics::Auto); + ASSERT_TRUE(Server.blockUntilIdleForTest()); + EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags()); + // Subsequent addDocument call should finish without errors too. + runAddDocument(Server, FooCpp, SourceContents); + EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags()); +} + +// Test ClangdServer.reparseOpenedFiles. +TEST_F(ClangdVFSTest, ReparseOpenedFiles) { + Annotations FooSource(R"cpp( +#ifdef MACRO +static void $one[[bob]]() {} +#else +static void $two[[bob]]() {} +#endif + +int main () { bo^b (); return 0; } +)cpp"); + + Annotations BarSource(R"cpp( +#ifdef MACRO +this is an error +#endif +)cpp"); + + Annotations BazSource(R"cpp( +int hello; +)cpp"); + + MockFSProvider FS; + MockCompilationDatabase CDB; + MultipleErrorCheckingDiagConsumer DiagConsumer; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + auto FooCpp = testPath("foo.cpp"); + auto BarCpp = testPath("bar.cpp"); + auto BazCpp = testPath("baz.cpp"); + + FS.Files[FooCpp] = ""; + FS.Files[BarCpp] = ""; + FS.Files[BazCpp] = ""; + + CDB.ExtraClangFlags = {"-DMACRO=1"}; + Server.addDocument(FooCpp, FooSource.code()); + Server.addDocument(BarCpp, BarSource.code()); + Server.addDocument(BazCpp, BazSource.code()); + ASSERT_TRUE(Server.blockUntilIdleForTest()); + + EXPECT_THAT(DiagConsumer.filesWithDiags(), + UnorderedElementsAre(Pair(FooCpp, false), Pair(BarCpp, true), + Pair(BazCpp, false))); + + auto Locations = runLocateSymbolAt(Server, FooCpp, FooSource.point()); + EXPECT_TRUE(bool(Locations)); + EXPECT_THAT(*Locations, ElementsAre(DeclAt(FooCpp, FooSource.range("one")))); + + // Undefine MACRO, close baz.cpp. + CDB.ExtraClangFlags.clear(); + DiagConsumer.clear(); + Server.removeDocument(BazCpp); + Server.addDocument(FooCpp, FooSource.code(), WantDiagnostics::Auto); + Server.addDocument(BarCpp, BarSource.code(), WantDiagnostics::Auto); + ASSERT_TRUE(Server.blockUntilIdleForTest()); + + EXPECT_THAT(DiagConsumer.filesWithDiags(), + UnorderedElementsAre(Pair(FooCpp, false), Pair(BarCpp, false))); + + Locations = runLocateSymbolAt(Server, FooCpp, FooSource.point()); + EXPECT_TRUE(bool(Locations)); + EXPECT_THAT(*Locations, ElementsAre(DeclAt(FooCpp, FooSource.range("two")))); +} + +TEST_F(ClangdVFSTest, MemoryUsage) { + MockFSProvider FS; + ErrorCheckingDiagConsumer DiagConsumer; + MockCompilationDatabase CDB; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + Path FooCpp = testPath("foo.cpp"); + const auto SourceContents = R"cpp( +struct Something { + int method(); +}; +)cpp"; + Path BarCpp = testPath("bar.cpp"); + + FS.Files[FooCpp] = ""; + FS.Files[BarCpp] = ""; + + EXPECT_THAT(Server.getUsedBytesPerFile(), IsEmpty()); + + Server.addDocument(FooCpp, SourceContents); + Server.addDocument(BarCpp, SourceContents); + ASSERT_TRUE(Server.blockUntilIdleForTest()); + + EXPECT_THAT(Server.getUsedBytesPerFile(), + UnorderedElementsAre(Pair(FooCpp, Gt(0u)), Pair(BarCpp, Gt(0u)))); + + Server.removeDocument(FooCpp); + ASSERT_TRUE(Server.blockUntilIdleForTest()); + EXPECT_THAT(Server.getUsedBytesPerFile(), ElementsAre(Pair(BarCpp, Gt(0u)))); + + Server.removeDocument(BarCpp); + ASSERT_TRUE(Server.blockUntilIdleForTest()); + EXPECT_THAT(Server.getUsedBytesPerFile(), IsEmpty()); +} + +TEST_F(ClangdVFSTest, InvalidCompileCommand) { + MockFSProvider FS; + ErrorCheckingDiagConsumer DiagConsumer; + MockCompilationDatabase CDB; + + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + auto FooCpp = testPath("foo.cpp"); + // clang cannot create CompilerInvocation if we pass two files in the + // CompileCommand. We pass the file in ExtraFlags once and CDB adds another + // one in getCompileCommand(). + CDB.ExtraClangFlags.push_back(FooCpp); + + // Clang can't parse command args in that case, but we shouldn't crash. + runAddDocument(Server, FooCpp, "int main() {}"); + + EXPECT_EQ(runDumpAST(Server, FooCpp), "<no-ast>"); + EXPECT_ERROR(runLocateSymbolAt(Server, FooCpp, Position())); + EXPECT_ERROR(runFindDocumentHighlights(Server, FooCpp, Position())); + EXPECT_ERROR(runRename(Server, FooCpp, Position(), "new_name")); + // Identifier-based fallback completion. + EXPECT_THAT(cantFail(runCodeComplete(Server, FooCpp, Position(), + clangd::CodeCompleteOptions())) + .Completions, + ElementsAre(Field(&CodeCompletion::Name, "int"), + Field(&CodeCompletion::Name, "main"))); + auto SigHelp = runSignatureHelp(Server, FooCpp, Position()); + ASSERT_TRUE(bool(SigHelp)) << "signatureHelp returned an error"; + EXPECT_THAT(SigHelp->signatures, IsEmpty()); +} + +class ClangdThreadingTest : public ClangdVFSTest {}; + +TEST_F(ClangdThreadingTest, StressTest) { + // Without 'static' clang gives an error for a usage inside TestDiagConsumer. + static const unsigned FilesCount = 5; + const unsigned RequestsCount = 500; + // Blocking requests wait for the parsing to complete, they slow down the test + // dramatically, so they are issued rarely. Each + // BlockingRequestInterval-request will be a blocking one. + const unsigned BlockingRequestInterval = 40; + + const auto SourceContentsWithoutErrors = R"cpp( +int a; +int b; +int c; +int d; +)cpp"; + + const auto SourceContentsWithErrors = R"cpp( +int a = x; +int b; +int c; +int d; +)cpp"; + + // Giving invalid line and column number should not crash ClangdServer, but + // just to make sure we're sometimes hitting the bounds inside the file we + // limit the intervals of line and column number that are generated. + unsigned MaxLineForFileRequests = 7; + unsigned MaxColumnForFileRequests = 10; + + std::vector<std::string> FilePaths; + MockFSProvider FS; + for (unsigned I = 0; I < FilesCount; ++I) { + std::string Name = std::string("Foo") + std::to_string(I) + ".cpp"; + FS.Files[Name] = ""; + FilePaths.push_back(testPath(Name)); + } + + struct FileStat { + unsigned HitsWithoutErrors = 0; + unsigned HitsWithErrors = 0; + bool HadErrorsInLastDiags = false; + }; + + class TestDiagConsumer : public DiagnosticsConsumer { + public: + TestDiagConsumer() : Stats(FilesCount, FileStat()) {} + + void onDiagnosticsReady(PathRef File, + std::vector<Diag> Diagnostics) override { + StringRef FileIndexStr = llvm::sys::path::stem(File); + ASSERT_TRUE(FileIndexStr.consume_front("Foo")); + + unsigned long FileIndex = std::stoul(FileIndexStr.str()); + + bool HadError = diagsContainErrors(Diagnostics); + + std::lock_guard<std::mutex> Lock(Mutex); + if (HadError) + Stats[FileIndex].HitsWithErrors++; + else + Stats[FileIndex].HitsWithoutErrors++; + Stats[FileIndex].HadErrorsInLastDiags = HadError; + } + + std::vector<FileStat> takeFileStats() { + std::lock_guard<std::mutex> Lock(Mutex); + return std::move(Stats); + } + + private: + std::mutex Mutex; + std::vector<FileStat> Stats; + }; + + struct RequestStats { + unsigned RequestsWithoutErrors = 0; + unsigned RequestsWithErrors = 0; + bool LastContentsHadErrors = false; + bool FileIsRemoved = true; + }; + + std::vector<RequestStats> ReqStats; + ReqStats.reserve(FilesCount); + for (unsigned FileIndex = 0; FileIndex < FilesCount; ++FileIndex) + ReqStats.emplace_back(); + + TestDiagConsumer DiagConsumer; + { + MockCompilationDatabase CDB; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + // Prepare some random distributions for the test. + std::random_device RandGen; + + std::uniform_int_distribution<unsigned> FileIndexDist(0, FilesCount - 1); + // Pass a text that contains compiler errors to addDocument in about 20% of + // all requests. + std::bernoulli_distribution ShouldHaveErrorsDist(0.2); + // Line and Column numbers for requests that need them. + std::uniform_int_distribution<int> LineDist(0, MaxLineForFileRequests); + std::uniform_int_distribution<int> ColumnDist(0, MaxColumnForFileRequests); + + // Some helpers. + auto UpdateStatsOnAddDocument = [&](unsigned FileIndex, bool HadErrors) { + auto &Stats = ReqStats[FileIndex]; + + if (HadErrors) + ++Stats.RequestsWithErrors; + else + ++Stats.RequestsWithoutErrors; + Stats.LastContentsHadErrors = HadErrors; + Stats.FileIsRemoved = false; + }; + + auto UpdateStatsOnRemoveDocument = [&](unsigned FileIndex) { + auto &Stats = ReqStats[FileIndex]; + + Stats.FileIsRemoved = true; + }; + + auto AddDocument = [&](unsigned FileIndex, bool SkipCache) { + bool ShouldHaveErrors = ShouldHaveErrorsDist(RandGen); + Server.addDocument(FilePaths[FileIndex], + ShouldHaveErrors ? SourceContentsWithErrors + : SourceContentsWithoutErrors, + WantDiagnostics::Auto); + UpdateStatsOnAddDocument(FileIndex, ShouldHaveErrors); + }; + + // Various requests that we would randomly run. + auto AddDocumentRequest = [&]() { + unsigned FileIndex = FileIndexDist(RandGen); + AddDocument(FileIndex, /*SkipCache=*/false); + }; + + auto ForceReparseRequest = [&]() { + unsigned FileIndex = FileIndexDist(RandGen); + AddDocument(FileIndex, /*SkipCache=*/true); + }; + + auto RemoveDocumentRequest = [&]() { + unsigned FileIndex = FileIndexDist(RandGen); + // Make sure we don't violate the ClangdServer's contract. + if (ReqStats[FileIndex].FileIsRemoved) + AddDocument(FileIndex, /*SkipCache=*/false); + + Server.removeDocument(FilePaths[FileIndex]); + UpdateStatsOnRemoveDocument(FileIndex); + }; + + auto CodeCompletionRequest = [&]() { + unsigned FileIndex = FileIndexDist(RandGen); + // Make sure we don't violate the ClangdServer's contract. + if (ReqStats[FileIndex].FileIsRemoved) + AddDocument(FileIndex, /*SkipCache=*/false); + + Position Pos; + Pos.line = LineDist(RandGen); + Pos.character = ColumnDist(RandGen); + // FIXME(ibiryukov): Also test async completion requests. + // Simply putting CodeCompletion into async requests now would make + // tests slow, since there's no way to cancel previous completion + // requests as opposed to AddDocument/RemoveDocument, which are implicitly + // cancelled by any subsequent AddDocument/RemoveDocument request to the + // same file. + cantFail(runCodeComplete(Server, FilePaths[FileIndex], Pos, + clangd::CodeCompleteOptions())); + }; + + auto LocateSymbolRequest = [&]() { + unsigned FileIndex = FileIndexDist(RandGen); + // Make sure we don't violate the ClangdServer's contract. + if (ReqStats[FileIndex].FileIsRemoved) + AddDocument(FileIndex, /*SkipCache=*/false); + + Position Pos; + Pos.line = LineDist(RandGen); + Pos.character = ColumnDist(RandGen); + + ASSERT_TRUE(!!runLocateSymbolAt(Server, FilePaths[FileIndex], Pos)); + }; + + std::vector<std::function<void()>> AsyncRequests = { + AddDocumentRequest, ForceReparseRequest, RemoveDocumentRequest}; + std::vector<std::function<void()>> BlockingRequests = { + CodeCompletionRequest, LocateSymbolRequest}; + + // Bash requests to ClangdServer in a loop. + std::uniform_int_distribution<int> AsyncRequestIndexDist( + 0, AsyncRequests.size() - 1); + std::uniform_int_distribution<int> BlockingRequestIndexDist( + 0, BlockingRequests.size() - 1); + for (unsigned I = 1; I <= RequestsCount; ++I) { + if (I % BlockingRequestInterval != 0) { + // Issue an async request most of the time. It should be fast. + unsigned RequestIndex = AsyncRequestIndexDist(RandGen); + AsyncRequests[RequestIndex](); + } else { + // Issue a blocking request once in a while. + auto RequestIndex = BlockingRequestIndexDist(RandGen); + BlockingRequests[RequestIndex](); + } + } + ASSERT_TRUE(Server.blockUntilIdleForTest()); + } + + // Check some invariants about the state of the program. + std::vector<FileStat> Stats = DiagConsumer.takeFileStats(); + for (unsigned I = 0; I < FilesCount; ++I) { + if (!ReqStats[I].FileIsRemoved) { + ASSERT_EQ(Stats[I].HadErrorsInLastDiags, + ReqStats[I].LastContentsHadErrors); + } + + ASSERT_LE(Stats[I].HitsWithErrors, ReqStats[I].RequestsWithErrors); + ASSERT_LE(Stats[I].HitsWithoutErrors, ReqStats[I].RequestsWithoutErrors); + } +} + +TEST_F(ClangdVFSTest, CheckSourceHeaderSwitch) { + MockFSProvider FS; + ErrorCheckingDiagConsumer DiagConsumer; + MockCompilationDatabase CDB; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + auto SourceContents = R"cpp( + #include "foo.h" + int b = a; + )cpp"; + + auto FooCpp = testPath("foo.cpp"); + auto FooH = testPath("foo.h"); + auto Invalid = testPath("main.cpp"); + + FS.Files[FooCpp] = SourceContents; + FS.Files[FooH] = "int a;"; + FS.Files[Invalid] = "int main() { \n return 0; \n }"; + + Optional<Path> PathResult = Server.switchSourceHeader(FooCpp); + EXPECT_TRUE(PathResult.hasValue()); + ASSERT_EQ(PathResult.getValue(), FooH); + + PathResult = Server.switchSourceHeader(FooH); + EXPECT_TRUE(PathResult.hasValue()); + ASSERT_EQ(PathResult.getValue(), FooCpp); + + SourceContents = R"c( + #include "foo.HH" + int b = a; + )c"; + + // Test with header file in capital letters and different extension, source + // file with different extension + auto FooC = testPath("bar.c"); + auto FooHH = testPath("bar.HH"); + + FS.Files[FooC] = SourceContents; + FS.Files[FooHH] = "int a;"; + + PathResult = Server.switchSourceHeader(FooC); + EXPECT_TRUE(PathResult.hasValue()); + ASSERT_EQ(PathResult.getValue(), FooHH); + + // Test with both capital letters + auto Foo2C = testPath("foo2.C"); + auto Foo2HH = testPath("foo2.HH"); + FS.Files[Foo2C] = SourceContents; + FS.Files[Foo2HH] = "int a;"; + + PathResult = Server.switchSourceHeader(Foo2C); + EXPECT_TRUE(PathResult.hasValue()); + ASSERT_EQ(PathResult.getValue(), Foo2HH); + + // Test with source file as capital letter and .hxx header file + auto Foo3C = testPath("foo3.C"); + auto Foo3HXX = testPath("foo3.hxx"); + + SourceContents = R"c( + #include "foo3.hxx" + int b = a; + )c"; + + FS.Files[Foo3C] = SourceContents; + FS.Files[Foo3HXX] = "int a;"; + + PathResult = Server.switchSourceHeader(Foo3C); + EXPECT_TRUE(PathResult.hasValue()); + ASSERT_EQ(PathResult.getValue(), Foo3HXX); + + // Test if asking for a corresponding file that doesn't exist returns an empty + // string. + PathResult = Server.switchSourceHeader(Invalid); + EXPECT_FALSE(PathResult.hasValue()); +} + +TEST_F(ClangdThreadingTest, NoConcurrentDiagnostics) { + class NoConcurrentAccessDiagConsumer : public DiagnosticsConsumer { + public: + std::atomic<int> Count = {0}; + + NoConcurrentAccessDiagConsumer(std::promise<void> StartSecondReparse) + : StartSecondReparse(std::move(StartSecondReparse)) {} + + void onDiagnosticsReady(PathRef, std::vector<Diag>) override { + ++Count; + std::unique_lock<std::mutex> Lock(Mutex, std::try_to_lock_t()); + ASSERT_TRUE(Lock.owns_lock()) + << "Detected concurrent onDiagnosticsReady calls for the same file."; + + // If we started the second parse immediately, it might cancel the first. + // So we don't allow it to start until the first has delivered diags... + if (FirstRequest) { + FirstRequest = false; + StartSecondReparse.set_value(); + // ... but then we wait long enough that the callbacks would overlap. + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + } + + private: + std::mutex Mutex; + bool FirstRequest = true; + std::promise<void> StartSecondReparse; + }; + + const auto SourceContentsWithoutErrors = R"cpp( +int a; +int b; +int c; +int d; +)cpp"; + + const auto SourceContentsWithErrors = R"cpp( +int a = x; +int b; +int c; +int d; +)cpp"; + + auto FooCpp = testPath("foo.cpp"); + MockFSProvider FS; + FS.Files[FooCpp] = ""; + + std::promise<void> StartSecondPromise; + std::future<void> StartSecond = StartSecondPromise.get_future(); + + NoConcurrentAccessDiagConsumer DiagConsumer(std::move(StartSecondPromise)); + MockCompilationDatabase CDB; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + Server.addDocument(FooCpp, SourceContentsWithErrors); + StartSecond.wait(); + Server.addDocument(FooCpp, SourceContentsWithoutErrors); + ASSERT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for diagnostics"; + ASSERT_EQ(DiagConsumer.Count, 2); // Sanity check - we actually ran both? +} + +TEST_F(ClangdVFSTest, FormatCode) { + MockFSProvider FS; + ErrorCheckingDiagConsumer DiagConsumer; + MockCompilationDatabase CDB; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + auto Path = testPath("foo.cpp"); + std::string Code = R"cpp( +#include "x.h" +#include "y.h" + +void f( ) {} +)cpp"; + std::string Expected = R"cpp( +#include "x.h" +#include "y.h" + +void f() {} +)cpp"; + FS.Files[Path] = Code; + runAddDocument(Server, Path, Code); + + auto Replaces = Server.formatFile(Code, Path); + EXPECT_TRUE(static_cast<bool>(Replaces)); + auto Changed = tooling::applyAllReplacements(Code, *Replaces); + EXPECT_TRUE(static_cast<bool>(Changed)); + EXPECT_EQ(Expected, *Changed); +} + +TEST_F(ClangdVFSTest, ChangedHeaderFromISystem) { + MockFSProvider FS; + ErrorCheckingDiagConsumer DiagConsumer; + MockCompilationDatabase CDB; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + auto SourcePath = testPath("source/foo.cpp"); + auto HeaderPath = testPath("headers/foo.h"); + FS.Files[HeaderPath] = "struct X { int bar; };"; + Annotations Code(R"cpp( + #include "foo.h" + + int main() { + X().ba^ + })cpp"); + CDB.ExtraClangFlags.push_back("-xc++"); + CDB.ExtraClangFlags.push_back("-isystem" + testPath("headers")); + + runAddDocument(Server, SourcePath, Code.code()); + auto Completions = cantFail(runCodeComplete(Server, SourcePath, Code.point(), + clangd::CodeCompleteOptions())) + .Completions; + EXPECT_THAT(Completions, ElementsAre(Field(&CodeCompletion::Name, "bar"))); + // Update the header and rerun addDocument to make sure we get the updated + // files. + FS.Files[HeaderPath] = "struct X { int bar; int baz; };"; + runAddDocument(Server, SourcePath, Code.code()); + Completions = cantFail(runCodeComplete(Server, SourcePath, Code.point(), + clangd::CodeCompleteOptions())) + .Completions; + // We want to make sure we see the updated version. + EXPECT_THAT(Completions, ElementsAre(Field(&CodeCompletion::Name, "bar"), + Field(&CodeCompletion::Name, "baz"))); +} + +// FIXME(ioeric): make this work for windows again. +#ifndef _WIN32 +// Check that running code completion doesn't stat() a bunch of files from the +// preamble again. (They should be using the preamble's stat-cache) +TEST(ClangdTests, PreambleVFSStatCache) { + class ListenStatsFSProvider : public FileSystemProvider { + public: + ListenStatsFSProvider(llvm::StringMap<unsigned> &CountStats) + : CountStats(CountStats) {} + + IntrusiveRefCntPtr<llvm::vfs::FileSystem> getFileSystem() const override { + class ListenStatVFS : public llvm::vfs::ProxyFileSystem { + public: + ListenStatVFS(IntrusiveRefCntPtr<llvm::vfs::FileSystem> FS, + llvm::StringMap<unsigned> &CountStats) + : ProxyFileSystem(std::move(FS)), CountStats(CountStats) {} + + llvm::ErrorOr<std::unique_ptr<llvm::vfs::File>> + openFileForRead(const Twine &Path) override { + ++CountStats[llvm::sys::path::filename(Path.str())]; + return ProxyFileSystem::openFileForRead(Path); + } + llvm::ErrorOr<llvm::vfs::Status> status(const Twine &Path) override { + ++CountStats[llvm::sys::path::filename(Path.str())]; + return ProxyFileSystem::status(Path); + } + + private: + llvm::StringMap<unsigned> &CountStats; + }; + + return IntrusiveRefCntPtr<ListenStatVFS>( + new ListenStatVFS(buildTestFS(Files), CountStats)); + } + + // If relative paths are used, they are resolved with testPath(). + llvm::StringMap<std::string> Files; + llvm::StringMap<unsigned> &CountStats; + }; + + llvm::StringMap<unsigned> CountStats; + ListenStatsFSProvider FS(CountStats); + ErrorCheckingDiagConsumer DiagConsumer; + MockCompilationDatabase CDB; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + auto SourcePath = testPath("foo.cpp"); + auto HeaderPath = testPath("foo.h"); + FS.Files[HeaderPath] = "struct TestSym {};"; + Annotations Code(R"cpp( + #include "foo.h" + + int main() { + TestSy^ + })cpp"); + + runAddDocument(Server, SourcePath, Code.code()); + + unsigned Before = CountStats["foo.h"]; + EXPECT_GT(Before, 0u); + auto Completions = cantFail(runCodeComplete(Server, SourcePath, Code.point(), + clangd::CodeCompleteOptions())) + .Completions; + EXPECT_EQ(CountStats["foo.h"], Before); + EXPECT_THAT(Completions, + ElementsAre(Field(&CodeCompletion::Name, "TestSym"))); +} +#endif + +TEST_F(ClangdVFSTest, FlagsWithPlugins) { + MockFSProvider FS; + ErrorCheckingDiagConsumer DiagConsumer; + MockCompilationDatabase CDB; + CDB.ExtraClangFlags = { + "-Xclang", + "-add-plugin", + "-Xclang", + "random-plugin", + }; + OverlayCDB OCDB(&CDB); + ClangdServer Server(OCDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + auto FooCpp = testPath("foo.cpp"); + const auto SourceContents = "int main() { return 0; }"; + FS.Files[FooCpp] = FooCpp; + Server.addDocument(FooCpp, SourceContents); + auto Result = dumpASTWithoutMemoryLocs(Server, FooCpp); + EXPECT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for diagnostics"; + EXPECT_NE(Result, "<no-ast>"); +} + +TEST_F(ClangdVFSTest, FallbackWhenPreambleIsNotReady) { + MockFSProvider FS; + ErrorCheckingDiagConsumer DiagConsumer; + MockCompilationDatabase CDB; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + auto FooCpp = testPath("foo.cpp"); + Annotations Code(R"cpp( + namespace ns { int xyz; } + using namespace ns; + int main() { + xy^ + })cpp"); + FS.Files[FooCpp] = FooCpp; + + auto Opts = clangd::CodeCompleteOptions(); + Opts.AllowFallback = true; + + // This will make compile command broken and preamble absent. + CDB.ExtraClangFlags = {"yolo.cc"}; + Server.addDocument(FooCpp, Code.code()); + ASSERT_TRUE(Server.blockUntilIdleForTest()); + auto Res = cantFail(runCodeComplete(Server, FooCpp, Code.point(), Opts)); + EXPECT_EQ(Res.Context, CodeCompletionContext::CCC_Recovery); + // Identifier-based fallback completion doesn't know about "symbol" scope. + EXPECT_THAT(Res.Completions, + ElementsAre(AllOf(Field(&CodeCompletion::Name, "xyz"), + Field(&CodeCompletion::Scope, "")))); + + // Make the compile command work again. + CDB.ExtraClangFlags = {"-std=c++11"}; + Server.addDocument(FooCpp, Code.code()); + ASSERT_TRUE(Server.blockUntilIdleForTest()); + EXPECT_THAT(cantFail(runCodeComplete(Server, FooCpp, Code.point(), + clangd::CodeCompleteOptions())) + .Completions, + ElementsAre(AllOf(Field(&CodeCompletion::Name, "xyz"), + Field(&CodeCompletion::Scope, "ns::")))); +} + +TEST_F(ClangdVFSTest, FallbackWhenWaitingForCompileCommand) { + MockFSProvider FS; + ErrorCheckingDiagConsumer DiagConsumer; + // Returns compile command only when notified. + class DelayedCompilationDatabase : public GlobalCompilationDatabase { + public: + DelayedCompilationDatabase(Notification &CanReturnCommand) + : CanReturnCommand(CanReturnCommand) {} + + llvm::Optional<tooling::CompileCommand> + getCompileCommand(PathRef File, ProjectInfo * = nullptr) const override { + // FIXME: make this timeout and fail instead of waiting forever in case + // something goes wrong. + CanReturnCommand.wait(); + auto FileName = llvm::sys::path::filename(File); + std::vector<std::string> CommandLine = {"clangd", "-ffreestanding", File}; + return {tooling::CompileCommand(llvm::sys::path::parent_path(File), + FileName, std::move(CommandLine), "")}; + } + + std::vector<std::string> ExtraClangFlags; + + private: + Notification &CanReturnCommand; + }; + + Notification CanReturnCommand; + DelayedCompilationDatabase CDB(CanReturnCommand); + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + auto FooCpp = testPath("foo.cpp"); + Annotations Code(R"cpp( + namespace ns { int xyz; } + using namespace ns; + int main() { + xy^ + })cpp"); + FS.Files[FooCpp] = FooCpp; + Server.addDocument(FooCpp, Code.code()); + + // Sleep for some time to make sure code completion is not run because update + // hasn't been scheduled. + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + auto Opts = clangd::CodeCompleteOptions(); + Opts.AllowFallback = true; + + auto Res = cantFail(runCodeComplete(Server, FooCpp, Code.point(), Opts)); + EXPECT_EQ(Res.Context, CodeCompletionContext::CCC_Recovery); + + CanReturnCommand.notify(); + ASSERT_TRUE(Server.blockUntilIdleForTest()); + EXPECT_THAT(cantFail(runCodeComplete(Server, FooCpp, Code.point(), + clangd::CodeCompleteOptions())) + .Completions, + ElementsAre(AllOf(Field(&CodeCompletion::Name, "xyz"), + Field(&CodeCompletion::Scope, "ns::")))); +} + +} // namespace +} // namespace clangd +} // namespace clang |