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