| //====-- 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<std::string, unsigned> ReadCounts; |
| |
| public: |
| ErrorOr<std::unique_ptr<vfs::File>> openFileForRead(const Twine &Path) override |
| { |
| SmallVector<char, 128> 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<ReadCountingInMemoryFileSystem> VFS; |
| StringMap<std::string> RemappedFiles; |
| std::shared_ptr<PCHContainerOperations> 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<ASTUnit> ParseAST(const std::string &EntryFile) { |
| PCHContainerOpts = std::make_shared<PCHContainerOperations>(); |
| std::shared_ptr<CompilerInvocation> 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<DiagnosticsEngine> |
| Diags(CompilerInstance::createDiagnostics(new DiagnosticOptions, new DiagnosticConsumer)); |
| |
| FileManager *FileMgr = new FileManager(FSOpts, VFS); |
| |
| std::unique_ptr<ASTUnit> AST = ASTUnit::LoadFromCompilerInvocation( |
| CI, PCHContainerOpts, Diags, FileMgr, false, CaptureDiagsKind::None, |
| /*PrecompilePreambleAfterNParses=*/1); |
| return AST; |
| } |
| |
| bool ReparseAST(const std::unique_ptr<ASTUnit> &AST) { |
| bool reparseFailed = AST->Reparse(PCHContainerOpts, GetRemappedFiles(), VFS); |
| return !reparseFailed; |
| } |
| |
| unsigned GetFileReadCount(const std::string &Filename) const { |
| return VFS->GetReadCount(Filename); |
| } |
| |
| private: |
| std::vector<std::pair<std::string, llvm::MemoryBuffer *>> |
| GetRemappedFiles() const { |
| std::vector<std::pair<std::string, llvm::MemoryBuffer *>> Remapped; |
| for (const auto &RemappedFile : RemappedFiles) { |
| std::unique_ptr<MemoryBuffer> 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<ASTUnit> 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<ASTUnit> 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<ASTUnit> 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<ASTUnit> 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<ASTUnit> 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 |