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 <noreply@anthropic.com>
This commit is contained in:
parent
aec93617ee
commit
e62d33c7f0
6 changed files with 175 additions and 0 deletions
29
tests/ProjectArgs/fixture/app/project.cpp
Normal file
29
tests/ProjectArgs/fixture/app/project.cpp
Normal file
|
|
@ -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<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 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<std::string>(args.begin(), args.end());
|
||||
cfg.compileFlags.push_back(gitDefault ? "-DGIT_DEFAULT=1" : "-DGIT_DEFAULT=0");
|
||||
return cfg;
|
||||
}
|
||||
17
tests/ProjectArgs/fixture/leaf/project.cpp
Normal file
17
tests/ProjectArgs/fixture/leaf/project.cpp
Normal file
|
|
@ -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<const std::string_view> args) {
|
||||
Configuration cfg;
|
||||
cfg.path = "./";
|
||||
cfg.name = "leaf";
|
||||
cfg.outputName = "leaf";
|
||||
cfg.type = ConfigurationType::LibraryStatic;
|
||||
ApplyStandardArgs(cfg, args);
|
||||
return cfg;
|
||||
}
|
||||
21
tests/ProjectArgs/fixture/lib/project.cpp
Normal file
21
tests/ProjectArgs/fixture/lib/project.cpp
Normal file
|
|
@ -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<const std::string_view> 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;
|
||||
}
|
||||
91
tests/ProjectArgs/main.cpp
Normal file
91
tests/ProjectArgs/main.cpp
Normal file
|
|
@ -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<std::string> 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<std::string_view, 2> args = { "--debug", "--flavor=x" };
|
||||
const std::vector<std::string> flavor = { "--flavor=x" };
|
||||
const std::vector<std::string> 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;
|
||||
}
|
||||
|
|
@ -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<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;
|
||||
}
|
||||
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