test: specs and ProjectDependency() in a project file's static initialisers

ProjectArgs gets a sibling that makes its own sibling's spec at
namespace scope. Before the fix that spec took the args the file was
first loaded with; now it takes none on every load. The checks go
through one CheckLoaded helper, which prints what each sibling was
given, so a failure says which args a wrong frame handed over.

ProjectDependency loads a fixture whose static initialiser asks for its
pragma dependency, in a child, because a throw there terminates the
process: before the fix it found none. The check that the dependency
gets none of the consumer's args moves into the first load, which now
passes the app an arg it ignores; the separate load could not fail its
own check, and its early return skipped every later one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jorijn van der Graaf 2026-10-31 05:10:02 +01:00
commit e6363b95fe
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
5 changed files with 102 additions and 36 deletions

View file

@ -90,14 +90,19 @@ int main(int argc, char** argv) {
const fs::path app = scratch / "app" / "project.cpp";
const fs::path appSo = scratch / "app" / "build" / "project.so";
std::array<std::string_view, 0> noArgs = {};
Check(IsTopLevelProject(), "outside any project file counts as top level");
// The dependency is fetched, built and linked before project.cpp runs,
// and ProjectDependency() hands it back by either name.
// and ProjectDependency() hands it back by either name. The app ignores
// the arg it is loaded with, and the dependency must not get it: host
// tooling gets none of the consumer's args, although a LocalProject spec
// defaults to them. The consumer's flags would split the tool's variant
// from every other consumer's, and its --target= would have the tool's
// project.cpp choose for the wrong triple.
try {
Configuration cfg = LoadProject(app, noArgs);
std::array<std::string_view, 1> flavored = { "--flavor=x" };
Configuration cfg = LoadProject(app, flavored);
Check(cfg.name == "hello from greeter", std::format("project.cpp imported and called the dependency's module (got '{}')", cfg.name));
Check(std::ranges::contains(cfg.compileFlags, "-DTOP_LEVEL=1"), "the project LoadProject was pointed at sees itself as top level");
Check(cfg.dependencies.size() == 2, "ProjectDependency resolved by module name and by configuration name");
@ -109,6 +114,7 @@ int main(int argc, char** argv) {
Check(dep.march == "native", "the environment's march was overridden for host tooling");
Check(std::ranges::contains(dep.compileFlags, "-fPIC"), "the dependency archive was built position-independent");
Check(std::ranges::contains(dep.compileFlags, "-DTOP_LEVEL=0"), "a dependency's project file does not see itself as top level");
Check(dep.projectArgs.empty(), std::format("the dependency got none of the consumer's args (got {})", dep.projectArgs));
Check(fs::exists(ExpectedOutput(dep)), "the dependency archive exists");
}
} catch (const std::exception& e) {
@ -116,20 +122,15 @@ int main(int argc, char** argv) {
return 1;
}
// Host tooling gets none of the consumer's args, although a LocalProject
// spec defaults to them: the consumer's flags would split the tool's
// variant from every other consumer's, and its --target= would have the
// tool's project.cpp choose for the wrong triple.
try {
std::array<std::string_view, 1> flavored = { "--flavor=x" };
Configuration cfg = LoadProject(app, flavored);
const bool found = !cfg.dependencies.empty();
Check(found, "a consumer loaded with args still finds its pragma dependency");
if (found) Check(cfg.dependencies[0]->projectArgs.empty(), std::format("a pragma dependency is loaded without the consumer's args (got {})", cfg.dependencies[0]->projectArgs));
} catch (const std::exception& e) {
std::println(std::cerr, "FAIL: LoadProject with args threw: {}", e.what());
return 1;
// A static initialiser of the project file runs while LoadProject
// dlopens it, before CrafterBuildProject is called, and can already ask
// for a dependency. In a child, because a static initialiser that
// throws terminates the process.
{
const std::string loaded = LoadInChild(scratch / "static-dep" / "project.cpp");
Check(loaded == "static greeter", std::format("ProjectDependency() works in a static initialiser (got '{}')", loaded));
}
Check(fs::exists(appSo), "project.so was produced");
if (!fs::exists(appSo)) return 1;
const fs::file_time_type firstSo = fs::last_write_time(appSo);