//====-- 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
