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