mirror-llvm-project/clang/unittests/Serialization/ModuleCacheTest.cpp
Qiongsi Wu 202cb24d24
[clang][Modules] Adding Specific Diagnostics when a Finalized PCM is Out-of-date due to Input File Changes (#209857)
We have good diagnostics coverage when a pcm cannot be dropped in
`ASTReader::readASTCore`, because we call `canRecoverFromOutOfDate` to
check if the pcm is finalized or not. This check is missing for input
file changes.

This PR fixes the diagnostics, so we do not fall to the generic error
`err_module_rebuild_finalized`, which is hard to reason and act on.
2026-07-16 10:39:15 -07:00

298 lines
11 KiB
C++

//===- unittests/Serialization/ModuleCacheTest.cpp - CI tests -------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
#include "clang/Basic/AllDiagnostics.h"
#include "clang/Basic/FileManager.h"
#include "clang/Driver/CreateInvocationFromArgs.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Frontend/CompilerInvocation.h"
#include "clang/Frontend/FrontendActions.h"
#include "clang/Frontend/Utils.h"
#include "clang/Lex/HeaderSearch.h"
#include "llvm/ADT/SmallString.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/VirtualFileSystem.h"
#include "llvm/Support/raw_ostream.h"
#include "gtest/gtest.h"
using namespace llvm;
using namespace clang;
namespace {
class ModuleCacheTest : public ::testing::Test {
void SetUp() override {
ASSERT_FALSE(sys::fs::createUniqueDirectory("modulecache-test", TestDir));
ModuleCachePath = SmallString<256>(TestDir);
sys::path::append(ModuleCachePath, "mcp");
ASSERT_FALSE(sys::fs::create_directories(ModuleCachePath));
}
void TearDown() override { sys::fs::remove_directories(TestDir); }
public:
SmallString<256> TestDir;
SmallString<256> ModuleCachePath;
void addFile(StringRef Path, StringRef Contents) {
ASSERT_FALSE(sys::path::is_absolute(Path));
SmallString<256> AbsPath(TestDir);
sys::path::append(AbsPath, Path);
std::error_code EC;
ASSERT_FALSE(
sys::fs::create_directories(llvm::sys::path::parent_path(AbsPath)));
llvm::raw_fd_ostream OS(AbsPath, EC);
ASSERT_FALSE(EC);
OS << Contents;
}
void addDuplicateFrameworks() {
addFile("test.m", R"cpp(
@import Top;
)cpp");
addFile("frameworks/Top.framework/Headers/top.h", R"cpp(
@import M;
)cpp");
addFile("frameworks/Top.framework/Modules/module.modulemap", R"cpp(
framework module Top [system] {
header "top.h"
export *
}
)cpp");
addFile("frameworks/M.framework/Headers/m.h", R"cpp(
void foo();
)cpp");
addFile("frameworks/M.framework/Modules/module.modulemap", R"cpp(
framework module M [system] {
header "m.h"
export *
}
)cpp");
addFile("frameworks2/M.framework/Headers/m.h", R"cpp(
void foo();
)cpp");
addFile("frameworks2/M.framework/Modules/module.modulemap", R"cpp(
framework module M [system] {
header "m.h"
export *
}
)cpp");
}
std::unique_ptr<CompilerInvocation>
createInvocationAndEnableFree(ArrayRef<const char *> Args,
CreateInvocationOptions Opts) {
std::unique_ptr<CompilerInvocation> Invocation =
createInvocation(Args, Opts);
if (Invocation)
Invocation->getFrontendOpts().DisableFree = false;
return Invocation;
}
};
TEST_F(ModuleCacheTest, CachedModuleNewPath) {
addDuplicateFrameworks();
SmallString<256> MCPArg("-fmodules-cache-path=");
MCPArg.append(ModuleCachePath);
CreateInvocationOptions CIOpts;
CIOpts.VFS = llvm::vfs::createPhysicalFileSystem();
DiagnosticOptions DiagOpts;
IntrusiveRefCntPtr<DiagnosticsEngine> Diags =
CompilerInstance::createDiagnostics(*CIOpts.VFS, DiagOpts);
CIOpts.Diags = Diags;
// First run should pass with no errors
const char *Args[] = {"clang", "-fmodules", "-Fframeworks",
MCPArg.c_str(), "-working-directory", TestDir.c_str(),
"test.m"};
std::shared_ptr<CompilerInvocation> Invocation =
createInvocationAndEnableFree(Args, CIOpts);
ASSERT_TRUE(Invocation);
CompilerInstance Instance(std::move(Invocation));
Instance.setVirtualFileSystem(CIOpts.VFS);
Instance.setDiagnostics(Diags);
SyntaxOnlyAction Action;
ASSERT_TRUE(Instance.ExecuteAction(Action));
ASSERT_FALSE(Diags->hasErrorOccurred());
// Now add `frameworks2` to the search path. `Top.pcm` will have a reference
// to the `M` from `frameworks`, but a search will find the `M` from
// `frameworks2` - causing a mismatch and it to be considered out of date.
//
// Normally this would be fine - `M` and the modules it depends on would be
// rebuilt. However, since we have a shared module cache and thus an already
// finalized `Top`, recompiling `Top` will cause the existing module to be
// removed from the cache, causing possible crashed if it is ever used.
//
// Make sure that an error occurs instead.
const char *Args2[] = {"clang", "-fmodules", "-Fframeworks2",
"-Fframeworks", MCPArg.c_str(), "-working-directory",
TestDir.c_str(), "test.m"};
std::shared_ptr<CompilerInvocation> Invocation2 =
createInvocationAndEnableFree(Args2, CIOpts);
ASSERT_TRUE(Invocation2);
CompilerInstance Instance2(std::move(Invocation2),
Instance.getPCHContainerOperations(),
Instance.getModuleCachePtr());
Instance2.setVirtualFileSystem(CIOpts.VFS);
Instance2.setDiagnostics(Diags);
SyntaxOnlyAction Action2;
ASSERT_FALSE(Instance2.ExecuteAction(Action2));
ASSERT_TRUE(Diags->hasErrorOccurred());
}
TEST_F(ModuleCacheTest, CachedModuleNewPathAllowErrors) {
addDuplicateFrameworks();
SmallString<256> MCPArg("-fmodules-cache-path=");
MCPArg.append(ModuleCachePath);
CreateInvocationOptions CIOpts;
CIOpts.VFS = llvm::vfs::createPhysicalFileSystem();
DiagnosticOptions DiagOpts;
IntrusiveRefCntPtr<DiagnosticsEngine> Diags =
CompilerInstance::createDiagnostics(*CIOpts.VFS, DiagOpts);
CIOpts.Diags = Diags;
// First run should pass with no errors
const char *Args[] = {"clang", "-fmodules", "-Fframeworks",
MCPArg.c_str(), "-working-directory", TestDir.c_str(),
"test.m"};
std::shared_ptr<CompilerInvocation> Invocation =
createInvocationAndEnableFree(Args, CIOpts);
ASSERT_TRUE(Invocation);
CompilerInstance Instance(std::move(Invocation));
Instance.setVirtualFileSystem(CIOpts.VFS);
Instance.setDiagnostics(Diags);
SyntaxOnlyAction Action;
ASSERT_TRUE(Instance.ExecuteAction(Action));
ASSERT_FALSE(Diags->hasErrorOccurred());
// Same as `CachedModuleNewPath` but while allowing errors. This is a hard
// failure where the module wasn't created, so it should still fail.
const char *Args2[] = {
"clang", "-fmodules", "-Fframeworks2",
"-Fframeworks", MCPArg.c_str(), "-working-directory",
TestDir.c_str(), "-Xclang", "-fallow-pcm-with-compiler-errors",
"test.m"};
std::shared_ptr<CompilerInvocation> Invocation2 =
createInvocationAndEnableFree(Args2, CIOpts);
ASSERT_TRUE(Invocation2);
CompilerInstance Instance2(std::move(Invocation2),
Instance.getPCHContainerOperations(),
Instance.getModuleCachePtr());
Instance2.setVirtualFileSystem(CIOpts.VFS);
Instance2.setDiagnostics(Diags);
SyntaxOnlyAction Action2;
ASSERT_FALSE(Instance2.ExecuteAction(Action2));
ASSERT_TRUE(Diags->hasErrorOccurred());
}
struct CapturedDiag {
unsigned ID;
std::string Message;
};
class CapturedDiagConsumer : public DiagnosticConsumer {
std::vector<CapturedDiag> &Diags;
public:
CapturedDiagConsumer(std::vector<CapturedDiag> &D) : Diags(D) {}
void HandleDiagnostic(DiagnosticsEngine::Level Level,
const Diagnostic &Info) override {
DiagnosticConsumer::HandleDiagnostic(Level, Info);
SmallString<128> Msg;
Info.FormatDiagnostic(Msg);
Diags.push_back({Info.getID(), std::string(Msg)});
}
};
TEST_F(ModuleCacheTest, RebuildFinalizedModuleAfterInputChange) {
addFile("test.m", R"cpp(
@import M;
)cpp");
addFile("frameworks/M.framework/Headers/m.h", R"cpp(
void foo(void);
)cpp");
addFile("frameworks/M.framework/Modules/module.modulemap", R"cpp(
framework module M [system] {
header "m.h"
export *
}
)cpp");
SmallString<256> MCPArg("-fmodules-cache-path=");
MCPArg.append(ModuleCachePath);
CreateInvocationOptions CIOpts;
CIOpts.VFS = llvm::vfs::createPhysicalFileSystem();
DiagnosticOptions DiagOpts;
IntrusiveRefCntPtr<DiagnosticsEngine> Diags =
CompilerInstance::createDiagnostics(*CIOpts.VFS, DiagOpts);
CIOpts.Diags = Diags;
const char *Args[] = {"clang", "-fmodules", "-Fframeworks",
MCPArg.c_str(), "-working-directory", TestDir.c_str(),
"test.m"};
// Run 1: builds, loads, and finalizes M in the shared in-memory cache.
std::shared_ptr<CompilerInvocation> Invocation =
createInvocationAndEnableFree(Args, CIOpts);
ASSERT_TRUE(Invocation);
CompilerInstance Instance(std::move(Invocation));
Instance.setVirtualFileSystem(CIOpts.VFS);
Instance.setDiagnostics(Diags);
SyntaxOnlyAction Action;
ASSERT_TRUE(Instance.ExecuteAction(Action));
ASSERT_FALSE(Diags->hasErrorOccurred());
// Grow m.h so its recorded size no longer matches, marking M out of date on
// the next load. Because M is finalized in the shared cache, it can be
// neither dropped nor rebuilt.
addFile("frameworks/M.framework/Headers/m.h", R"cpp(
void foo(void);
void bar(void);
void baz(void);
)cpp");
std::vector<CapturedDiag> Captured;
Diags->setClient(new CapturedDiagConsumer(Captured),
/*ShouldOwnClient=*/true);
// Run 2 shares the module cache (and thus the finalized M).
std::shared_ptr<CompilerInvocation> Invocation2 =
createInvocationAndEnableFree(Args, CIOpts);
ASSERT_TRUE(Invocation2);
CompilerInstance Instance2(std::move(Invocation2),
Instance.getPCHContainerOperations(),
Instance.getModuleCachePtr());
Instance2.setVirtualFileSystem(CIOpts.VFS);
Instance2.setDiagnostics(Diags);
SyntaxOnlyAction Action2;
ASSERT_FALSE(Instance2.ExecuteAction(Action2));
ASSERT_TRUE(Diags->hasErrorOccurred());
ASSERT_EQ(Captured.size(), 4u);
EXPECT_EQ(Captured[0].ID, (unsigned)diag::err_fe_ast_file_modified);
EXPECT_NE(Captured[0].Message.find("m.h"), std::string::npos);
EXPECT_EQ(Captured[1].ID, (unsigned)diag::note_fe_ast_file_modified);
EXPECT_EQ(Captured[2].ID,
(unsigned)diag::note_fe_ast_file_modified_finalized);
EXPECT_NE(Captured[2].Message.find("'M'"), std::string::npos);
EXPECT_EQ(Captured[3].ID,
(unsigned)diag::note_ast_file_input_files_validation_status);
}
} // anonymous namespace