//====-- unittests/Frontend/PCHPreambleTest.cpp - FrontendAction tests ---====// // // 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 "clang/Frontend/ASTUnit.h" #include "clang/Frontend/CompilerInvocation.h" #include "clang/Frontend/CompilerInstance.h" #include "clang/Frontend/FrontendActions.h" #include "clang/Frontend/FrontendOptions.h" #include "clang/Lex/PreprocessorOptions.h" #include "clang/Basic/Diagnostic.h" #include "clang/Basic/FileManager.h" #include "llvm/Support/FileSystem.h" #include "llvm/Support/MemoryBuffer.h" #include "llvm/Support/Path.h" #include "gtest/gtest.h" using namespace llvm; using namespace clang; namespace { class ReadCountingInMemoryFileSystem : public vfs::InMemoryFileSystem { std::map ReadCounts; public: ErrorOr> openFileForRead(const Twine &Path) override { SmallVector PathVec; Path.toVector(PathVec); llvm::sys::path::remove_dots(PathVec, true); ++ReadCounts[std::string(PathVec.begin(), PathVec.end())]; return InMemoryFileSystem::openFileForRead(Path); } unsigned GetReadCount(const Twine &Path) const { auto it = ReadCounts.find(Path.str()); return it == ReadCounts.end() ? 0 : it->second; } }; class PCHPreambleTest : public ::testing::Test { IntrusiveRefCntPtr VFS; StringMap RemappedFiles; std::shared_ptr PCHContainerOpts; FileSystemOptions FSOpts; public: void SetUp() override { ResetVFS(); } void TearDown() override {} void ResetVFS() { VFS = new ReadCountingInMemoryFileSystem(); // We need the working directory to be set to something absolute, // otherwise it ends up being inadvertently set to the current // working directory in the real file system due to a series of // unfortunate conditions interacting badly. // What's more, this path *must* be absolute on all (real) // filesystems, so just '/' won't work (e.g. on Win32). VFS->setCurrentWorkingDirectory("//./"); } void AddFile(const std::string &Filename, const std::string &Contents) { ::time_t now; ::time(&now); VFS->addFile(Filename, now, MemoryBuffer::getMemBufferCopy(Contents, Filename)); } void RemapFile(const std::string &Filename, const std::string &Contents) { RemappedFiles[Filename] = Contents; } std::unique_ptr ParseAST(const std::string &EntryFile) { PCHContainerOpts = std::make_shared(); std::shared_ptr CI(new CompilerInvocation); CI->getFrontendOpts().Inputs.push_back( FrontendInputFile(EntryFile, FrontendOptions::getInputKindForExtension( llvm::sys::path::extension(EntryFile).substr(1)))); CI->getTargetOpts().Triple = "i386-unknown-linux-gnu"; CI->getPreprocessorOpts().RemappedFileBuffers = GetRemappedFiles(); PreprocessorOptions &PPOpts = CI->getPreprocessorOpts(); PPOpts.RemappedFilesKeepOriginalName = true; IntrusiveRefCntPtr Diags(CompilerInstance::createDiagnostics(new DiagnosticOptions, new DiagnosticConsumer)); FileManager *FileMgr = new FileManager(FSOpts, VFS); std::unique_ptr AST = ASTUnit::LoadFromCompilerInvocation( CI, PCHContainerOpts, Diags, FileMgr, false, CaptureDiagsKind::None, /*PrecompilePreambleAfterNParses=*/1); return AST; } bool ReparseAST(const std::unique_ptr &AST) { bool reparseFailed = AST->Reparse(PCHContainerOpts, GetRemappedFiles(), VFS); return !reparseFailed; } unsigned GetFileReadCount(const std::string &Filename) const { return VFS->GetReadCount(Filename); } private: std::vector> GetRemappedFiles() const { std::vector> Remapped; for (const auto &RemappedFile : RemappedFiles) { std::unique_ptr buf = MemoryBuffer::getMemBufferCopy( RemappedFile.second, RemappedFile.first()); Remapped.emplace_back(std::string(RemappedFile.first()), buf.release()); } return Remapped; } }; TEST_F(PCHPreambleTest, ReparseReusesPreambleWithUnsavedFileNotExistingOnDisk) { std::string Header1 = "//./header1.h"; std::string MainName = "//./main.cpp"; AddFile(MainName, R"cpp( #include "//./header1.h" int main() { return ZERO; } )cpp"); RemapFile(Header1, "#define ZERO 0\n"); // Parse with header file provided as unsaved file, which does not exist on // disk. std::unique_ptr AST(ParseAST(MainName)); ASSERT_TRUE(AST.get()); ASSERT_FALSE(AST->getDiagnostics().hasErrorOccurred()); // Reparse and check that the preamble was reused. ASSERT_TRUE(ReparseAST(AST)); ASSERT_EQ(AST->getPreambleCounterForTests(), 1U); } TEST_F(PCHPreambleTest, ReparseReusesPreambleAfterUnsavedFileWasCreatedOnDisk) { std::string Header1 = "//./header1.h"; std::string MainName = "//./main.cpp"; AddFile(MainName, R"cpp( #include "//./header1.h" int main() { return ZERO; } )cpp"); RemapFile(Header1, "#define ZERO 0\n"); // Parse with header file provided as unsaved file, which does not exist on // disk. std::unique_ptr AST(ParseAST(MainName)); ASSERT_TRUE(AST.get()); ASSERT_FALSE(AST->getDiagnostics().hasErrorOccurred()); // Create the unsaved file also on disk and check that preamble was reused. AddFile(Header1, "#define ZERO 0\n"); ASSERT_TRUE(ReparseAST(AST)); ASSERT_EQ(AST->getPreambleCounterForTests(), 1U); } TEST_F(PCHPreambleTest, ReparseReusesPreambleAfterUnsavedFileWasRemovedFromDisk) { std::string Header1 = "//./foo/header1.h"; std::string MainName = "//./main.cpp"; std::string MainFileContent = R"cpp( #include "//./foo/header1.h" int main() { return ZERO; } )cpp"; AddFile(MainName, MainFileContent); AddFile(Header1, "#define ZERO 0\n"); RemapFile(Header1, "#define ZERO 0\n"); // Parse with header file provided as unsaved file, which exists on disk. std::unique_ptr AST(ParseAST(MainName)); ASSERT_TRUE(AST.get()); ASSERT_FALSE(AST->getDiagnostics().hasErrorOccurred()); ASSERT_EQ(AST->getPreambleCounterForTests(), 1U); // Remove the unsaved file from disk and check that the preamble was reused. ResetVFS(); AddFile(MainName, MainFileContent); ASSERT_TRUE(ReparseAST(AST)); ASSERT_EQ(AST->getPreambleCounterForTests(), 1U); } TEST_F(PCHPreambleTest, ReparseWithOverriddenFileDoesNotInvalidatePreamble) { std::string Header1 = "//./header1.h"; std::string Header2 = "//./header2.h"; std::string MainName = "//./main.cpp"; AddFile(Header1, ""); AddFile(Header2, "#pragma once"); AddFile(MainName, "#include \"//./foo/../header1.h\"\n" "#include \"//./foo/../header2.h\"\n" "int main() { return ZERO; }"); RemapFile(Header1, "static const int ZERO = 0;\n"); std::unique_ptr AST(ParseAST(MainName)); ASSERT_TRUE(AST.get()); ASSERT_FALSE(AST->getDiagnostics().hasErrorOccurred()); unsigned initialCounts[] = { GetFileReadCount(MainName), GetFileReadCount(Header1), GetFileReadCount(Header2) }; ASSERT_TRUE(ReparseAST(AST)); ASSERT_NE(initialCounts[0], GetFileReadCount(MainName)); ASSERT_EQ(initialCounts[1], GetFileReadCount(Header1)); ASSERT_EQ(initialCounts[2], GetFileReadCount(Header2)); } TEST_F(PCHPreambleTest, ParseWithBom) { std::string Header = "//./header.h"; std::string Main = "//./main.cpp"; AddFile(Header, "int random() { return 4; }"); AddFile(Main, "\xef\xbb\xbf" "#include \"//./header.h\"\n" "int main() { return random() -2; }"); std::unique_ptr AST(ParseAST(Main)); ASSERT_TRUE(AST.get()); ASSERT_FALSE(AST->getDiagnostics().hasErrorOccurred()); unsigned HeaderReadCount = GetFileReadCount(Header); ASSERT_TRUE(ReparseAST(AST)); ASSERT_FALSE(AST->getDiagnostics().hasErrorOccurred()); // Check preamble PCH was really reused ASSERT_EQ(HeaderReadCount, GetFileReadCount(Header)); // Remove BOM RemapFile(Main, "#include \"//./header.h\"\n" "int main() { return random() -2; }"); ASSERT_TRUE(ReparseAST(AST)); ASSERT_FALSE(AST->getDiagnostics().hasErrorOccurred()); ASSERT_LE(HeaderReadCount, GetFileReadCount(Header)); HeaderReadCount = GetFileReadCount(Header); // Add BOM back RemapFile(Main, "\xef\xbb\xbf" "#include \"//./header.h\"\n" "int main() { return random() -2; }"); ASSERT_TRUE(ReparseAST(AST)); ASSERT_FALSE(AST->getDiagnostics().hasErrorOccurred()); ASSERT_LE(HeaderReadCount, GetFileReadCount(Header)); } } // anonymous namespace