137 lines
5.7 KiB
C++
137 lines
5.7 KiB
C++
|
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||
|
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||
|
|
|
||
|
|
import std;
|
||
|
|
import Crafter.Build;
|
||
|
|
namespace fs = std::filesystem;
|
||
|
|
using namespace Crafter;
|
||
|
|
|
||
|
|
// A project whose tests each copy its externalDependencies gives every test
|
||
|
|
// configuration the same unpinned dependency, and `crafter-build test`
|
||
|
|
// builds those configurations side by side. Each of them pulled the shared
|
||
|
|
// clone: at first all at once, which broke the clone's FETCH_HEAD and failed
|
||
|
|
// every test (issue #34), and then, under the clone's lock, one after
|
||
|
|
// another, a network round trip per test. The configurations of one build
|
||
|
|
// pass now share a single fetch and build of the dependency, and the next
|
||
|
|
// pass fetches it again.
|
||
|
|
//
|
||
|
|
// Eight tests that include a header from the same tracked branch of a local
|
||
|
|
// repository run through RunTests at once, and again after the branch has
|
||
|
|
// moved. GIT_TRACE logs every git process, and every fetch of the
|
||
|
|
// repository, clone or pull, runs `git upload-pack` in it once. Everything
|
||
|
|
// is local; XDG_CACHE_HOME keeps the clone out of the real cache.
|
||
|
|
|
||
|
|
extern "C" int setenv(const char* name, const char* value, int overwrite);
|
||
|
|
|
||
|
|
namespace {
|
||
|
|
std::int32_t failures = 0;
|
||
|
|
|
||
|
|
void Check(bool cond, std::string_view msg) {
|
||
|
|
if (!cond) {
|
||
|
|
std::println(std::cerr, "FAIL: {}", msg);
|
||
|
|
++failures;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void Git(const fs::path& dir, std::string_view args) {
|
||
|
|
const std::string command = std::format("git -C {} {}", ShellQuote(dir.string()), args);
|
||
|
|
auto result = RunCommandWithTimeout(command, std::chrono::seconds(60));
|
||
|
|
if (result.exitCode != 0) throw std::runtime_error(std::format("{} failed: {}", command, result.output));
|
||
|
|
}
|
||
|
|
|
||
|
|
void Write(const fs::path& file, std::string_view text) {
|
||
|
|
std::ofstream(file, std::ios::binary | std::ios::trunc) << text;
|
||
|
|
}
|
||
|
|
|
||
|
|
void Commit(const fs::path& repo, std::uint32_t round) {
|
||
|
|
Write(repo / "round.h", std::format("#define ROUND {}\n", round));
|
||
|
|
Git(repo, "add round.h");
|
||
|
|
Git(repo, std::format("commit -q -m round-{}", round));
|
||
|
|
}
|
||
|
|
|
||
|
|
std::uint32_t Fetches(const fs::path& trace) {
|
||
|
|
std::ifstream in(trace);
|
||
|
|
std::uint32_t count = 0;
|
||
|
|
for (std::string line; std::getline(in, line);) {
|
||
|
|
if (line.contains("trace: built-in: git upload-pack")) ++count;
|
||
|
|
}
|
||
|
|
return count;
|
||
|
|
}
|
||
|
|
|
||
|
|
// The first test that did not pass, for the message.
|
||
|
|
std::string FirstFailure(const TestSummary& summary) {
|
||
|
|
for (const TestResult& result : summary.results) {
|
||
|
|
if (result.outcome != TestOutcome::Pass) return std::format(", the first: {}: {}", result.name, result.output);
|
||
|
|
}
|
||
|
|
return "";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
int main() {
|
||
|
|
const fs::path scratch = fs::temp_directory_path() / "crafter-build-shared-external-fetch";
|
||
|
|
fs::remove_all(scratch);
|
||
|
|
fs::create_directories(scratch);
|
||
|
|
setenv("XDG_CACHE_HOME", (scratch / "cache").c_str(), 1);
|
||
|
|
setenv("GIT_CONFIG_NOSYSTEM", "1", 1);
|
||
|
|
setenv("GIT_CONFIG_GLOBAL", "/dev/null", 1);
|
||
|
|
for (const char* name : { "GIT_AUTHOR_NAME", "GIT_COMMITTER_NAME" }) setenv(name, "crafter-build test", 1);
|
||
|
|
for (const char* name : { "GIT_AUTHOR_EMAIL", "GIT_COMMITTER_EMAIL" }) setenv(name, "test@example.invalid", 1);
|
||
|
|
const fs::path trace = scratch / "git-trace.log";
|
||
|
|
setenv("GIT_TRACE", trace.c_str(), 1);
|
||
|
|
|
||
|
|
const fs::path repo = scratch / "shared";
|
||
|
|
fs::create_directories(repo);
|
||
|
|
Git(repo, "init -q -b main");
|
||
|
|
Commit(repo, 1);
|
||
|
|
|
||
|
|
ExternalDependency shared;
|
||
|
|
shared.name = "shared";
|
||
|
|
shared.source = { .url = std::format("file://{}", repo.string()), .branch = "main" };
|
||
|
|
shared.includeDirs = { "" };
|
||
|
|
|
||
|
|
const fs::path project = scratch / "project";
|
||
|
|
fs::create_directories(project);
|
||
|
|
Write(project / "main.cpp", "#include \"round.h\"\n\nint main() { return ROUND > 0 ? 0 : 1; }\n");
|
||
|
|
|
||
|
|
constexpr std::int32_t tests = 8;
|
||
|
|
Configuration cfg;
|
||
|
|
cfg.path = project;
|
||
|
|
cfg.name = "project";
|
||
|
|
cfg.target = HostTarget();
|
||
|
|
for (std::int32_t i = 0; i < tests; ++i) {
|
||
|
|
Test& test = cfg.tests.emplace_back();
|
||
|
|
test.config.path = project;
|
||
|
|
test.config.name = std::format("t{}", i);
|
||
|
|
test.config.outputName = test.config.name;
|
||
|
|
test.config.target = HostTarget();
|
||
|
|
test.config.type = ConfigurationType::Executable;
|
||
|
|
test.config.externalDependencies = { shared };
|
||
|
|
std::array<fs::path, 0> interfaces = {};
|
||
|
|
std::array<fs::path, 1> implementations = { "main" };
|
||
|
|
test.config.GetInterfacesAndImplementations(interfaces, implementations);
|
||
|
|
}
|
||
|
|
const RunTestsOptions options { .jobs = tests };
|
||
|
|
|
||
|
|
TestSummary summary = RunTests(cfg, options);
|
||
|
|
Check(summary.passed == tests, std::format("{} tests that share the dependency build and pass at once ({} passed{})", tests, summary.passed, FirstFailure(summary)));
|
||
|
|
const std::uint32_t first = Fetches(trace);
|
||
|
|
Check(first == 1, std::format("and fetch it once between them, not once each ({} fetches)", first));
|
||
|
|
|
||
|
|
Commit(repo, 2);
|
||
|
|
summary = RunTests(cfg, options);
|
||
|
|
Check(summary.passed == tests, std::format("after the branch moved, they pass again ({} passed{})", summary.passed, FirstFailure(summary)));
|
||
|
|
const std::uint32_t second = Fetches(trace) - first;
|
||
|
|
Check(second == 1, std::format("and the next run fetches it once more ({} fetches)", second));
|
||
|
|
std::ifstream header(ExternalCloneDir(shared, HostTarget()) / "round.h");
|
||
|
|
std::string round;
|
||
|
|
std::getline(header, round);
|
||
|
|
Check(round == "#define ROUND 2", std::format("which brings the clone to the new commit ({})", round));
|
||
|
|
|
||
|
|
std::error_code ec;
|
||
|
|
fs::remove_all(scratch, ec);
|
||
|
|
if (failures > 0) {
|
||
|
|
std::println(std::cerr, "{} assertions failed", failures);
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
return 0;
|
||
|
|
}
|