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 | FileSystemOptions FSOpts; |
57 | |
58 | public: |
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 | |
117 | private: |
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 | |
130 | TEST_F(PCHPreambleTest, ReparseReusesPreambleWithUnsavedFileNotExistingOnDisk) { |
131 | std::string = "//./header1.h" ; |
132 | std::string MainName = "//./main.cpp" ; |
133 | AddFile(Filename: MainName, Contents: R"cpp( |
134 | #include "//./header1.h" |
135 | int 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 | |
150 | TEST_F(PCHPreambleTest, ReparseReusesPreambleAfterUnsavedFileWasCreatedOnDisk) { |
151 | std::string = "//./header1.h" ; |
152 | std::string MainName = "//./main.cpp" ; |
153 | AddFile(Filename: MainName, Contents: R"cpp( |
154 | #include "//./header1.h" |
155 | int 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 | |
171 | TEST_F(PCHPreambleTest, |
172 | ReparseReusesPreambleAfterUnsavedFileWasRemovedFromDisk) { |
173 | std::string = "//./foo/header1.h" ; |
174 | std::string MainName = "//./main.cpp" ; |
175 | std::string MainFileContent = R"cpp( |
176 | #include "//./foo/header1.h" |
177 | int 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 | |
196 | TEST_F(PCHPreambleTest, ReparseWithOverriddenFileDoesNotInvalidatePreamble) { |
197 | std::string = "//./header1.h" ; |
198 | std::string = "//./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 | |
225 | TEST_F(PCHPreambleTest, ParseWithBom) { |
226 | std::string = "//./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 = 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 | |