1//====-- unittests/Frontend/PCHPreambleTest.cpp - FrontendAction tests ---====//
2//
3// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4// See https://llvm.org/LICENSE.txt for license information.
5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6//
7//===----------------------------------------------------------------------===//
8
9#include "clang/Frontend/ASTUnit.h"
10#include "clang/Frontend/CompilerInvocation.h"
11#include "clang/Frontend/CompilerInstance.h"
12#include "clang/Frontend/FrontendActions.h"
13#include "clang/Frontend/FrontendOptions.h"
14#include "clang/Lex/PreprocessorOptions.h"
15#include "clang/Basic/Diagnostic.h"
16#include "clang/Basic/FileManager.h"
17#include "llvm/Support/FileSystem.h"
18#include "llvm/Support/MemoryBuffer.h"
19#include "llvm/Support/Path.h"
20#include "gtest/gtest.h"
21
22using namespace llvm;
23using namespace clang;
24
25namespace {
26
27std::string Canonicalize(const Twine &Path) {
28 SmallVector<char, 128> PathVec;
29 Path.toVector(Out&: PathVec);
30 llvm::sys::path::remove_dots(path&: PathVec, remove_dot_dot: true);
31 return std::string(PathVec.begin(), PathVec.end());
32}
33
34class ReadCountingInMemoryFileSystem : public vfs::InMemoryFileSystem
35{
36 std::map<std::string, unsigned> ReadCounts;
37
38public:
39 ErrorOr<std::unique_ptr<vfs::File>> openFileForRead(const Twine &Path) override
40 {
41 ++ReadCounts[Canonicalize(Path)];
42 return InMemoryFileSystem::openFileForRead(Path);
43 }
44
45 unsigned GetReadCount(const Twine &Path) const
46 {
47 auto it = ReadCounts.find(x: Canonicalize(Path));
48 return it == ReadCounts.end() ? 0 : it->second;
49 }
50};
51
52class PCHPreambleTest : public ::testing::Test {
53 IntrusiveRefCntPtr<ReadCountingInMemoryFileSystem> VFS;
54 StringMap<std::string> RemappedFiles;
55 std::shared_ptr<PCHContainerOperations> PCHContainerOpts;
56 FileSystemOptions FSOpts;
57
58public:
59 void SetUp() override { ResetVFS(); }
60 void TearDown() override {}
61
62 void ResetVFS() {
63 VFS = new ReadCountingInMemoryFileSystem();
64 // We need the working directory to be set to something absolute,
65 // otherwise it ends up being inadvertently set to the current
66 // working directory in the real file system due to a series of
67 // unfortunate conditions interacting badly.
68 // What's more, this path *must* be absolute on all (real)
69 // filesystems, so just '/' won't work (e.g. on Win32).
70 VFS->setCurrentWorkingDirectory("//./");
71 }
72
73 void AddFile(const std::string &Filename, const std::string &Contents) {
74 ::time_t now;
75 ::time(timer: &now);
76 VFS->addFile(Path: Filename, ModificationTime: now, Buffer: MemoryBuffer::getMemBufferCopy(InputData: Contents, BufferName: Filename));
77 }
78
79 void RemapFile(const std::string &Filename, const std::string &Contents) {
80 RemappedFiles[Filename] = Contents;
81 }
82
83 std::unique_ptr<ASTUnit> ParseAST(const std::string &EntryFile) {
84 PCHContainerOpts = std::make_shared<PCHContainerOperations>();
85 std::shared_ptr<CompilerInvocation> CI(new CompilerInvocation);
86 CI->getFrontendOpts().Inputs.push_back(
87 Elt: FrontendInputFile(EntryFile, FrontendOptions::getInputKindForExtension(
88 Extension: llvm::sys::path::extension(path: EntryFile).substr(Start: 1))));
89
90 CI->getTargetOpts().Triple = "i386-unknown-linux-gnu";
91
92 CI->getPreprocessorOpts().RemappedFileBuffers = GetRemappedFiles();
93
94 PreprocessorOptions &PPOpts = CI->getPreprocessorOpts();
95 PPOpts.RemappedFilesKeepOriginalName = true;
96
97 IntrusiveRefCntPtr<DiagnosticsEngine>
98 Diags(CompilerInstance::createDiagnostics(Opts: new DiagnosticOptions, Client: new DiagnosticConsumer));
99
100 FileManager *FileMgr = new FileManager(FSOpts, VFS);
101
102 std::unique_ptr<ASTUnit> AST = ASTUnit::LoadFromCompilerInvocation(
103 CI, PCHContainerOps: PCHContainerOpts, Diags, FileMgr, OnlyLocalDecls: false, CaptureDiagnostics: CaptureDiagsKind::None,
104 /*PrecompilePreambleAfterNParses=*/1);
105 return AST;
106 }
107
108 bool ReparseAST(const std::unique_ptr<ASTUnit> &AST) {
109 bool reparseFailed = AST->Reparse(PCHContainerOps: PCHContainerOpts, RemappedFiles: GetRemappedFiles(), VFS);
110 return !reparseFailed;
111 }
112
113 unsigned GetFileReadCount(const std::string &Filename) const {
114 return VFS->GetReadCount(Path: Filename);
115 }
116
117private:
118 std::vector<std::pair<std::string, llvm::MemoryBuffer *>>
119 GetRemappedFiles() const {
120 std::vector<std::pair<std::string, llvm::MemoryBuffer *>> Remapped;
121 for (const auto &RemappedFile : RemappedFiles) {
122 std::unique_ptr<MemoryBuffer> buf = MemoryBuffer::getMemBufferCopy(
123 InputData: RemappedFile.second, BufferName: RemappedFile.first());
124 Remapped.emplace_back(args: std::string(RemappedFile.first()), args: buf.release());
125 }
126 return Remapped;
127 }
128};
129
130TEST_F(PCHPreambleTest, ReparseReusesPreambleWithUnsavedFileNotExistingOnDisk) {
131 std::string Header1 = "//./header1.h";
132 std::string MainName = "//./main.cpp";
133 AddFile(Filename: MainName, Contents: R"cpp(
134#include "//./header1.h"
135int main() { return ZERO; }
136)cpp");
137 RemapFile(Filename: Header1, Contents: "#define ZERO 0\n");
138
139 // Parse with header file provided as unsaved file, which does not exist on
140 // disk.
141 std::unique_ptr<ASTUnit> AST(ParseAST(EntryFile: MainName));
142 ASSERT_TRUE(AST.get());
143 ASSERT_FALSE(AST->getDiagnostics().hasErrorOccurred());
144
145 // Reparse and check that the preamble was reused.
146 ASSERT_TRUE(ReparseAST(AST));
147 ASSERT_EQ(AST->getPreambleCounterForTests(), 1U);
148}
149
150TEST_F(PCHPreambleTest, ReparseReusesPreambleAfterUnsavedFileWasCreatedOnDisk) {
151 std::string Header1 = "//./header1.h";
152 std::string MainName = "//./main.cpp";
153 AddFile(Filename: MainName, Contents: R"cpp(
154#include "//./header1.h"
155int main() { return ZERO; }
156)cpp");
157 RemapFile(Filename: Header1, Contents: "#define ZERO 0\n");
158
159 // Parse with header file provided as unsaved file, which does not exist on
160 // disk.
161 std::unique_ptr<ASTUnit> AST(ParseAST(EntryFile: MainName));
162 ASSERT_TRUE(AST.get());
163 ASSERT_FALSE(AST->getDiagnostics().hasErrorOccurred());
164
165 // Create the unsaved file also on disk and check that preamble was reused.
166 AddFile(Filename: Header1, Contents: "#define ZERO 0\n");
167 ASSERT_TRUE(ReparseAST(AST));
168 ASSERT_EQ(AST->getPreambleCounterForTests(), 1U);
169}
170
171TEST_F(PCHPreambleTest,
172 ReparseReusesPreambleAfterUnsavedFileWasRemovedFromDisk) {
173 std::string Header1 = "//./foo/header1.h";
174 std::string MainName = "//./main.cpp";
175 std::string MainFileContent = R"cpp(
176#include "//./foo/header1.h"
177int main() { return ZERO; }
178)cpp";
179 AddFile(Filename: MainName, Contents: MainFileContent);
180 AddFile(Filename: Header1, Contents: "#define ZERO 0\n");
181 RemapFile(Filename: Header1, Contents: "#define ZERO 0\n");
182
183 // Parse with header file provided as unsaved file, which exists on disk.
184 std::unique_ptr<ASTUnit> AST(ParseAST(EntryFile: MainName));
185 ASSERT_TRUE(AST.get());
186 ASSERT_FALSE(AST->getDiagnostics().hasErrorOccurred());
187 ASSERT_EQ(AST->getPreambleCounterForTests(), 1U);
188
189 // Remove the unsaved file from disk and check that the preamble was reused.
190 ResetVFS();
191 AddFile(Filename: MainName, Contents: MainFileContent);
192 ASSERT_TRUE(ReparseAST(AST));
193 ASSERT_EQ(AST->getPreambleCounterForTests(), 1U);
194}
195
196TEST_F(PCHPreambleTest, ReparseWithOverriddenFileDoesNotInvalidatePreamble) {
197 std::string Header1 = "//./header1.h";
198 std::string Header2 = "//./header2.h";
199 std::string MainName = "//./main.cpp";
200 AddFile(Filename: Header1, Contents: "");
201 AddFile(Filename: Header2, Contents: "#pragma once");
202 AddFile(Filename: MainName,
203 Contents: "#include \"//./foo/../header1.h\"\n"
204 "#include \"//./foo/../header2.h\"\n"
205 "int main() { return ZERO; }");
206 RemapFile(Filename: Header1, Contents: "static const int ZERO = 0;\n");
207
208 std::unique_ptr<ASTUnit> AST(ParseAST(EntryFile: MainName));
209 ASSERT_TRUE(AST.get());
210 ASSERT_FALSE(AST->getDiagnostics().hasErrorOccurred());
211
212 unsigned initialCounts[] = {
213 GetFileReadCount(Filename: MainName),
214 GetFileReadCount(Filename: Header1),
215 GetFileReadCount(Filename: Header2)
216 };
217
218 ASSERT_TRUE(ReparseAST(AST));
219
220 ASSERT_NE(initialCounts[0], GetFileReadCount(MainName));
221 ASSERT_EQ(initialCounts[1], GetFileReadCount(Header1));
222 ASSERT_EQ(initialCounts[2], GetFileReadCount(Header2));
223}
224
225TEST_F(PCHPreambleTest, ParseWithBom) {
226 std::string Header = "//./header.h";
227 std::string Main = "//./main.cpp";
228 AddFile(Filename: Header, Contents: "int random() { return 4; }");
229 AddFile(Filename: Main,
230 Contents: "\xef\xbb\xbf"
231 "#include \"//./header.h\"\n"
232 "int main() { return random() -2; }");
233
234 std::unique_ptr<ASTUnit> AST(ParseAST(EntryFile: Main));
235 ASSERT_TRUE(AST.get());
236 ASSERT_FALSE(AST->getDiagnostics().hasErrorOccurred());
237
238 unsigned HeaderReadCount = GetFileReadCount(Filename: Header);
239
240 ASSERT_TRUE(ReparseAST(AST));
241 ASSERT_FALSE(AST->getDiagnostics().hasErrorOccurred());
242
243 // Check preamble PCH was really reused
244 ASSERT_EQ(HeaderReadCount, GetFileReadCount(Header));
245
246 // Remove BOM
247 RemapFile(Filename: Main,
248 Contents: "#include \"//./header.h\"\n"
249 "int main() { return random() -2; }");
250
251 ASSERT_TRUE(ReparseAST(AST));
252 ASSERT_FALSE(AST->getDiagnostics().hasErrorOccurred());
253
254 ASSERT_LE(HeaderReadCount, GetFileReadCount(Header));
255 HeaderReadCount = GetFileReadCount(Filename: Header);
256
257 // Add BOM back
258 RemapFile(Filename: Main,
259 Contents: "\xef\xbb\xbf"
260 "#include \"//./header.h\"\n"
261 "int main() { return random() -2; }");
262
263 ASSERT_TRUE(ReparseAST(AST));
264 ASSERT_FALSE(AST->getDiagnostics().hasErrorOccurred());
265
266 ASSERT_LE(HeaderReadCount, GetFileReadCount(Header));
267}
268
269} // anonymous namespace
270

source code of clang/unittests/Frontend/PCHPreambleTest.cpp