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:
parent
060ab4380f
commit
e6363b95fe
5 changed files with 102 additions and 36 deletions
|
|
@ -6,12 +6,14 @@ import Crafter.Build;
|
|||
using namespace Crafter;
|
||||
|
||||
// Loads the same sibling three ways: with its args left at their default,
|
||||
// with nothing forwarded, and with a list of its own. The test reads what
|
||||
// each copy received back out of cfg.dependencies.
|
||||
// with nothing forwarded, and with a list of its own. Then a sibling that
|
||||
// makes its own sibling's spec at namespace scope. The test reads what each
|
||||
// copy received back out of cfg.dependencies.
|
||||
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
|
||||
Configuration* inherited = LocalProject({ .projectFile = "../lib/project.cpp" });
|
||||
Configuration* none = LocalProject({ .projectFile = "../lib/project.cpp", .args = {} });
|
||||
Configuration* replaced = LocalProject({ .projectFile = "../lib/project.cpp", .args = { "--only" } });
|
||||
Configuration* fromStatic = LocalProject({ .projectFile = "../static/project.cpp" });
|
||||
|
||||
Configuration cfg;
|
||||
cfg.path = "./";
|
||||
|
|
@ -19,7 +21,7 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
|||
cfg.outputName = "app";
|
||||
cfg.type = ConfigurationType::Executable;
|
||||
ApplyStandardArgs(cfg, args);
|
||||
cfg.dependencies = { inherited, none, replaced };
|
||||
cfg.dependencies = { inherited, none, replaced, fromStatic };
|
||||
// GitProjectSpec takes the same default. Checked on the spec itself,
|
||||
// since a GitProject would need a repository to clone.
|
||||
const GitProjectSpec git{ .source = { .url = "unused" } };
|
||||
|
|
|
|||
26
tests/ProjectArgs/fixture/static/project.cpp
Normal file
26
tests/ProjectArgs/fixture/static/project.cpp
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
import std;
|
||||
import Crafter.Build;
|
||||
using namespace Crafter;
|
||||
|
||||
// Makes its sibling's spec at namespace scope, so the spec's args default
|
||||
// while LoadProject dlopens this file: before CrafterBuildProject is called,
|
||||
// and on the first load of the file only.
|
||||
namespace {
|
||||
const LocalProjectSpec LeafSpec{ .projectFile = "../leaf/project.cpp" };
|
||||
}
|
||||
|
||||
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
|
||||
Configuration* leaf = LocalProject(LeafSpec);
|
||||
|
||||
Configuration cfg;
|
||||
cfg.path = "./";
|
||||
cfg.name = "static";
|
||||
cfg.outputName = "static";
|
||||
cfg.type = ConfigurationType::LibraryStatic;
|
||||
ApplyStandardArgs(cfg, args);
|
||||
cfg.dependencies = { leaf };
|
||||
return cfg;
|
||||
}
|
||||
|
|
@ -15,7 +15,10 @@ using namespace Crafter;
|
|||
// release while the project built --debug. The fixture is a chain
|
||||
// app -> lib -> leaf of LocalProjects; app also loads lib with an empty and
|
||||
// with a replaced list, and checks the GitProjectSpec default itself, which
|
||||
// a clone would need a repository for.
|
||||
// a clone would need a repository for. A spec made at namespace scope gets
|
||||
// no args: made while dlopen runs the file's static initialisers, which
|
||||
// happens on its first load only, it would otherwise keep that load's args
|
||||
// for every later one.
|
||||
namespace {
|
||||
std::int32_t Failures = 0;
|
||||
|
||||
|
|
@ -26,10 +29,21 @@ namespace {
|
|||
}
|
||||
}
|
||||
|
||||
// The single sibling lib and leaf declare, or nullptr.
|
||||
// The single sibling lib, static and leaf declare, or nullptr.
|
||||
const Configuration* OnlyDependency(const Configuration& cfg) {
|
||||
return cfg.dependencies.size() == 1 ? cfg.dependencies[0] : nullptr;
|
||||
}
|
||||
|
||||
// What a sibling was loaded with, as ApplyStandardArgs split it up:
|
||||
// --debug into cfg.debug, the uninterpreted rest into cfg.projectArgs.
|
||||
// Printed either way, so a failure says which args the sibling read.
|
||||
void CheckLoaded(const Configuration* cfg, bool debug, const std::vector<std::string>& rest, std::string_view what) {
|
||||
if (!cfg) {
|
||||
Check(false, std::format("{} (no such configuration)", what));
|
||||
return;
|
||||
}
|
||||
Check(cfg->debug == debug && cfg->projectArgs == rest, std::format("{} (debug {}, projectArgs {})", what, cfg->debug, cfg->projectArgs));
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
|
|
@ -60,24 +74,25 @@ int main() {
|
|||
try {
|
||||
Configuration cfg = LoadProject("project.cpp", args);
|
||||
Check(std::ranges::contains(cfg.compileFlags, "-DGIT_DEFAULT=1"), "a GitProjectSpec's args default to the project's own");
|
||||
Check(cfg.dependencies.size() == 3, std::format("the project declared its three siblings (got {})", cfg.dependencies.size()));
|
||||
if (cfg.dependencies.size() == 3) {
|
||||
const Configuration& inherited = *cfg.dependencies[0];
|
||||
const Configuration& none = *cfg.dependencies[1];
|
||||
const Configuration& replaced = *cfg.dependencies[2];
|
||||
Check(inherited.debug && inherited.projectArgs == flavor, std::format("a LocalProject without .args gets the project's args (debug {}, projectArgs {})", inherited.debug, inherited.projectArgs));
|
||||
Check(!none.debug && none.projectArgs.empty(), std::format("`.args = {{}}` forwards nothing (debug {}, projectArgs {})", none.debug, none.projectArgs));
|
||||
Check(!replaced.debug && replaced.projectArgs == only, std::format("an explicit list replaces the args rather than adding to them (debug {}, projectArgs {})", replaced.debug, replaced.projectArgs));
|
||||
Check(&inherited != &none && &inherited != &replaced && &none != &replaced, "each list loads its own configuration");
|
||||
Check(cfg.dependencies.size() == 4, std::format("the project declared its four siblings (got {})", cfg.dependencies.size()));
|
||||
if (cfg.dependencies.size() == 4) {
|
||||
const Configuration* inherited = cfg.dependencies[0];
|
||||
const Configuration* none = cfg.dependencies[1];
|
||||
const Configuration* replaced = cfg.dependencies[2];
|
||||
const Configuration* fromStatic = cfg.dependencies[3];
|
||||
CheckLoaded(inherited, true, flavor, "a LocalProject without .args gets the project's args");
|
||||
CheckLoaded(none, false, {}, "`.args = {}` forwards nothing");
|
||||
CheckLoaded(replaced, false, only, "an explicit list replaces the args rather than adding to them");
|
||||
Check(inherited != none && inherited != replaced && none != replaced, "each list loads its own configuration");
|
||||
|
||||
// One sibling further down, the same default passes on what the
|
||||
// middle project was given, not what the top one was.
|
||||
const Configuration* inheritedLeaf = OnlyDependency(inherited);
|
||||
Check(inheritedLeaf && inheritedLeaf->debug && inheritedLeaf->projectArgs == flavor, "the project's args reach its sibling's sibling");
|
||||
const Configuration* noneLeaf = OnlyDependency(none);
|
||||
Check(noneLeaf && !noneLeaf->debug && noneLeaf->projectArgs.empty(), "a sibling loaded with nothing passes nothing on");
|
||||
const Configuration* replacedLeaf = OnlyDependency(replaced);
|
||||
Check(replacedLeaf && !replacedLeaf->debug && replacedLeaf->projectArgs == only, "a sibling passes on the list it was given");
|
||||
CheckLoaded(OnlyDependency(*inherited), true, flavor, "the project's args reach its sibling's sibling");
|
||||
CheckLoaded(OnlyDependency(*none), false, {}, "a sibling loaded with nothing passes nothing on");
|
||||
CheckLoaded(OnlyDependency(*replaced), false, only, "a sibling passes on the list it was given");
|
||||
|
||||
CheckLoaded(fromStatic, true, flavor, "the sibling with a namespace-scope spec is itself loaded with the args");
|
||||
CheckLoaded(OnlyDependency(*fromStatic), false, {}, "a spec made at namespace scope forwards nothing");
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
fs::current_path(cwd);
|
||||
|
|
|
|||
22
tests/ProjectDependency/fixture/static-dep/project.cpp
Normal file
22
tests/ProjectDependency/fixture/static-dep/project.cpp
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
import std;
|
||||
import Crafter.Build;
|
||||
#pragma crafter dependency "../greeter"
|
||||
using namespace Crafter;
|
||||
|
||||
// Asks for its dependency at namespace scope, so the call runs while
|
||||
// LoadProject dlopens this file, before CrafterBuildProject is called.
|
||||
namespace {
|
||||
Configuration* const Greeter = ProjectDependency("greeter");
|
||||
}
|
||||
|
||||
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view>) {
|
||||
Configuration cfg;
|
||||
cfg.path = "./";
|
||||
cfg.name = std::format("static {}", Greeter->name);
|
||||
cfg.outputName = "static-dep";
|
||||
cfg.type = ConfigurationType::Executable;
|
||||
return cfg;
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue