Crafter.Build/tests/ConcurrentCacheRace/main.cpp

117 lines
4.1 KiB
C++
Raw Normal View History

#include <stdlib.h>
import std;
import Crafter.Build;
namespace fs = std::filesystem;
using namespace Crafter;
// Regression for the host-cache write race: when several crafter-build
// invocations shared the same XDG_CACHE_HOME, two LoadProject() calls would
// clobber each other's writes to `<cache>/<target>-<march>/std.pcm` and the
// `Crafter.Build-*.pcm` host modules. The loser's clang would then read a
// torn file via -fprebuilt-module-path and die with "malformed or corrupted
// precompiled file: 'can't skip to bit X from Y'". The fix routes every PCM
// write through a per-process temp + atomic rename so a reader always sees
// either the old or the new file, never a half-written one.
//
// LoadProject() is the right surface to test because it runs the full host
// bootstrap (BuildStdPcm + EnsureCrafterBuildPcms × the Crafter.Build-*
// modules), giving every concurrent thread enough writing-to-the-same-paths
// time for the race to surface. A pure BuildStdPcm test wouldn't — the std
// PCM write is too short to overlap reliably between threads.
namespace {
fs::path FindCrafterBuildHome() {
if (const char* env = std::getenv("CRAFTER_BUILD_HOME"); env && *env) {
return env;
}
for (const char* candidate : {
"/usr/local/share/crafter-build",
"/usr/share/crafter-build",
}) {
if (fs::exists(fs::path(candidate) / "Crafter.Build.cppm")) {
return candidate;
}
}
return {};
}
void WriteFile(const fs::path& p, std::string_view contents) {
std::ofstream out(p);
out << contents;
}
}
int main() {
fs::path home = FindCrafterBuildHome();
if (home.empty()) {
std::println(std::cerr,
"SKIP: no installed share/crafter-build found and CRAFTER_BUILD_HOME unset");
return 77;
}
setenv("CRAFTER_BUILD_HOME", home.string().c_str(), 1);
fs::path scratch = fs::temp_directory_path() / "crafter-build-concurrent-cache-race";
std::error_code ec;
fs::remove_all(scratch, ec);
fs::create_directories(scratch);
// Cold scratch cache. setenv before any thread starts so every thread's
// GetCacheDir() call sees the same override.
fs::path cacheDir = scratch / "cache";
fs::create_directories(cacheDir);
setenv("XDG_CACHE_HOME", cacheDir.string().c_str(), 1);
constexpr int N = 4;
std::vector<fs::path> projects;
for (int i = 0; i < N; ++i) {
fs::path dir = scratch / std::format("p{}", i);
fs::create_directories(dir);
WriteFile(dir / "project.cpp", std::format(
"import std;\n"
"import Crafter.Build;\n"
"namespace fs = std::filesystem;\n"
"using namespace Crafter;\n"
"extern \"C\" Configuration CrafterBuildProject(std::span<const std::string_view>) {{\n"
" Configuration cfg;\n"
" cfg.path = \"./\";\n"
" cfg.name = \"p{}\";\n"
" cfg.outputName = \"p{}\";\n"
" cfg.target = HostTarget();\n"
" cfg.type = ConfigurationType::Executable;\n"
" return cfg;\n"
"}}\n", i, i));
projects.push_back(dir / "project.cpp");
}
std::array<std::string, N> errors;
std::vector<std::thread> threads;
threads.reserve(N);
for (int i = 0; i < N; ++i) {
threads.emplace_back([&, i]() {
try {
std::array<std::string_view, 0> args = {};
(void)LoadProject(projects[i], args);
} catch (const std::exception& e) {
errors[i] = e.what();
}
});
}
for (std::thread& t : threads) t.join();
int failures = 0;
for (int i = 0; i < N; ++i) {
if (!errors[i].empty()) {
std::println(std::cerr, "FAIL p{}: {}", i, errors[i]);
++failures;
}
}
fs::remove_all(scratch, ec);
if (failures > 0) {
std::println(std::cerr,
"{}/{} concurrent LoadProject() invocations failed (host-cache race)",
failures, N);
return 1;
}
return 0;
}