From 3ad19510570b9e31ff429f7d17aa450066e416be Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Sat, 31 Oct 2026 02:56:15 +0100 Subject: [PATCH] test: sibling projects inherit the loading project's args ProjectArgs loads a chain app -> lib -> leaf of LocalProjects with --debug and a project flag. lib, loaded without .args, has to see both, and so does its own sibling, which takes the default one level further down. Copies loaded with `.args = {}` and with an explicit list get nothing and exactly that list. The GitProjectSpec default is checked on the spec itself, since a GitProject would need a repository to clone. Without the defaults, five of its checks fail. ProjectDependency now also loads its consumer with a project flag: the pragma dependency must still come back with no args. Without the explicit `.args = {}` in the pragma path it gets the consumer's flag. Co-Authored-By: Claude Opus 5.5 --- project.cpp | 2 + tests/ProjectArgs/fixture/app/project.cpp | 29 +++++++ tests/ProjectArgs/fixture/leaf/project.cpp | 17 ++++ tests/ProjectArgs/fixture/lib/project.cpp | 21 +++++ tests/ProjectArgs/main.cpp | 91 ++++++++++++++++++++++ tests/ProjectDependency/main.cpp | 15 ++++ 6 files changed, 175 insertions(+) create mode 100644 tests/ProjectArgs/fixture/app/project.cpp create mode 100644 tests/ProjectArgs/fixture/leaf/project.cpp create mode 100644 tests/ProjectArgs/fixture/lib/project.cpp create mode 100644 tests/ProjectArgs/main.cpp diff --git a/project.cpp b/project.cpp index af83a8d..d3771f8 100644 --- a/project.cpp +++ b/project.cpp @@ -287,6 +287,8 @@ extern "C" Configuration CrafterBuildProject(std::span a .LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl"); cfg.AddTest("ProjectDependency").Dependencies({ CrafterBuildLib.get() }) .LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl"); + cfg.AddTest("ProjectArgs").Dependencies({ CrafterBuildLib.get() }) + .LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl"); cfg.AddTest("Script").Dependencies({ CrafterBuildLib.get() }) .LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl"); cfg.AddTest("ConcurrentDependencyReset").Dependencies({ CrafterBuildLib.get() }); diff --git a/tests/ProjectArgs/fixture/app/project.cpp b/tests/ProjectArgs/fixture/app/project.cpp new file mode 100644 index 0000000..353c592 --- /dev/null +++ b/tests/ProjectArgs/fixture/app/project.cpp @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® + +import std; +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. +extern "C" Configuration CrafterBuildProject(std::span 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 cfg; + cfg.path = "./"; + cfg.name = "app"; + cfg.outputName = "app"; + cfg.type = ConfigurationType::Executable; + ApplyStandardArgs(cfg, args); + cfg.dependencies = { inherited, none, replaced }; + // 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" } }; + const bool gitDefault = git.args == std::vector(args.begin(), args.end()); + cfg.compileFlags.push_back(gitDefault ? "-DGIT_DEFAULT=1" : "-DGIT_DEFAULT=0"); + return cfg; +} diff --git a/tests/ProjectArgs/fixture/leaf/project.cpp b/tests/ProjectArgs/fixture/leaf/project.cpp new file mode 100644 index 0000000..15b16d4 --- /dev/null +++ b/tests/ProjectArgs/fixture/leaf/project.cpp @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® + +import std; +import Crafter.Build; +using namespace Crafter; + +// The end of the chain: records what it was loaded with and nothing else. +extern "C" Configuration CrafterBuildProject(std::span args) { + Configuration cfg; + cfg.path = "./"; + cfg.name = "leaf"; + cfg.outputName = "leaf"; + cfg.type = ConfigurationType::LibraryStatic; + ApplyStandardArgs(cfg, args); + return cfg; +} diff --git a/tests/ProjectArgs/fixture/lib/project.cpp b/tests/ProjectArgs/fixture/lib/project.cpp new file mode 100644 index 0000000..f1702c9 --- /dev/null +++ b/tests/ProjectArgs/fixture/lib/project.cpp @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® + +import std; +import Crafter.Build; +using namespace Crafter; + +// The middle of the chain. Its own sibling takes the default too, so +// whatever this file was loaded with has to reach the leaf unchanged. +extern "C" Configuration CrafterBuildProject(std::span args) { + Configuration* leaf = LocalProject({ .projectFile = "../leaf/project.cpp" }); + + Configuration cfg; + cfg.path = "./"; + cfg.name = "lib"; + cfg.outputName = "lib"; + cfg.type = ConfigurationType::LibraryStatic; + ApplyStandardArgs(cfg, args); + cfg.dependencies = { leaf }; + return cfg; +} diff --git a/tests/ProjectArgs/main.cpp b/tests/ProjectArgs/main.cpp new file mode 100644 index 0000000..4b47938 --- /dev/null +++ b/tests/ProjectArgs/main.cpp @@ -0,0 +1,91 @@ +// 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 GitProject or LocalProject without `.args` forwards the args of the +// project file that declares it: both specs default their args to +// ProjectArgs(). Before that default, every project.cpp with a sibling +// dependency copied its span into a std::vector for each spec, +// and a spec that missed the copy built its sibling with none of the +// project's flags: for the host while the project cross-compiled, and in +// 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. +namespace { + std::int32_t Failures = 0; + + void Check(bool cond, std::string_view msg) { + if (!cond) { + std::println(std::cerr, "FAIL: {}", msg); + ++Failures; + } + } + + // The single sibling lib and leaf declare, or nullptr. + const Configuration* OnlyDependency(const Configuration& cfg) { + return cfg.dependencies.size() == 1 ? cfg.dependencies[0] : nullptr; + } +} + +int main() { + const fs::path installedHome = GetCrafterBuildHome(); + if (!fs::exists(installedHome / "Crafter.Build.cppm")) { + std::println(std::cerr, "SKIP: no module sources at {}", installedHome.string()); + return 77; + } + + // Staged because LoadProject drops a build/ tree next to every project + // file it compiles. + const fs::path fixture = fs::current_path() / "tests" / "ProjectArgs" / "fixture"; + const fs::path scratch = fs::temp_directory_path() / "crafter-build-project-args"; + std::error_code ec; + fs::remove_all(scratch, ec); + fs::create_directories(scratch); + fs::copy(fixture, scratch, fs::copy_options::recursive); + + Check(ProjectArgs().empty(), "no project args outside a project file"); + + // Loaded from the project's own directory, as crafter-build runs it: the + // fixture's LocalProject paths are relative to the working directory. + const fs::path cwd = fs::current_path(); + fs::current_path(scratch / "app"); + std::array args = { "--debug", "--flavor=x" }; + const std::vector flavor = { "--flavor=x" }; + const std::vector only = { "--only" }; + 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"); + + // 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"); + } + } catch (const std::exception& e) { + fs::current_path(cwd); + std::println(std::cerr, "FAIL: LoadProject threw: {}", e.what()); + return 1; + } + fs::current_path(cwd); + + Check(ProjectArgs().empty(), "no project args once the project file has returned"); + return Failures > 0 ? 1 : 0; +} diff --git a/tests/ProjectDependency/main.cpp b/tests/ProjectDependency/main.cpp index fcf1c57..de01130 100644 --- a/tests/ProjectDependency/main.cpp +++ b/tests/ProjectDependency/main.cpp @@ -115,6 +115,21 @@ int main(int argc, char** argv) { std::println(std::cerr, "FAIL: LoadProject threw: {}", e.what()); 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 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; + } 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);