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