1 | //===-- GlobalCompilationDatabaseTests.cpp ----------------------*- C++ -*-===// |
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 "GlobalCompilationDatabase.h" |
10 | |
11 | #include "CompileCommands.h" |
12 | #include "Config.h" |
13 | #include "TestFS.h" |
14 | #include "support/Path.h" |
15 | #include "support/ThreadsafeFS.h" |
16 | #include "clang/Tooling/CompilationDatabase.h" |
17 | #include "llvm/ADT/STLExtras.h" |
18 | #include "llvm/ADT/SmallString.h" |
19 | #include "llvm/ADT/StringRef.h" |
20 | #include "llvm/Support/FormatVariadic.h" |
21 | #include "llvm/Support/Path.h" |
22 | #include "gmock/gmock.h" |
23 | #include "gtest/gtest.h" |
24 | #include <chrono> |
25 | #include <fstream> |
26 | #include <optional> |
27 | #include <string> |
28 | |
29 | namespace clang { |
30 | namespace clangd { |
31 | namespace { |
32 | using ::testing::AllOf; |
33 | using ::testing::Contains; |
34 | using ::testing::ElementsAre; |
35 | using ::testing::EndsWith; |
36 | using ::testing::HasSubstr; |
37 | using ::testing::IsEmpty; |
38 | using ::testing::Not; |
39 | using ::testing::UnorderedElementsAre; |
40 | |
41 | TEST(GlobalCompilationDatabaseTest, FallbackCommand) { |
42 | MockFS TFS; |
43 | DirectoryBasedGlobalCompilationDatabase DB(TFS); |
44 | auto Cmd = DB.getFallbackCommand(File: testPath(File: "foo/bar.cc" )); |
45 | EXPECT_EQ(Cmd.Directory, testPath("foo" )); |
46 | EXPECT_THAT(Cmd.CommandLine, ElementsAre("clang" , testPath("foo/bar.cc" ))); |
47 | EXPECT_EQ(Cmd.Output, "" ); |
48 | |
49 | // .h files have unknown language, so they are parsed liberally as obj-c++. |
50 | Cmd = DB.getFallbackCommand(File: testPath(File: "foo/bar.h" )); |
51 | EXPECT_THAT(Cmd.CommandLine, ElementsAre("clang" , "-xobjective-c++-header" , |
52 | testPath("foo/bar.h" ))); |
53 | Cmd = DB.getFallbackCommand(File: testPath(File: "foo/bar" )); |
54 | EXPECT_THAT(Cmd.CommandLine, ElementsAre("clang" , "-xobjective-c++-header" , |
55 | testPath("foo/bar" ))); |
56 | } |
57 | |
58 | static tooling::CompileCommand cmd(llvm::StringRef File, llvm::StringRef Arg) { |
59 | return tooling::CompileCommand( |
60 | testRoot(), File, {"clang" , std::string(Arg), std::string(File)}, "" ); |
61 | } |
62 | |
63 | class OverlayCDBTest : public ::testing::Test { |
64 | class BaseCDB : public GlobalCompilationDatabase { |
65 | public: |
66 | std::optional<tooling::CompileCommand> |
67 | getCompileCommand(llvm::StringRef File) const override { |
68 | if (File == testPath(File: "foo.cc" )) |
69 | return cmd(File, Arg: "-DA=1" ); |
70 | return std::nullopt; |
71 | } |
72 | |
73 | tooling::CompileCommand |
74 | getFallbackCommand(llvm::StringRef File) const override { |
75 | return cmd(File, Arg: "-DA=2" ); |
76 | } |
77 | |
78 | std::optional<ProjectInfo> getProjectInfo(PathRef File) const override { |
79 | return ProjectInfo{.SourceRoot: testRoot()}; |
80 | } |
81 | }; |
82 | |
83 | protected: |
84 | OverlayCDBTest() : Base(std::make_unique<BaseCDB>()) {} |
85 | std::unique_ptr<GlobalCompilationDatabase> Base; |
86 | }; |
87 | |
88 | TEST_F(OverlayCDBTest, GetCompileCommand) { |
89 | OverlayCDB CDB(Base.get()); |
90 | EXPECT_THAT(CDB.getCompileCommand(testPath("foo.cc" ))->CommandLine, |
91 | AllOf(Contains(testPath("foo.cc" )), Contains("-DA=1" ))); |
92 | EXPECT_EQ(CDB.getCompileCommand(testPath("missing.cc" )), std::nullopt); |
93 | |
94 | auto Override = cmd(File: testPath(File: "foo.cc" ), Arg: "-DA=3" ); |
95 | CDB.setCompileCommand(File: testPath(File: "foo.cc" ), CompilationCommand: Override); |
96 | EXPECT_THAT(CDB.getCompileCommand(testPath("foo.cc" ))->CommandLine, |
97 | Contains("-DA=3" )); |
98 | EXPECT_EQ(CDB.getCompileCommand(testPath("missing.cc" )), std::nullopt); |
99 | CDB.setCompileCommand(File: testPath(File: "missing.cc" ), CompilationCommand: Override); |
100 | EXPECT_THAT(CDB.getCompileCommand(testPath("missing.cc" ))->CommandLine, |
101 | Contains("-DA=3" )); |
102 | } |
103 | |
104 | TEST_F(OverlayCDBTest, GetFallbackCommand) { |
105 | OverlayCDB CDB(Base.get(), {"-DA=4" }); |
106 | EXPECT_THAT(CDB.getFallbackCommand(testPath("bar.cc" )).CommandLine, |
107 | ElementsAre("clang" , "-DA=2" , testPath("bar.cc" ), "-DA=4" )); |
108 | } |
109 | |
110 | TEST_F(OverlayCDBTest, NoBase) { |
111 | OverlayCDB CDB(nullptr, {"-DA=6" }); |
112 | EXPECT_EQ(CDB.getCompileCommand(testPath("bar.cc" )), std::nullopt); |
113 | auto Override = cmd(File: testPath(File: "bar.cc" ), Arg: "-DA=5" ); |
114 | CDB.setCompileCommand(File: testPath(File: "bar.cc" ), CompilationCommand: Override); |
115 | EXPECT_THAT(CDB.getCompileCommand(testPath("bar.cc" ))->CommandLine, |
116 | Contains("-DA=5" )); |
117 | |
118 | EXPECT_THAT(CDB.getFallbackCommand(testPath("foo.cc" )).CommandLine, |
119 | ElementsAre("clang" , testPath("foo.cc" ), "-DA=6" )); |
120 | } |
121 | |
122 | TEST_F(OverlayCDBTest, Watch) { |
123 | OverlayCDB Inner(nullptr); |
124 | OverlayCDB Outer(&Inner); |
125 | |
126 | std::vector<std::vector<std::string>> Changes; |
127 | auto Sub = Outer.watch(L: [&](const std::vector<std::string> &ChangedFiles) { |
128 | Changes.push_back(x: ChangedFiles); |
129 | }); |
130 | |
131 | Inner.setCompileCommand(File: "A.cpp" , CompilationCommand: tooling::CompileCommand()); |
132 | Outer.setCompileCommand(File: "B.cpp" , CompilationCommand: tooling::CompileCommand()); |
133 | Inner.setCompileCommand(File: "A.cpp" , CompilationCommand: std::nullopt); |
134 | Outer.setCompileCommand(File: "C.cpp" , CompilationCommand: std::nullopt); |
135 | EXPECT_THAT(Changes, ElementsAre(ElementsAre("A.cpp" ), ElementsAre("B.cpp" ), |
136 | ElementsAre("A.cpp" ), ElementsAre("C.cpp" ))); |
137 | } |
138 | |
139 | TEST_F(OverlayCDBTest, Adjustments) { |
140 | OverlayCDB CDB(Base.get(), {"-DFallback" }, |
141 | [](tooling::CompileCommand &Cmd, llvm::StringRef File) { |
142 | Cmd.CommandLine.push_back( |
143 | x: ("-DAdjust_" + llvm::sys::path::filename(path: File)).str()); |
144 | }); |
145 | // Command from underlying gets adjusted. |
146 | auto Cmd = *CDB.getCompileCommand(File: testPath(File: "foo.cc" )); |
147 | EXPECT_THAT(Cmd.CommandLine, ElementsAre("clang" , "-DA=1" , testPath("foo.cc" ), |
148 | "-DAdjust_foo.cc" )); |
149 | |
150 | // Command from overlay gets adjusted. |
151 | tooling::CompileCommand BarCommand; |
152 | BarCommand.Filename = testPath(File: "bar.cc" ); |
153 | BarCommand.CommandLine = {"clang++" , "-DB=1" , testPath(File: "bar.cc" )}; |
154 | CDB.setCompileCommand(File: testPath(File: "bar.cc" ), CompilationCommand: BarCommand); |
155 | Cmd = *CDB.getCompileCommand(File: testPath(File: "bar.cc" )); |
156 | EXPECT_THAT( |
157 | Cmd.CommandLine, |
158 | ElementsAre("clang++" , "-DB=1" , testPath("bar.cc" ), "-DAdjust_bar.cc" )); |
159 | |
160 | // Fallback gets adjusted. |
161 | Cmd = CDB.getFallbackCommand(File: "baz.cc" ); |
162 | EXPECT_THAT(Cmd.CommandLine, ElementsAre("clang" , "-DA=2" , "baz.cc" , |
163 | "-DFallback" , "-DAdjust_baz.cc" )); |
164 | } |
165 | |
166 | TEST_F(OverlayCDBTest, ExpandedResponseFiles) { |
167 | SmallString<1024> Path; |
168 | int FD; |
169 | ASSERT_FALSE(llvm::sys::fs::createTemporaryFile("args" , "" , FD, Path)); |
170 | llvm::raw_fd_ostream OutStream(FD, true); |
171 | OutStream << "-Wall" ; |
172 | OutStream.close(); |
173 | |
174 | OverlayCDB CDB(Base.get(), {"-DFallback" }); |
175 | auto Override = cmd(File: testPath(File: "foo.cc" ), Arg: ("@" + Path).str()); |
176 | CDB.setCompileCommand(File: testPath(File: "foo.cc" ), CompilationCommand: Override); |
177 | EXPECT_THAT(CDB.getCompileCommand(testPath("foo.cc" ))->CommandLine, |
178 | Contains("-Wall" )); |
179 | } |
180 | |
181 | TEST(GlobalCompilationDatabaseTest, DiscoveryWithNestedCDBs) { |
182 | const char *const CDBOuter = |
183 | R"cdb( |
184 | [ |
185 | { |
186 | "file": "a.cc", |
187 | "command": "", |
188 | "directory": "{0}", |
189 | }, |
190 | { |
191 | "file": "build/gen.cc", |
192 | "command": "", |
193 | "directory": "{0}", |
194 | }, |
195 | { |
196 | "file": "build/gen2.cc", |
197 | "command": "", |
198 | "directory": "{0}", |
199 | } |
200 | ] |
201 | )cdb" ; |
202 | const char *const CDBInner = |
203 | R"cdb( |
204 | [ |
205 | { |
206 | "file": "gen.cc", |
207 | "command": "", |
208 | "directory": "{0}/build", |
209 | } |
210 | ] |
211 | )cdb" ; |
212 | MockFS FS; |
213 | FS.Files[testPath(File: "compile_commands.json" )] = |
214 | llvm::formatv(Fmt: CDBOuter, Vals: llvm::sys::path::convert_to_slash(path: testRoot())); |
215 | FS.Files[testPath(File: "build/compile_commands.json" )] = |
216 | llvm::formatv(Fmt: CDBInner, Vals: llvm::sys::path::convert_to_slash(path: testRoot())); |
217 | FS.Files[testPath(File: "foo/compile_flags.txt" )] = "-DFOO" ; |
218 | |
219 | // Note that gen2.cc goes missing with our following model, not sure this |
220 | // happens in practice though. |
221 | { |
222 | SCOPED_TRACE("Default ancestor scanning" ); |
223 | DirectoryBasedGlobalCompilationDatabase DB(FS); |
224 | std::vector<std::string> DiscoveredFiles; |
225 | auto Sub = |
226 | DB.watch(L: [&DiscoveredFiles](const std::vector<std::string> Changes) { |
227 | DiscoveredFiles = Changes; |
228 | }); |
229 | |
230 | DB.getCompileCommand(File: testPath(File: "build/../a.cc" )); |
231 | ASSERT_TRUE(DB.blockUntilIdle(timeoutSeconds(10))); |
232 | EXPECT_THAT(DiscoveredFiles, UnorderedElementsAre(AllOf( |
233 | EndsWith("a.cc" ), Not(HasSubstr(".." ))))); |
234 | DiscoveredFiles.clear(); |
235 | |
236 | DB.getCompileCommand(File: testPath(File: "build/gen.cc" )); |
237 | ASSERT_TRUE(DB.blockUntilIdle(timeoutSeconds(10))); |
238 | EXPECT_THAT(DiscoveredFiles, UnorderedElementsAre(EndsWith("gen.cc" ))); |
239 | } |
240 | |
241 | { |
242 | SCOPED_TRACE("With config" ); |
243 | DirectoryBasedGlobalCompilationDatabase::Options Opts(FS); |
244 | Opts.ContextProvider = [&](llvm::StringRef Path) { |
245 | Config Cfg; |
246 | if (Path.ends_with(Suffix: "a.cc" )) { |
247 | // a.cc uses another directory's CDB, so it won't be discovered. |
248 | Cfg.CompileFlags.CDBSearch.Policy = Config::CDBSearchSpec::FixedDir; |
249 | Cfg.CompileFlags.CDBSearch.FixedCDBPath = testPath(File: "foo" ); |
250 | } else if (Path.ends_with(Suffix: "gen.cc" )) { |
251 | // gen.cc has CDB search disabled, so it won't be discovered. |
252 | Cfg.CompileFlags.CDBSearch.Policy = Config::CDBSearchSpec::NoCDBSearch; |
253 | } else if (Path.ends_with(Suffix: "gen2.cc" )) { |
254 | // gen2.cc explicitly lists this directory, so it will be discovered. |
255 | Cfg.CompileFlags.CDBSearch.Policy = Config::CDBSearchSpec::FixedDir; |
256 | Cfg.CompileFlags.CDBSearch.FixedCDBPath = testRoot(); |
257 | } |
258 | return Context::current().derive(Key: Config::Key, Value: std::move(Cfg)); |
259 | }; |
260 | DirectoryBasedGlobalCompilationDatabase DB(Opts); |
261 | std::vector<std::string> DiscoveredFiles; |
262 | auto Sub = |
263 | DB.watch(L: [&DiscoveredFiles](const std::vector<std::string> Changes) { |
264 | DiscoveredFiles = Changes; |
265 | }); |
266 | |
267 | // Does not use the root CDB, so no broadcast. |
268 | auto Cmd = DB.getCompileCommand(File: testPath(File: "build/../a.cc" )); |
269 | ASSERT_TRUE(Cmd); |
270 | EXPECT_THAT(Cmd->CommandLine, Contains("-DFOO" )) << "a.cc uses foo/ CDB" ; |
271 | ASSERT_TRUE(DB.blockUntilIdle(timeoutSeconds(10))); |
272 | EXPECT_THAT(DiscoveredFiles, IsEmpty()) << "Root CDB not discovered yet" ; |
273 | |
274 | // No special config for b.cc, so we trigger broadcast of the root CDB. |
275 | DB.getCompileCommand(File: testPath(File: "b.cc" )); |
276 | ASSERT_TRUE(DB.blockUntilIdle(timeoutSeconds(10))); |
277 | EXPECT_THAT(DiscoveredFiles, ElementsAre(testPath("build/gen2.cc" ))); |
278 | DiscoveredFiles.clear(); |
279 | |
280 | // No CDB search so no discovery/broadcast triggered for build/ CDB. |
281 | DB.getCompileCommand(File: testPath(File: "build/gen.cc" )); |
282 | ASSERT_TRUE(DB.blockUntilIdle(timeoutSeconds(10))); |
283 | EXPECT_THAT(DiscoveredFiles, IsEmpty()); |
284 | } |
285 | |
286 | { |
287 | SCOPED_TRACE("With custom compile commands dir" ); |
288 | DirectoryBasedGlobalCompilationDatabase::Options Opts(FS); |
289 | Opts.CompileCommandsDir = testRoot(); |
290 | DirectoryBasedGlobalCompilationDatabase DB(Opts); |
291 | std::vector<std::string> DiscoveredFiles; |
292 | auto Sub = |
293 | DB.watch(L: [&DiscoveredFiles](const std::vector<std::string> Changes) { |
294 | DiscoveredFiles = Changes; |
295 | }); |
296 | |
297 | DB.getCompileCommand(File: testPath(File: "a.cc" )); |
298 | ASSERT_TRUE(DB.blockUntilIdle(timeoutSeconds(10))); |
299 | EXPECT_THAT(DiscoveredFiles, |
300 | UnorderedElementsAre(EndsWith("a.cc" ), EndsWith("gen.cc" ), |
301 | EndsWith("gen2.cc" ))); |
302 | DiscoveredFiles.clear(); |
303 | |
304 | DB.getCompileCommand(File: testPath(File: "build/gen.cc" )); |
305 | ASSERT_TRUE(DB.blockUntilIdle(timeoutSeconds(10))); |
306 | EXPECT_THAT(DiscoveredFiles, IsEmpty()); |
307 | } |
308 | } |
309 | |
310 | TEST(GlobalCompilationDatabaseTest, BuildDir) { |
311 | MockFS FS; |
312 | auto Command = [&](llvm::StringRef Relative) { |
313 | DirectoryBasedGlobalCompilationDatabase::Options Opts(FS); |
314 | return DirectoryBasedGlobalCompilationDatabase(Opts) |
315 | .getCompileCommand(File: testPath(File: Relative)) |
316 | .value_or(u: tooling::CompileCommand()) |
317 | .CommandLine; |
318 | }; |
319 | EXPECT_THAT(Command("x/foo.cc" ), IsEmpty()); |
320 | const char *const CDB = |
321 | R"cdb( |
322 | [ |
323 | { |
324 | "file": "{0}/x/foo.cc", |
325 | "command": "clang -DXYZZY {0}/x/foo.cc", |
326 | "directory": "{0}", |
327 | }, |
328 | { |
329 | "file": "{0}/bar.cc", |
330 | "command": "clang -DXYZZY {0}/bar.cc", |
331 | "directory": "{0}", |
332 | } |
333 | ] |
334 | )cdb" ; |
335 | FS.Files[testPath(File: "x/build/compile_commands.json" )] = |
336 | llvm::formatv(Fmt: CDB, Vals: llvm::sys::path::convert_to_slash(path: testRoot())); |
337 | EXPECT_THAT(Command("x/foo.cc" ), Contains("-DXYZZY" )); |
338 | EXPECT_THAT(Command("bar.cc" ), IsEmpty()) |
339 | << "x/build/compile_flags.json only applicable to x/" ; |
340 | } |
341 | |
342 | TEST(GlobalCompilationDatabaseTest, CompileFlagsDirectory) { |
343 | MockFS FS; |
344 | FS.Files[testPath(File: "x/compile_flags.txt" )] = "-DFOO" ; |
345 | DirectoryBasedGlobalCompilationDatabase CDB(FS); |
346 | auto Commands = CDB.getCompileCommand(File: testPath(File: "x/y.cpp" )); |
347 | ASSERT_TRUE(Commands.has_value()); |
348 | EXPECT_THAT(Commands->CommandLine, Contains("-DFOO" )); |
349 | // Make sure we pick the right working directory. |
350 | EXPECT_EQ(testPath("x" ), Commands->Directory); |
351 | } |
352 | |
353 | MATCHER_P(hasArg, Flag, "" ) { |
354 | if (!arg) { |
355 | *result_listener << "command is null" ; |
356 | return false; |
357 | } |
358 | if (!llvm::is_contained(arg->CommandLine, Flag)) { |
359 | *result_listener << "flags are " << printArgv(arg->CommandLine); |
360 | return false; |
361 | } |
362 | return true; |
363 | } |
364 | |
365 | TEST(GlobalCompilationDatabaseTest, Config) { |
366 | MockFS FS; |
367 | FS.Files[testPath(File: "x/compile_flags.txt" )] = "-DX" ; |
368 | FS.Files[testPath(File: "x/y/z/compile_flags.txt" )] = "-DZ" ; |
369 | |
370 | Config::CDBSearchSpec Spec; |
371 | DirectoryBasedGlobalCompilationDatabase::Options Opts(FS); |
372 | Opts.ContextProvider = [&](llvm::StringRef Path) { |
373 | Config C; |
374 | C.CompileFlags.CDBSearch = Spec; |
375 | return Context::current().derive(Key: Config::Key, Value: std::move(C)); |
376 | }; |
377 | DirectoryBasedGlobalCompilationDatabase CDB(Opts); |
378 | |
379 | // Default ancestor behavior. |
380 | EXPECT_FALSE(CDB.getCompileCommand(testPath("foo.cc" ))); |
381 | EXPECT_THAT(CDB.getCompileCommand(testPath("x/foo.cc" )), hasArg("-DX" )); |
382 | EXPECT_THAT(CDB.getCompileCommand(testPath("x/y/foo.cc" )), hasArg("-DX" )); |
383 | EXPECT_THAT(CDB.getCompileCommand(testPath("x/y/z/foo.cc" )), hasArg("-DZ" )); |
384 | |
385 | Spec.Policy = Config::CDBSearchSpec::NoCDBSearch; |
386 | EXPECT_FALSE(CDB.getCompileCommand(testPath("foo.cc" ))); |
387 | EXPECT_FALSE(CDB.getCompileCommand(testPath("x/foo.cc" ))); |
388 | EXPECT_FALSE(CDB.getCompileCommand(testPath("x/y/foo.cc" ))); |
389 | EXPECT_FALSE(CDB.getCompileCommand(testPath("x/y/z/foo.cc" ))); |
390 | |
391 | Spec.Policy = Config::CDBSearchSpec::FixedDir; |
392 | Spec.FixedCDBPath = testPath(File: "w" ); // doesn't exist |
393 | EXPECT_FALSE(CDB.getCompileCommand(testPath("foo.cc" ))); |
394 | EXPECT_FALSE(CDB.getCompileCommand(testPath("x/foo.cc" ))); |
395 | EXPECT_FALSE(CDB.getCompileCommand(testPath("x/y/foo.cc" ))); |
396 | EXPECT_FALSE(CDB.getCompileCommand(testPath("x/y/z/foo.cc" ))); |
397 | |
398 | Spec.FixedCDBPath = testPath(File: "x/y/z" ); |
399 | EXPECT_THAT(CDB.getCompileCommand(testPath("foo.cc" )), hasArg("-DZ" )); |
400 | EXPECT_THAT(CDB.getCompileCommand(testPath("x/foo.cc" )), hasArg("-DZ" )); |
401 | EXPECT_THAT(CDB.getCompileCommand(testPath("x/y/foo.cc" )), hasArg("-DZ" )); |
402 | EXPECT_THAT(CDB.getCompileCommand(testPath("x/y/z/foo.cc" )), hasArg("-DZ" )); |
403 | } |
404 | |
405 | TEST(GlobalCompilationDatabaseTest, NonCanonicalFilenames) { |
406 | OverlayCDB DB(nullptr); |
407 | std::vector<std::string> DiscoveredFiles; |
408 | auto Sub = |
409 | DB.watch(L: [&DiscoveredFiles](const std::vector<std::string> Changes) { |
410 | DiscoveredFiles = Changes; |
411 | }); |
412 | |
413 | llvm::SmallString<128> Root(testRoot()); |
414 | llvm::sys::path::append(path&: Root, a: "build" , b: ".." , c: "a.cc" ); |
415 | DB.setCompileCommand(File: Root.str(), CompilationCommand: tooling::CompileCommand()); |
416 | EXPECT_THAT(DiscoveredFiles, UnorderedElementsAre(testPath("a.cc" ))); |
417 | DiscoveredFiles.clear(); |
418 | |
419 | llvm::SmallString<128> File(testRoot()); |
420 | llvm::sys::path::append(path&: File, a: "blabla" , b: ".." , c: "a.cc" ); |
421 | |
422 | EXPECT_TRUE(DB.getCompileCommand(File)); |
423 | EXPECT_FALSE(DB.getProjectInfo(File)); |
424 | } |
425 | |
426 | TEST_F(OverlayCDBTest, GetProjectInfo) { |
427 | OverlayCDB DB(Base.get()); |
428 | Path File = testPath(File: "foo.cc" ); |
429 | Path = testPath(File: "foo.h" ); |
430 | |
431 | EXPECT_EQ(DB.getProjectInfo(File)->SourceRoot, testRoot()); |
432 | EXPECT_EQ(DB.getProjectInfo(Header)->SourceRoot, testRoot()); |
433 | |
434 | // Shouldn't change after an override. |
435 | DB.setCompileCommand(File, CompilationCommand: tooling::CompileCommand()); |
436 | EXPECT_EQ(DB.getProjectInfo(File)->SourceRoot, testRoot()); |
437 | EXPECT_EQ(DB.getProjectInfo(Header)->SourceRoot, testRoot()); |
438 | } |
439 | |
440 | TEST(GlobalCompilationDatabaseTest, InferenceWithResponseFile) { |
441 | MockFS FS; |
442 | auto Command = [&](llvm::StringRef Relative) { |
443 | DirectoryBasedGlobalCompilationDatabase::Options Opts(FS); |
444 | return DirectoryBasedGlobalCompilationDatabase(Opts) |
445 | .getCompileCommand(File: testPath(File: Relative)) |
446 | .value_or(u: tooling::CompileCommand()) |
447 | .CommandLine; |
448 | }; |
449 | EXPECT_THAT(Command("foo.cc" ), IsEmpty()); |
450 | |
451 | // Have to use real FS for response file. |
452 | SmallString<1024> Path; |
453 | int FD; |
454 | ASSERT_FALSE(llvm::sys::fs::createTemporaryFile("args" , "" , FD, Path)); |
455 | llvm::raw_fd_ostream OutStream(FD, true); |
456 | OutStream << "-DXYZZY" ; |
457 | OutStream.close(); |
458 | |
459 | const char *const CDB = |
460 | R"cdb( |
461 | [ |
462 | { |
463 | "file": "{0}/foo.cc", |
464 | "command": "clang @{1} {0}/foo.cc", |
465 | "directory": "{0}", |
466 | } |
467 | ] |
468 | )cdb" ; |
469 | FS.Files[testPath(File: "compile_commands.json" )] = |
470 | llvm::formatv(Fmt: CDB, Vals: llvm::sys::path::convert_to_slash(path: testRoot()), |
471 | Vals: llvm::sys::path::convert_to_slash(path: Path)); |
472 | |
473 | // File from CDB. |
474 | EXPECT_THAT(Command("foo.cc" ), Contains("-DXYZZY" )); |
475 | // File not in CDB, use inference. |
476 | EXPECT_THAT(Command("foo.h" ), Contains("-DXYZZY" )); |
477 | } |
478 | } // namespace |
479 | |
480 | // Friend test has access to internals. |
481 | class DirectoryBasedGlobalCompilationDatabaseCacheTest |
482 | : public ::testing::Test { |
483 | protected: |
484 | std::shared_ptr<const tooling::CompilationDatabase> |
485 | lookupCDB(const DirectoryBasedGlobalCompilationDatabase &GDB, |
486 | llvm::StringRef Path, |
487 | std::chrono::steady_clock::time_point FreshTime) { |
488 | DirectoryBasedGlobalCompilationDatabase::CDBLookupRequest Req; |
489 | Req.FileName = Path; |
490 | Req.FreshTime = Req.FreshTimeMissing = FreshTime; |
491 | if (auto Result = GDB.lookupCDB(Request: Req)) |
492 | return std::move(Result->CDB); |
493 | return nullptr; |
494 | } |
495 | }; |
496 | |
497 | // Matches non-null CDBs which include the specified flag. |
498 | MATCHER_P2(hasFlag, Flag, Path, "" ) { |
499 | if (arg == nullptr) |
500 | return false; |
501 | auto Cmds = arg->getCompileCommands(Path); |
502 | if (Cmds.empty()) { |
503 | *result_listener << "yields no commands" ; |
504 | return false; |
505 | } |
506 | if (!llvm::is_contained(Cmds.front().CommandLine, Flag)) { |
507 | *result_listener << "flags are: " << printArgv(Cmds.front().CommandLine); |
508 | return false; |
509 | } |
510 | return true; |
511 | } |
512 | |
513 | auto hasFlag(llvm::StringRef Flag) { |
514 | return hasFlag(gmock_p0: Flag, gmock_p1: "mock_file_name.cc" ); |
515 | } |
516 | |
517 | TEST_F(DirectoryBasedGlobalCompilationDatabaseCacheTest, Cacheable) { |
518 | MockFS FS; |
519 | auto Stale = std::chrono::steady_clock::now() - std::chrono::minutes(1); |
520 | auto Fresh = std::chrono::steady_clock::now() + std::chrono::hours(24); |
521 | |
522 | DirectoryBasedGlobalCompilationDatabase GDB(FS); |
523 | FS.Files["compile_flags.txt" ] = "-DROOT" ; |
524 | auto Root = lookupCDB(GDB, Path: testPath(File: "foo/test.cc" ), FreshTime: Stale); |
525 | EXPECT_THAT(Root, hasFlag("-DROOT" )); |
526 | |
527 | // Add a compilation database to a subdirectory - CDB loaded. |
528 | FS.Files["foo/compile_flags.txt" ] = "-DFOO" ; |
529 | EXPECT_EQ(Root, lookupCDB(GDB, testPath("foo/test.cc" ), Stale)) |
530 | << "cache still valid" ; |
531 | auto Foo = lookupCDB(GDB, Path: testPath(File: "foo/test.cc" ), FreshTime: Fresh); |
532 | EXPECT_THAT(Foo, hasFlag("-DFOO" )) << "new cdb loaded" ; |
533 | EXPECT_EQ(Foo, lookupCDB(GDB, testPath("foo/test.cc" ), Stale)) |
534 | << "new cdb in cache" ; |
535 | |
536 | // Mtime changed, but no content change - CDB not reloaded. |
537 | ++FS.Timestamps["foo/compile_flags.txt" ]; |
538 | auto FooAgain = lookupCDB(GDB, Path: testPath(File: "foo/test.cc" ), FreshTime: Fresh); |
539 | EXPECT_EQ(Foo, FooAgain) << "Same content, read but not reloaded" ; |
540 | // Content changed, but not size or mtime - CDB not reloaded. |
541 | FS.Files["foo/compile_flags.txt" ] = "-DBAR" ; |
542 | auto FooAgain2 = lookupCDB(GDB, Path: testPath(File: "foo/test.cc" ), FreshTime: Fresh); |
543 | EXPECT_EQ(Foo, FooAgain2) << "Same filesize, change not detected" ; |
544 | // Mtime change forces a re-read, and we notice the different content. |
545 | ++FS.Timestamps["foo/compile_flags.txt" ]; |
546 | auto Bar = lookupCDB(GDB, Path: testPath(File: "foo/test.cc" ), FreshTime: Fresh); |
547 | EXPECT_THAT(Bar, hasFlag("-DBAR" )) << "refreshed with mtime change" ; |
548 | |
549 | // Size and content both change - CDB reloaded. |
550 | FS.Files["foo/compile_flags.txt" ] = "-DFOOBAR" ; |
551 | EXPECT_EQ(Bar, lookupCDB(GDB, testPath("foo/test.cc" ), Stale)) |
552 | << "cache still valid" ; |
553 | auto FooBar = lookupCDB(GDB, Path: testPath(File: "foo/test.cc" ), FreshTime: Fresh); |
554 | EXPECT_THAT(FooBar, hasFlag("-DFOOBAR" )) << "cdb reloaded" ; |
555 | |
556 | // compile_commands.json takes precedence over compile_flags.txt. |
557 | FS.Files["foo/compile_commands.json" ] = |
558 | llvm::formatv(Fmt: R"json([{ |
559 | "file": "{0}/foo/mock_file.cc", |
560 | "command": "clang -DBAZ mock_file.cc", |
561 | "directory": "{0}/foo", |
562 | }])json" , |
563 | Vals: llvm::sys::path::convert_to_slash(path: testRoot())); |
564 | EXPECT_EQ(FooBar, lookupCDB(GDB, testPath("foo/test.cc" ), Stale)) |
565 | << "cache still valid" ; |
566 | auto Baz = lookupCDB(GDB, Path: testPath(File: "foo/test.cc" ), FreshTime: Fresh); |
567 | EXPECT_THAT(Baz, hasFlag("-DBAZ" , testPath("foo/mock_file.cc" ))) |
568 | << "compile_commands overrides compile_flags" ; |
569 | |
570 | // Removing compile_commands.json reveals compile_flags.txt again. |
571 | // However this *does* cause a CDB reload (we cache only one CDB per dir). |
572 | FS.Files.erase(Key: "foo/compile_commands.json" ); |
573 | auto FoobarAgain = lookupCDB(GDB, Path: testPath(File: "foo/test.cc" ), FreshTime: Fresh); |
574 | EXPECT_THAT(FoobarAgain, hasFlag("-DFOOBAR" )) << "reloaded compile_flags" ; |
575 | EXPECT_NE(FoobarAgain, FooBar) << "CDB discarded (shadowed within directory)" ; |
576 | |
577 | // Removing the directory's CDB leaves the parent CDB active. |
578 | // The parent CDB is *not* reloaded (we cache the CDB per-directory). |
579 | FS.Files.erase(Key: "foo/compile_flags.txt" ); |
580 | EXPECT_EQ(Root, lookupCDB(GDB, testPath("foo/test.cc" ), Fresh)) |
581 | << "CDB retained (shadowed by another directory)" ; |
582 | } |
583 | |
584 | } // namespace clangd |
585 | } // namespace clang |
586 | |