xref: /llvm-project/clang-tools-extra/clangd/unittests/PrerequisiteModulesTest.cpp (revision 2b0e2255d6067872e844ff07d67342a6c97d8049)
1 //===--------------- PrerequisiteModulesTests.cpp -------------------*- C++
2 //-*-===//
3 //
4 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
5 // See https://llvm.org/LICENSE.txt for license information.
6 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
7 //
8 //===----------------------------------------------------------------------===//
9 
10 /// FIXME: Skip testing on windows temporarily due to the different escaping
11 /// code mode.
12 #ifndef _WIN32
13 
14 #include "Annotations.h"
15 #include "CodeComplete.h"
16 #include "Compiler.h"
17 #include "ModulesBuilder.h"
18 #include "ScanningProjectModules.h"
19 #include "TestTU.h"
20 #include "support/ThreadsafeFS.h"
21 #include "llvm/ADT/StringRef.h"
22 #include "llvm/Support/FileSystem.h"
23 #include "llvm/Support/raw_ostream.h"
24 #include "gmock/gmock.h"
25 #include "gtest/gtest.h"
26 
27 namespace clang::clangd {
28 namespace {
29 
30 class MockDirectoryCompilationDatabase : public MockCompilationDatabase {
31 public:
32   MockDirectoryCompilationDatabase(StringRef TestDir, const ThreadsafeFS &TFS)
33       : MockCompilationDatabase(TestDir),
34         MockedCDBPtr(std::make_shared<MockClangCompilationDatabase>(*this)),
35         TFS(TFS) {
36     this->ExtraClangFlags.push_back("-std=c++20");
37     this->ExtraClangFlags.push_back("-c");
38   }
39 
40   void addFile(llvm::StringRef Path, llvm::StringRef Contents);
41 
42   std::unique_ptr<ProjectModules> getProjectModules(PathRef) const override {
43     return scanningProjectModules(MockedCDBPtr, TFS);
44   }
45 
46 private:
47   class MockClangCompilationDatabase : public tooling::CompilationDatabase {
48   public:
49     MockClangCompilationDatabase(MockDirectoryCompilationDatabase &MCDB)
50         : MCDB(MCDB) {}
51 
52     std::vector<tooling::CompileCommand>
53     getCompileCommands(StringRef FilePath) const override {
54       std::optional<tooling::CompileCommand> Cmd =
55           MCDB.getCompileCommand(FilePath);
56       EXPECT_TRUE(Cmd);
57       return {*Cmd};
58     }
59 
60     std::vector<std::string> getAllFiles() const override { return Files; }
61 
62     void AddFile(StringRef File) { Files.push_back(File.str()); }
63 
64   private:
65     MockDirectoryCompilationDatabase &MCDB;
66     std::vector<std::string> Files;
67   };
68 
69   std::shared_ptr<MockClangCompilationDatabase> MockedCDBPtr;
70   const ThreadsafeFS &TFS;
71 };
72 
73 // Add files to the working testing directory and the compilation database.
74 void MockDirectoryCompilationDatabase::addFile(llvm::StringRef Path,
75                                                llvm::StringRef Contents) {
76   ASSERT_FALSE(llvm::sys::path::is_absolute(Path));
77 
78   SmallString<256> AbsPath(Directory);
79   llvm::sys::path::append(AbsPath, Path);
80 
81   ASSERT_FALSE(
82       llvm::sys::fs::create_directories(llvm::sys::path::parent_path(AbsPath)));
83 
84   std::error_code EC;
85   llvm::raw_fd_ostream OS(AbsPath, EC);
86   ASSERT_FALSE(EC);
87   OS << Contents;
88 
89   MockedCDBPtr->AddFile(Path);
90 }
91 
92 class PrerequisiteModulesTests : public ::testing::Test {
93 protected:
94   void SetUp() override {
95     ASSERT_FALSE(llvm::sys::fs::createUniqueDirectory("modules-test", TestDir));
96   }
97 
98   void TearDown() override {
99     ASSERT_FALSE(llvm::sys::fs::remove_directories(TestDir));
100   }
101 
102 public:
103   // Get the absolute path for file specified by Path under testing working
104   // directory.
105   std::string getFullPath(llvm::StringRef Path) {
106     SmallString<128> Result(TestDir);
107     llvm::sys::path::append(Result, Path);
108     EXPECT_TRUE(llvm::sys::fs::exists(Result.str()));
109     return Result.str().str();
110   }
111 
112   ParseInputs getInputs(llvm::StringRef FileName,
113                         const GlobalCompilationDatabase &CDB) {
114     std::string FullPathName = getFullPath(FileName);
115 
116     ParseInputs Inputs;
117     std::optional<tooling::CompileCommand> Cmd =
118         CDB.getCompileCommand(FullPathName);
119     EXPECT_TRUE(Cmd);
120     Inputs.CompileCommand = std::move(*Cmd);
121     Inputs.TFS = &FS;
122 
123     if (auto Contents = FS.view(TestDir)->getBufferForFile(FullPathName))
124       Inputs.Contents = Contents->get()->getBuffer().str();
125 
126     return Inputs;
127   }
128 
129   SmallString<256> TestDir;
130   // FIXME: It will be better to use the MockFS if the scanning process and
131   // build module process doesn't depend on reading real IO.
132   RealThreadsafeFS FS;
133 
134   DiagnosticConsumer DiagConsumer;
135 };
136 
137 TEST_F(PrerequisiteModulesTests, NonModularTest) {
138   MockDirectoryCompilationDatabase CDB(TestDir, FS);
139 
140   CDB.addFile("foo.h", R"cpp(
141 inline void foo() {}
142   )cpp");
143 
144   CDB.addFile("NonModular.cpp", R"cpp(
145 #include "foo.h"
146 void use() {
147   foo();
148 }
149   )cpp");
150 
151   ModulesBuilder Builder(CDB);
152 
153   // NonModular.cpp is not related to modules. So nothing should be built.
154   auto NonModularInfo =
155       Builder.buildPrerequisiteModulesFor(getFullPath("NonModular.cpp"), FS);
156   EXPECT_TRUE(NonModularInfo);
157 
158   HeaderSearchOptions HSOpts;
159   NonModularInfo->adjustHeaderSearchOptions(HSOpts);
160   EXPECT_TRUE(HSOpts.PrebuiltModuleFiles.empty());
161 
162   auto Invocation =
163       buildCompilerInvocation(getInputs("NonModular.cpp", CDB), DiagConsumer);
164   EXPECT_TRUE(NonModularInfo->canReuse(*Invocation, FS.view(TestDir)));
165 }
166 
167 TEST_F(PrerequisiteModulesTests, ModuleWithoutDepTest) {
168   MockDirectoryCompilationDatabase CDB(TestDir, FS);
169 
170   CDB.addFile("foo.h", R"cpp(
171 inline void foo() {}
172   )cpp");
173 
174   CDB.addFile("M.cppm", R"cpp(
175 module;
176 #include "foo.h"
177 export module M;
178   )cpp");
179 
180   ModulesBuilder Builder(CDB);
181 
182   auto MInfo = Builder.buildPrerequisiteModulesFor(getFullPath("M.cppm"), FS);
183   EXPECT_TRUE(MInfo);
184 
185   // Nothing should be built since M doesn't dependent on anything.
186   HeaderSearchOptions HSOpts;
187   MInfo->adjustHeaderSearchOptions(HSOpts);
188   EXPECT_TRUE(HSOpts.PrebuiltModuleFiles.empty());
189 
190   auto Invocation =
191       buildCompilerInvocation(getInputs("M.cppm", CDB), DiagConsumer);
192   EXPECT_TRUE(MInfo->canReuse(*Invocation, FS.view(TestDir)));
193 }
194 
195 TEST_F(PrerequisiteModulesTests, ModuleWithArgumentPatch) {
196   MockDirectoryCompilationDatabase CDB(TestDir, FS);
197 
198   CDB.ExtraClangFlags.push_back("-invalid-unknown-flag");
199 
200   CDB.addFile("Dep.cppm", R"cpp(
201 export module Dep;
202   )cpp");
203 
204   CDB.addFile("M.cppm", R"cpp(
205 export module M;
206 import Dep;
207   )cpp");
208 
209   // An invalid flag will break the module compilation and the
210   // getRequiredModules would return an empty array
211   auto ProjectModules = CDB.getProjectModules(getFullPath("M.cppm"));
212   EXPECT_TRUE(
213       ProjectModules->getRequiredModules(getFullPath("M.cppm")).empty());
214 
215   // Set the mangler to filter out the invalid flag
216   ProjectModules->setCommandMangler(
217       [](tooling::CompileCommand &Command, PathRef) {
218         auto const It =
219             std::find(Command.CommandLine.begin(), Command.CommandLine.end(),
220                       "-invalid-unknown-flag");
221         Command.CommandLine.erase(It);
222       });
223 
224   // And now it returns a non-empty list of required modules since the
225   // compilation succeeded
226   EXPECT_FALSE(
227       ProjectModules->getRequiredModules(getFullPath("M.cppm")).empty());
228 }
229 
230 TEST_F(PrerequisiteModulesTests, ModuleWithDepTest) {
231   MockDirectoryCompilationDatabase CDB(TestDir, FS);
232 
233   CDB.addFile("foo.h", R"cpp(
234 inline void foo() {}
235   )cpp");
236 
237   CDB.addFile("M.cppm", R"cpp(
238 module;
239 #include "foo.h"
240 export module M;
241   )cpp");
242 
243   CDB.addFile("N.cppm", R"cpp(
244 export module N;
245 import :Part;
246 import M;
247   )cpp");
248 
249   CDB.addFile("N-part.cppm", R"cpp(
250 // Different module name with filename intentionally.
251 export module N:Part;
252   )cpp");
253 
254   ModulesBuilder Builder(CDB);
255 
256   auto NInfo = Builder.buildPrerequisiteModulesFor(getFullPath("N.cppm"), FS);
257   EXPECT_TRUE(NInfo);
258 
259   ParseInputs NInput = getInputs("N.cppm", CDB);
260   std::unique_ptr<CompilerInvocation> Invocation =
261       buildCompilerInvocation(NInput, DiagConsumer);
262   // Test that `PrerequisiteModules::canReuse` works basically.
263   EXPECT_TRUE(NInfo->canReuse(*Invocation, FS.view(TestDir)));
264 
265   {
266     // Check that
267     // `PrerequisiteModules::adjustHeaderSearchOptions(HeaderSearchOptions&)`
268     // can appending HeaderSearchOptions correctly.
269     HeaderSearchOptions HSOpts;
270     NInfo->adjustHeaderSearchOptions(HSOpts);
271 
272     EXPECT_TRUE(HSOpts.PrebuiltModuleFiles.count("M"));
273     EXPECT_TRUE(HSOpts.PrebuiltModuleFiles.count("N:Part"));
274   }
275 
276   {
277     // Check that
278     // `PrerequisiteModules::adjustHeaderSearchOptions(HeaderSearchOptions&)`
279     // can replace HeaderSearchOptions correctly.
280     HeaderSearchOptions HSOpts;
281     HSOpts.PrebuiltModuleFiles["M"] = "incorrect_path";
282     HSOpts.PrebuiltModuleFiles["N:Part"] = "incorrect_path";
283     NInfo->adjustHeaderSearchOptions(HSOpts);
284 
285     EXPECT_TRUE(StringRef(HSOpts.PrebuiltModuleFiles["M"]).ends_with(".pcm"));
286     EXPECT_TRUE(
287         StringRef(HSOpts.PrebuiltModuleFiles["N:Part"]).ends_with(".pcm"));
288   }
289 }
290 
291 TEST_F(PrerequisiteModulesTests, ReusabilityTest) {
292   MockDirectoryCompilationDatabase CDB(TestDir, FS);
293 
294   CDB.addFile("foo.h", R"cpp(
295 inline void foo() {}
296   )cpp");
297 
298   CDB.addFile("M.cppm", R"cpp(
299 module;
300 #include "foo.h"
301 export module M;
302   )cpp");
303 
304   CDB.addFile("N.cppm", R"cpp(
305 export module N;
306 import :Part;
307 import M;
308   )cpp");
309 
310   CDB.addFile("N-part.cppm", R"cpp(
311 // Different module name with filename intentionally.
312 export module N:Part;
313   )cpp");
314 
315   ModulesBuilder Builder(CDB);
316 
317   auto NInfo = Builder.buildPrerequisiteModulesFor(getFullPath("N.cppm"), FS);
318   EXPECT_TRUE(NInfo);
319   EXPECT_TRUE(NInfo);
320 
321   ParseInputs NInput = getInputs("N.cppm", CDB);
322   std::unique_ptr<CompilerInvocation> Invocation =
323       buildCompilerInvocation(NInput, DiagConsumer);
324   EXPECT_TRUE(NInfo->canReuse(*Invocation, FS.view(TestDir)));
325 
326   // Test that we can still reuse the NInfo after we touch a unrelated file.
327   {
328     CDB.addFile("L.cppm", R"cpp(
329 module;
330 #include "foo.h"
331 export module L;
332 export int ll = 43;
333   )cpp");
334     EXPECT_TRUE(NInfo->canReuse(*Invocation, FS.view(TestDir)));
335 
336     CDB.addFile("bar.h", R"cpp(
337 inline void bar() {}
338 inline void bar(int) {}
339   )cpp");
340     EXPECT_TRUE(NInfo->canReuse(*Invocation, FS.view(TestDir)));
341   }
342 
343   // Test that we can't reuse the NInfo after we touch a related file.
344   {
345     CDB.addFile("M.cppm", R"cpp(
346 module;
347 #include "foo.h"
348 export module M;
349 export int mm = 44;
350   )cpp");
351     EXPECT_FALSE(NInfo->canReuse(*Invocation, FS.view(TestDir)));
352 
353     NInfo = Builder.buildPrerequisiteModulesFor(getFullPath("N.cppm"), FS);
354     EXPECT_TRUE(NInfo->canReuse(*Invocation, FS.view(TestDir)));
355 
356     CDB.addFile("foo.h", R"cpp(
357 inline void foo() {}
358 inline void foo(int) {}
359   )cpp");
360     EXPECT_FALSE(NInfo->canReuse(*Invocation, FS.view(TestDir)));
361 
362     NInfo = Builder.buildPrerequisiteModulesFor(getFullPath("N.cppm"), FS);
363     EXPECT_TRUE(NInfo->canReuse(*Invocation, FS.view(TestDir)));
364   }
365 
366   CDB.addFile("N-part.cppm", R"cpp(
367 export module N:Part;
368 // Intentioned to make it uncompilable.
369 export int NPart = 4LIdjwldijaw
370   )cpp");
371   EXPECT_FALSE(NInfo->canReuse(*Invocation, FS.view(TestDir)));
372   NInfo = Builder.buildPrerequisiteModulesFor(getFullPath("N.cppm"), FS);
373   EXPECT_TRUE(NInfo);
374   EXPECT_FALSE(NInfo->canReuse(*Invocation, FS.view(TestDir)));
375 
376   CDB.addFile("N-part.cppm", R"cpp(
377 export module N:Part;
378 export int NPart = 43;
379   )cpp");
380   EXPECT_TRUE(NInfo);
381   EXPECT_FALSE(NInfo->canReuse(*Invocation, FS.view(TestDir)));
382   NInfo = Builder.buildPrerequisiteModulesFor(getFullPath("N.cppm"), FS);
383   EXPECT_TRUE(NInfo);
384   EXPECT_TRUE(NInfo->canReuse(*Invocation, FS.view(TestDir)));
385 
386   // Test that if we changed the modification time of the file, the module files
387   // info is still reusable if its content doesn't change.
388   CDB.addFile("N-part.cppm", R"cpp(
389 export module N:Part;
390 export int NPart = 43;
391   )cpp");
392   EXPECT_TRUE(NInfo->canReuse(*Invocation, FS.view(TestDir)));
393 
394   CDB.addFile("N.cppm", R"cpp(
395 export module N;
396 import :Part;
397 import M;
398 
399 export int nn = 43;
400   )cpp");
401   // NInfo should be reusable after we change its content.
402   EXPECT_TRUE(NInfo->canReuse(*Invocation, FS.view(TestDir)));
403 }
404 
405 // An End-to-End test for modules.
406 TEST_F(PrerequisiteModulesTests, ParsedASTTest) {
407   MockDirectoryCompilationDatabase CDB(TestDir, FS);
408 
409   CDB.addFile("A.cppm", R"cpp(
410 export module A;
411 export void printA();
412   )cpp");
413 
414   CDB.addFile("Use.cpp", R"cpp(
415 import A;
416 )cpp");
417 
418   ModulesBuilder Builder(CDB);
419 
420   ParseInputs Use = getInputs("Use.cpp", CDB);
421   Use.ModulesManager = &Builder;
422 
423   std::unique_ptr<CompilerInvocation> CI =
424       buildCompilerInvocation(Use, DiagConsumer);
425   EXPECT_TRUE(CI);
426 
427   auto Preamble =
428       buildPreamble(getFullPath("Use.cpp"), *CI, Use, /*InMemory=*/true,
429                     /*Callback=*/nullptr);
430   EXPECT_TRUE(Preamble);
431   EXPECT_TRUE(Preamble->RequiredModules);
432 
433   auto AST = ParsedAST::build(getFullPath("Use.cpp"), Use, std::move(CI), {},
434                               Preamble);
435   EXPECT_TRUE(AST);
436 
437   const NamedDecl &D = findDecl(*AST, "printA");
438   EXPECT_TRUE(D.isFromASTFile());
439 }
440 
441 // An end to end test for code complete in modules
442 TEST_F(PrerequisiteModulesTests, CodeCompleteTest) {
443   MockDirectoryCompilationDatabase CDB(TestDir, FS);
444 
445   CDB.addFile("A.cppm", R"cpp(
446 export module A;
447 export void printA();
448   )cpp");
449 
450   llvm::StringLiteral UserContents = R"cpp(
451 import A;
452 void func() {
453   print^
454 }
455 )cpp";
456 
457   CDB.addFile("Use.cpp", UserContents);
458   Annotations Test(UserContents);
459 
460   ModulesBuilder Builder(CDB);
461 
462   ParseInputs Use = getInputs("Use.cpp", CDB);
463   Use.ModulesManager = &Builder;
464 
465   std::unique_ptr<CompilerInvocation> CI =
466       buildCompilerInvocation(Use, DiagConsumer);
467   EXPECT_TRUE(CI);
468 
469   auto Preamble =
470       buildPreamble(getFullPath("Use.cpp"), *CI, Use, /*InMemory=*/true,
471                     /*Callback=*/nullptr);
472   EXPECT_TRUE(Preamble);
473   EXPECT_TRUE(Preamble->RequiredModules);
474 
475   auto Result = codeComplete(getFullPath("Use.cpp"), Test.point(),
476                              Preamble.get(), Use, {});
477   EXPECT_FALSE(Result.Completions.empty());
478   EXPECT_EQ(Result.Completions[0].Name, "printA");
479 }
480 
481 TEST_F(PrerequisiteModulesTests, SignatureHelpTest) {
482   MockDirectoryCompilationDatabase CDB(TestDir, FS);
483 
484   CDB.addFile("A.cppm", R"cpp(
485 export module A;
486 export void printA(int a);
487   )cpp");
488 
489   llvm::StringLiteral UserContents = R"cpp(
490 import A;
491 void func() {
492   printA(^);
493 }
494 )cpp";
495 
496   CDB.addFile("Use.cpp", UserContents);
497   Annotations Test(UserContents);
498 
499   ModulesBuilder Builder(CDB);
500 
501   ParseInputs Use = getInputs("Use.cpp", CDB);
502   Use.ModulesManager = &Builder;
503 
504   std::unique_ptr<CompilerInvocation> CI =
505       buildCompilerInvocation(Use, DiagConsumer);
506   EXPECT_TRUE(CI);
507 
508   auto Preamble =
509       buildPreamble(getFullPath("Use.cpp"), *CI, Use, /*InMemory=*/true,
510                     /*Callback=*/nullptr);
511   EXPECT_TRUE(Preamble);
512   EXPECT_TRUE(Preamble->RequiredModules);
513 
514   auto Result = signatureHelp(getFullPath("Use.cpp"), Test.point(),
515                               *Preamble.get(), Use, MarkupKind::PlainText);
516   EXPECT_FALSE(Result.signatures.empty());
517   EXPECT_EQ(Result.signatures[0].label, "printA(int a) -> void");
518   EXPECT_EQ(Result.signatures[0].parameters[0].labelString, "int a");
519 }
520 
521 TEST_F(PrerequisiteModulesTests, ReusablePrerequisiteModulesTest) {
522   MockDirectoryCompilationDatabase CDB(TestDir, FS);
523 
524   CDB.addFile("M.cppm", R"cpp(
525 export module M;
526 export int M = 43;
527   )cpp");
528   CDB.addFile("A.cppm", R"cpp(
529 export module A;
530 import M;
531 export int A = 43 + M;
532   )cpp");
533   CDB.addFile("B.cppm", R"cpp(
534 export module B;
535 import M;
536 export int B = 44 + M;
537   )cpp");
538 
539   ModulesBuilder Builder(CDB);
540 
541   auto AInfo = Builder.buildPrerequisiteModulesFor(getFullPath("A.cppm"), FS);
542   EXPECT_TRUE(AInfo);
543   auto BInfo = Builder.buildPrerequisiteModulesFor(getFullPath("B.cppm"), FS);
544   EXPECT_TRUE(BInfo);
545   HeaderSearchOptions HSOptsA(TestDir);
546   HeaderSearchOptions HSOptsB(TestDir);
547   AInfo->adjustHeaderSearchOptions(HSOptsA);
548   BInfo->adjustHeaderSearchOptions(HSOptsB);
549 
550   EXPECT_FALSE(HSOptsA.PrebuiltModuleFiles.empty());
551   EXPECT_FALSE(HSOptsB.PrebuiltModuleFiles.empty());
552 
553   // Check that we're reusing the module files.
554   EXPECT_EQ(HSOptsA.PrebuiltModuleFiles, HSOptsB.PrebuiltModuleFiles);
555 
556   // Update M.cppm to check if the modules builder can update correctly.
557   CDB.addFile("M.cppm", R"cpp(
558 export module M;
559 export constexpr int M = 43;
560   )cpp");
561 
562   ParseInputs AUse = getInputs("A.cppm", CDB);
563   AUse.ModulesManager = &Builder;
564   std::unique_ptr<CompilerInvocation> AInvocation =
565       buildCompilerInvocation(AUse, DiagConsumer);
566   EXPECT_FALSE(AInfo->canReuse(*AInvocation, FS.view(TestDir)));
567 
568   ParseInputs BUse = getInputs("B.cppm", CDB);
569   AUse.ModulesManager = &Builder;
570   std::unique_ptr<CompilerInvocation> BInvocation =
571       buildCompilerInvocation(BUse, DiagConsumer);
572   EXPECT_FALSE(BInfo->canReuse(*BInvocation, FS.view(TestDir)));
573 
574   auto NewAInfo =
575       Builder.buildPrerequisiteModulesFor(getFullPath("A.cppm"), FS);
576   auto NewBInfo =
577       Builder.buildPrerequisiteModulesFor(getFullPath("B.cppm"), FS);
578   EXPECT_TRUE(NewAInfo);
579   EXPECT_TRUE(NewBInfo);
580   HeaderSearchOptions NewHSOptsA(TestDir);
581   HeaderSearchOptions NewHSOptsB(TestDir);
582   NewAInfo->adjustHeaderSearchOptions(NewHSOptsA);
583   NewBInfo->adjustHeaderSearchOptions(NewHSOptsB);
584 
585   EXPECT_FALSE(NewHSOptsA.PrebuiltModuleFiles.empty());
586   EXPECT_FALSE(NewHSOptsB.PrebuiltModuleFiles.empty());
587 
588   EXPECT_EQ(NewHSOptsA.PrebuiltModuleFiles, NewHSOptsB.PrebuiltModuleFiles);
589   // Check that we didn't reuse the old and stale module files.
590   EXPECT_NE(NewHSOptsA.PrebuiltModuleFiles, HSOptsA.PrebuiltModuleFiles);
591 }
592 
593 } // namespace
594 } // namespace clang::clangd
595 
596 #endif
597