From fe3365d5d4ea934e1daab0257549550413ff568c Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 13 Nov 2026 16:02:39 +0900 Subject: [PATCH 001/115] wip: a row's GCC payload as data (gcc-triple); the COFF object format --- modules/manifest/src/platform.cppm | 4 +- modules/manifest/src/platform_toml.cppm | 6 ++- modules/toolchain-model/build.mcpp | 6 +-- .../toolchain-model/src/target_schema.cppm | 5 ++ modules/toolchain-model/src/triple.cppm | 21 +++++--- src/build/flags.cppm | 9 +++- src/build/prepare/platform.cpp | 1 + src/build/prepare/retarget.cpp | 20 ++++++++ src/toolchain/gcc.cppm | 10 ++++ src/toolchain/registry.cppm | 49 +++++++++++++++++++ 10 files changed, 117 insertions(+), 14 deletions(-) diff --git a/modules/manifest/src/platform.cppm b/modules/manifest/src/platform.cppm index fee29752e..ae0798d59 100644 --- a/modules/manifest/src/platform.cppm +++ b/modules/manifest/src/platform.cppm @@ -246,7 +246,7 @@ struct Hosted { // engine's table does not have, with the facts a build of it needs. struct RowSpec { std::string triple; - std::string tier = "preview", note, clangTriple, objectFormat = "elf", family, dataModel, + std::string tier = "preview", note, clangTriple, gccTriple, objectFormat = "elf", family, dataModel, exeSuffix, sharedSuffix, loader, execution; std::string march, mabi, codeModel, multilib, linkEmulation; std::vector flags, descriptorKeys; @@ -373,7 +373,7 @@ inline constexpr std::string_view kPlatformKeys[] = { "define", "provider", "system", "priority", }; inline constexpr std::string_view kDefineKeys[] = { - "triple", "tier", "note", "standard", "exact", "clang-triple", "object-format", "family", + "triple", "tier", "note", "standard", "exact", "clang-triple", "gcc-triple", "object-format", "family", "data-model", "exe-suffix", "shared-suffix", "loader", "execution", "march", "mabi", "code-model", "multilib", "flags", "link-emulation", "descriptor-keys", }; diff --git a/modules/manifest/src/platform_toml.cppm b/modules/manifest/src/platform_toml.cppm index 714de9d84..544f07b64 100644 --- a/modules/manifest/src/platform_toml.cppm +++ b/modules/manifest/src/platform_toml.cppm @@ -584,6 +584,7 @@ std::expected Reader::define(const toml::Table& t, std::st list_of(kDefineKeys))); for (auto [key, to] : std::initializer_list>{ {"triple", &r.triple}, {"tier", &r.tier}, {"note", &r.note}, {"clang-triple", &r.clangTriple}, + {"gcc-triple", &r.gccTriple}, {"object-format", &r.objectFormat}, {"family", &r.family}, {"data-model", &r.dataModel}, {"exe-suffix", &r.exeSuffix}, {"shared-suffix", &r.sharedSuffix}, {"loader", &r.loader}, {"execution", &r.execution}, {"march", &r.march}, {"mabi", &r.mabi}, @@ -611,8 +612,9 @@ std::expected Reader::define(const toml::Table& t, std::st "\"x86_64-freebsd\"", where)); if (r.tier != "verified" && r.tier != "preview" && r.tier != "planned") return std::unexpected(std::format("{}.tier is verified, preview or planned", where)); - if (r.objectFormat != "elf" && r.objectFormat != "mach-o" && r.objectFormat != "pe" && r.objectFormat != "wasm") - return std::unexpected(std::format("{}.object-format is elf, mach-o, pe or wasm", where)); + if (r.objectFormat != "elf" && r.objectFormat != "mach-o" && r.objectFormat != "pe" && r.objectFormat != "wasm" + && r.objectFormat != "coff") + return std::unexpected(std::format("{}.object-format is elf, mach-o, pe, wasm or coff", where)); return r; } diff --git a/modules/toolchain-model/build.mcpp b/modules/toolchain-model/build.mcpp index 6b79f4586..c614c70a1 100644 --- a/modules/toolchain-model/build.mcpp +++ b/modules/toolchain-model/build.mcpp @@ -16,7 +16,7 @@ namespace { constexpr std::string_view kRowKeys[] = { "triple", "tier", "note", "standard", "exact", "march", "mabi", "code-model", "multilib", "flags", - "link-emulation", "clang-triple", "family", "object-format", "data-model", "exe-suffix", + "link-emulation", "clang-triple", "gcc-triple", "family", "object-format", "data-model", "exe-suffix", "shared-suffix", "loader", "execution", }; constexpr std::string_view kProviderKeys[] = { @@ -117,14 +117,14 @@ std::string row_entry(const toml::Table& t, Arrays& arrays, std::set}` + // The target a cross GCC payload is built for, in GNU's spelling + // (`i586-pc-msdosdjgpp`): the payload is `-gcc` and its + // frontends are `-gcc` and `-g++`. Empty: the engine's + // conventions for the row's system name the payload. + std::string_view gccTriple; std::string_view objectFormat; // elf | mach-o | pe | wasm std::string_view family; // unix | windows | "" std::string_view dataModel; // lp64 | llp64 | ilp32 diff --git a/modules/toolchain-model/src/triple.cppm b/modules/toolchain-model/src/triple.cppm index 9a5f7f1b8..5793c47bd 100644 --- a/modules/toolchain-model/src/triple.cppm +++ b/modules/toolchain-model/src/triple.cppm @@ -52,7 +52,9 @@ export namespace mcpp::toolchain::triple { // an operating system to link against" are different axes: a bare-metal // RISC-V image is ELF with no OS, and a wasm module has an OS-like layer // (Emscripten's POSIX emulation) and is not ELF. -enum class ObjectFormat { Elf, MachO, Pe, Wasm }; +// COFF is the object format of a DJGPP program (go32: an MZ stub before a COFF +// image), which no other row produces; it is never a shared object. +enum class ObjectFormat { Elf, MachO, Pe, Wasm, Coff }; std::string_view to_string(ObjectFormat f) { switch (f) { @@ -60,6 +62,7 @@ std::string_view to_string(ObjectFormat f) { case ObjectFormat::MachO: return "Mach-O"; case ObjectFormat::Pe: return "PE"; case ObjectFormat::Wasm: return "wasm"; + case ObjectFormat::Coff: return "COFF"; } return "ELF"; } @@ -259,12 +262,18 @@ struct Triple { // fallback would otherwise claim ELF for it -- the silent wrong answer this // whole axis exists to remove. ObjectFormat object_format() const { - if (auto* d = defined()) { - if (d->objectFormat == "pe") return ObjectFormat::Pe; - if (d->objectFormat == "mach-o") return ObjectFormat::MachO; - if (d->objectFormat == "wasm") return ObjectFormat::Wasm; + // A row that states its format -- one a platform defines, or a built-in + // row whose data carries `object-format` -- is taken as written. + auto stated = [](std::string_view f) { + if (f == "pe") return ObjectFormat::Pe; + if (f == "mach-o") return ObjectFormat::MachO; + if (f == "wasm") return ObjectFormat::Wasm; + if (f == "coff") return ObjectFormat::Coff; return ObjectFormat::Elf; - } + }; + if (auto* d = defined()) return stated(d->objectFormat); + if (auto const* r = mcpp::toolchain::target::find_row(str()); r && !r->objectFormat.empty()) + return stated(r->objectFormat); if (os == "windows") return ObjectFormat::Pe; if (os == "macos" || os == "ios") return ObjectFormat::MachO; if (arch.starts_with("wasm")) return ObjectFormat::Wasm; diff --git a/src/build/flags.cppm b/src/build/flags.cppm index 86cf0da13..9de48a9c3 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -1113,6 +1113,10 @@ CompileFlags compute_flags_impl(const BuildPlan& plan, bool packageLinkFlags) { return LinkIntentFlavor::Wasm; case mcpp::toolchain::triple::ObjectFormat::Elf: return LinkIntentFlavor::Elf; + case mcpp::toolchain::triple::ObjectFormat::Coff: + // DJGPP links with GNU ld, which takes a search path and a + // library in ELF's spelling (`-L`, `-l`). + return LinkIntentFlavor::Elf; } } auto triple = plan.toolchain.targetTriple; @@ -1170,7 +1174,10 @@ CompileFlags compute_flags_impl(const BuildPlan& plan, bool packageLinkFlags) { f.ldRuntimeFallback = farm_ld; std::filesystem::path binutilsBin; - if (!isMuslTc && !isMingwTc && caps.stdlib_id == "libstdc++") { + // A self-contained cross payload finds its own assembler and linker under + // its prefix: musl, MinGW, and a payload a row names by its GCC target. + if (!isMuslTc && !isMingwTc && caps.stdlib_id == "libstdc++" + && !mcpp::toolchain::row_states_gcc_target(plan.toolchain)) { auto ar = mcpp::toolchain::archive_tool(plan.toolchain); if (!ar.empty()) binutilsBin = ar.parent_path(); diff --git a/src/build/prepare/platform.cpp b/src/build/prepare/platform.cpp index fd2400c79..e04115eb3 100644 --- a/src/build/prepare/platform.cpp +++ b/src/build/prepare/platform.cpp @@ -204,6 +204,7 @@ void define_platform(const mp::Statement& s, std::string_view shortName, std::st r.flags = set.keep(d.flags); r.linkEmulation = set.keep(d.linkEmulation); r.clangTriple = set.keep(d.clangTriple); + r.gccTriple = set.keep(d.gccTriple); r.objectFormat = set.keep(d.objectFormat); r.family = set.keep(d.family); r.dataModel = set.keep(d.dataModel); diff --git a/src/build/prepare/retarget.cpp b/src/build/prepare/retarget.cpp index 1aaad88db..1e7b6ba5a 100644 --- a/src/build/prepare/retarget.cpp +++ b/src/build/prepare/retarget.cpp @@ -33,6 +33,7 @@ import mcpp.freestanding.linkline; // the ISA profile, for the std module comman import mcpp.toolchain.post_install; import mcpp.toolchain.abi; import mcpp.toolchain.triple; +import mcpp.toolchain.target_rows; import mcpp.toolchain.base; import mcpp.toolchain.base_probe; import mcpp.home; @@ -109,6 +110,25 @@ step2_retarget_for_retargetable_driver(PrepareState& state) { // already names: a RETARGETABLE driver has to be told. gcc is not one — a // gcc payload IS its target — so the mingw and musl-gcc crosses keep // answering from `-dumpmachine`, which for them remains a measured fact. + // A ROW THAT STATES ITS GCC TARGET (`gcc-triple`) names the payload whose + // compiler is that target, and `-dumpmachine` answers in GCC's spelling + // (`i586-pc-msdosdjgpp`) rather than the row's (`i586-dos-djgpp`). The + // measured answer is checked against the row's statement and then named + // by the row, so the output directory, the fingerprint and every flag + // decision read the identity the build was asked for. + if (!state.overrides.target_triple.empty() + && state.tc->compiler == mcpp::toolchain::CompilerId::GCC) { + if (auto want = mcpp::toolchain::triple::parse(state.overrides.target_triple)) { + if (auto const* r = mcpp::toolchain::target::find_row(want->str()); r && !r->gccTriple.empty()) { + if (state.tc->targetTriple != r->gccTriple) + return std::unexpected(std::format( + "the compiler {} reports the target '{}', and the row '{}' states its GCC target " + "'{}' (`gcc-triple`)", + state.tc->binaryPath.string(), state.tc->targetTriple, want->str(), r->gccTriple)); + state.tc->targetTriple = want->str(); + } + } + } if (!state.overrides.target_triple.empty()) { if (auto want = mcpp::toolchain::triple::parse(state.overrides.target_triple); want && (want->is_freestanding() diff --git a/src/toolchain/gcc.cppm b/src/toolchain/gcc.cppm index b711fbf64..8dcfafc08 100644 --- a/src/toolchain/gcc.cppm +++ b/src/toolchain/gcc.cppm @@ -10,6 +10,8 @@ import mcpp.toolchain.model; import mcpp.toolchain.probe; import mcpp.provider; import mcpp.provider.select; +import mcpp.toolchain.triple; +import mcpp.toolchain.target_rows; export namespace mcpp::toolchain::gcc { @@ -183,6 +185,14 @@ std::filesystem::path binutils_prefix_dir(const Toolchain& tc) { // at all, so answering with one would describe a flag nobody passes. if (tc.compiler != CompilerId::GCC) return {}; if (is_musl_target(tc) || is_mingw_target(tc)) return {}; + // A cross payload a row names by its GCC target (`gcc-triple`) is built + // with its own binutils, under the same prefix; a host `as` would read its + // assembly as the host's (measured: DJGPP's 32-bit `push` refused). + // The target is read in either spelling: the row's, once the build has + // named it, or the compiler's own `-dumpmachine`, which detection reads. + for (auto const* r : mcpp::toolchain::target::rows()) + if (!r->gccTriple.empty() && (r->gccTriple == tc.targetTriple || r->triple == tc.targetTriple)) + return {}; // A GCC named by path (mcpp#755) finds its own assembler and linker, as // every self-contained GCC does; it is pointed elsewhere only when the // project stated where `as` (or `ld`) is. diff --git a/src/toolchain/registry.cppm b/src/toolchain/registry.cppm index df76d9b50..0089a4ef5 100644 --- a/src/toolchain/registry.cppm +++ b/src/toolchain/registry.cppm @@ -455,6 +455,11 @@ std::filesystem::path derive_c_compiler_path(const std::filesystem::path& cxxPat // missing" — see mcpp.pack.strip, which refuses only the second. std::filesystem::path binutils_tool(const Toolchain& tc, std::string_view name); +// Whether the target's row states its GCC target (`gcc-triple`), so that the +// payload is a self-contained cross GCC whose tools carry that prefix. The +// target is read in either spelling: the row's, or the compiler's own. +bool row_states_gcc_target(const Toolchain& tc); + std::filesystem::path archive_tool(const Toolchain& tc); std::filesystem::path link_tool(const Toolchain& tc); std::filesystem::path staged_std_bmi_path(const Toolchain& tc, @@ -877,6 +882,22 @@ XimToolchainPackage to_xim_package(const ToolchainSpec& spec) { // Family::Gcc — the target decides the payload. const auto& t = spec.target; + // A ROW THAT STATES ITS GCC TARGET (`gcc-triple`, a built-in row or one a + // platform defines) names its payload as data: `-gcc`, whose + // frontends carry that prefix. A cross GCC for a system the engine has no + // convention for (DJGPP's `i586-pc-msdosdjgpp`) is then a row and a + // provider, and no branch here. + if (auto const* r = mcpp::toolchain::target::find_row(t.str()); r && !r->gccTriple.empty()) { + const std::string g(r->gccTriple); + pkg.ximName = g + "-gcc"; + if constexpr (mcpplibs::os::is_windows) { + pkg.frontendCandidates = { g + "-g++.exe", g + "-g++" }; + } else { + pkg.frontendCandidates = { g + "-g++" }; + } + return pkg; + } + // `&& t.os == "linux"` — AND THE PARAGRAPH BELOW ALREADY SAID SO. // // "Canonical linux-musl triples coincide with the GNU tool spelling" is a @@ -1267,6 +1288,11 @@ std::optional identify_xim_payload(std::string_view ximDirName) return PayloadIdentity{ Family::Gcc, { "x86_64", "windows", "gnu" } }; if (ends_with(ximDirName, "-gcc")) { auto prefix = ximDirName.substr(0, ximDirName.size() - 4); + // The payload of a row that states its GCC target is named by it. + for (auto const* r : mcpp::toolchain::target::rows()) + if (r->gccTriple == prefix) + if (auto t = triple::parse(r->triple)) + return PayloadIdentity{ Family::Gcc, *t }; if (auto t = triple::parse(prefix)) return PayloadIdentity{ Family::Gcc, *t }; } @@ -1359,6 +1385,14 @@ std::filesystem::path derive_c_compiler(const Toolchain& tc) { return derive_c_compiler_path(tc.binaryPath); } +bool row_states_gcc_target(const Toolchain& tc) { + if (tc.compiler != CompilerId::GCC || tc.targetTriple.empty()) return false; + for (auto const* r : mcpp::toolchain::target::rows()) + if (!r->gccTriple.empty() && (r->gccTriple == tc.targetTriple || r->triple == tc.targetTriple)) + return true; + return false; +} + std::filesystem::path binutils_tool(const Toolchain& tc, std::string_view name) { // MSVC: no binutils, and none wanted — see the declaration. if (tc.compiler == CompilerId::MSVC) return {}; @@ -1389,6 +1423,21 @@ std::filesystem::path binutils_tool(const Toolchain& tc, std::string_view name) return {}; } + // A cross payload a row names by its GCC target (`gcc-triple`) carries its + // binutils beside the frontend under that prefix (`i586-pc-msdosdjgpp-ar`): + // the host's `ar` does not know the target's object format. The target is + // read in either spelling, as `gcc::binutils_prefix_dir` reads it. + for (auto const* r : mcpp::toolchain::target::rows()) { + if (r->gccTriple.empty() || (r->gccTriple != tc.targetTriple && r->triple != tc.targetTriple)) + continue; + const auto base = std::string(r->gccTriple) + "-" + std::string(name); + if constexpr (mcpplibs::os::is_windows) { + if (auto exe = dir / (base + ".exe"); std::filesystem::exists(exe, ec)) return exe; + } + if (auto p = dir / base; std::filesystem::exists(p, ec)) return p; + return {}; + } + // MinGW bundles its own binutils next to the frontend (self-contained, // like musl) — never an external binutils xpkg. Native (Windows-host) ships // `ar.exe`; the Linux-hosted cross ships the triple-prefixed ELF tool From b53719e38907435d72f15df3d4f48b31fceb2e8e Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 13 Nov 2026 16:59:47 +0900 Subject: [PATCH 002/115] tests/support and the first C++ end-to-end case; mtest 0.7.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tests/support (mcpp.test.support) is the support of the end-to-end cases (design 2026-11-12 §5.9): a project as a table of files, mcpp run with the shared home or a fresh one, the readings of a file's lines, a JSON document and an executable's header (ELF, PE, Mach-O) made by this process. A case file imports mcpplibs.mtest itself, which begins it at mtest's entry. tests/e2e/basics holds 02 (new, build, run) as the first case. mtest 0.7.1 is the dev-dependency and the engine CI pins; the unit suite holds under it (169 programs, 2627 cases). --- .github/versions.env | 4 +- mcpp.toml | 7 +- tests/e2e/basics/new_build_run.cpp | 34 ++++ tests/support/mcpp.toml | 17 ++ tests/support/src/support.cpp | 310 +++++++++++++++++++++++++++++ tests/support/src/support.cppm | 133 +++++++++++++ 6 files changed, 502 insertions(+), 3 deletions(-) create mode 100644 tests/e2e/basics/new_build_run.cpp create mode 100644 tests/support/mcpp.toml create mode 100644 tests/support/src/support.cpp create mode 100644 tests/support/src/support.cppm diff --git a/.github/versions.env b/.github/versions.env index 018aaffcf..45a88889b 100644 --- a/.github/versions.env +++ b/.github/versions.env @@ -1,5 +1,5 @@ # The engines CI pins, read by the composite actions (.github/actions/mtest). # mtest: an xlings package (xim:mtest) whose sha256 the index states; the release # bundle's sha256 checks the fallback download. -MTEST_VERSION=0.6.2 -MTEST_SHA256=06947a06ce4803bdc9ca9038bcda07b179d9ce46ab016355e772289abb4f6e30 +MTEST_VERSION=0.7.1 +MTEST_SHA256=473f7b61da1bb8206ff6f8e140fd997d0dc6edd32c84a6e21ba05a21a24e95d3 diff --git a/mcpp.toml b/mcpp.toml index dce864943..c73eb3289 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -60,6 +60,9 @@ ldflags = ["--coverage"] [test] windows_code_page = "utf-8" programs = "directory" +# The unit tests, and the end-to-end cases (tests/e2e//*.cpp, one program per +# subsystem); the support package and the scripts' fixtures are not tests. +discover = ["tests/**/*.cpp", "!tests/support/**", "!tests/e2e/fixtures/**", "!tests/fixtures/**"] # Each entry equals its row's Default (SPEC-009 §12): the LLVM line moved to # 23.1.3 — the first release carrying the macOS 27 arm64e.x1 linker fix @@ -165,7 +168,9 @@ members = [ # `mcpp build` ignores [dev-dependencies]; only `mcpp test` resolves them. [dev-dependencies] -mcpplibs.mtest = "0.5.2" +mcpplibs.mtest = "0.7.1" +# The support of the end-to-end cases (tests/support, design 2026-11-12 §5.9). +mcpp.test-support = { path = "tests/support" } # Everything else uses M5.0 conventions: # - target = mcpp (kind=bin inferred from src/main.cpp) diff --git a/tests/e2e/basics/new_build_run.cpp b/tests/e2e/basics/new_build_run.cpp new file mode 100644 index 000000000..ad99eb764 --- /dev/null +++ b/tests/e2e/basics/new_build_run.cpp @@ -0,0 +1,34 @@ +// mcpp new, build and run: the scaffold of a program (formerly tests/e2e/02_new_build_run.sh). +import std; +import mcpplibs.mtest; +import mcpp.test.support; + +using namespace mcpplibs::mtest; +namespace s = mcpp::test::support; + +namespace { + +E2E basics{"basics", tag{"requires:fresh-sandbox"}, [] { + "02 a new program is scaffolded, built and run with its arguments"_test = [] { + s::project root; + expect(fatal(e2e::succeeded(root.mcpp({"new", "hello"})))); + s::project p(root.at("hello"), {}); + for (auto f : {"mcpp.toml", "src/main.cpp", ".gitignore"}) + expect(p.exists(f)) << f; + const auto main_cpp = p.read("src/main.cpp"); + expect(e2e::contains(main_cpp, "import std")); + expect(e2e::contains(main_cpp, "std::println")); + + expect(fatal(e2e::succeeded(p.mcpp({"build"})))); + const auto binary = p.artifact(s::exe("hello")); + expect(fatal(binary.has_value())) << "no hello under target/"; + expect(std::filesystem::is_regular_file(*binary)); + + expect(e2e::contains(s::text(p.mcpp({"run"})), "Hello from hello")); + const auto args = s::text(p.mcpp({"run", "--", "foo", "bar"})); + expect(e2e::contains(args, "foo")); + expect(e2e::contains(args, "bar")); + }; +}}; + +} // namespace diff --git a/tests/support/mcpp.toml b/tests/support/mcpp.toml new file mode 100644 index 000000000..04f07d1de --- /dev/null +++ b/tests/support/mcpp.toml @@ -0,0 +1,17 @@ +# The support of the end-to-end cases (design 2026-11-12 §5.9): a project as a table of files, mcpp +# run in it with the shared home or a fresh one, and the readings a case makes of what mcpp wrote +# (lines of a file, a JSON document, the header of an executable). Built on mtest's e2e module; a +# failure prints the command line that repeats it. +[package] +namespace = "mcpp" +name = "test-support" +version = "0.1.0" +description = "The support of mcpp's end-to-end cases" +license = "Apache-2.0" + +[targets.test-support] +kind = "lib" + +[dependencies] +mcpplibs.mtest = "0.7.1" +nlohmann.json = "3.12.0" diff --git a/tests/support/src/support.cpp b/tests/support/src/support.cpp new file mode 100644 index 000000000..b2eff0fb3 --- /dev/null +++ b/tests/support/src/support.cpp @@ -0,0 +1,310 @@ +module mcpp.test.support; + +import std; + +namespace mcpp::test::support { + +// ---- the machine ------------------------------------------------------------------------------- + +bool on_windows() noexcept { +#if defined(_WIN32) + return true; +#else + return false; +#endif +} +bool on_macos() noexcept { +#if defined(__APPLE__) + return true; +#else + return false; +#endif +} +bool on_linux() noexcept { +#if defined(__linux__) + return true; +#else + return false; +#endif +} + +std::string exe(std::string_view name) { + return on_windows() ? std::string(name) + ".exe" : std::string(name); +} + +std::optional env(std::string_view name) { + if (const char* v = std::getenv(std::string(name).c_str())) + return std::string(v); + return std::nullopt; +} + +// ---- mcpp ---------------------------------------------------------------------------------------- + +std::string mcpp_path() { + if (auto v = env("MCPP_BIN_MCPP"); v && !v->empty()) + return *v; + if (auto v = env("MCPP"); v && !v->empty()) + return *v; + return e2e::program("mcpp"); +} + +e2e::outcome mcpp(std::vector args, const fs::path& cwd, const options& o) { + e2e::command c; + c.argv.push_back(mcpp_path()); + for (auto& a : args) c.argv.push_back(std::move(a)); + c.cwd = cwd; + if (o.where == home::fresh) { + auto h = mcpplibs::mtest::temp_dir() / "home"; + fs::create_directories(h); + c.env.emplace_back("MCPP_HOME", h.string()); + } else if (!env("MCPP_HOME")) { + if (auto u = env(on_windows() ? "USERPROFILE" : "HOME")) + c.env.emplace_back("MCPP_HOME", (fs::path(*u) / ".mcpp").string()); + } + for (const auto& kv : o.env) c.env.push_back(kv); + c.input = o.input; + c.timeout = o.timeout; + return e2e::run(std::move(c)); +} + +std::string text(const e2e::outcome& r) { return r.out + r.err; } + +// ---- a project ----------------------------------------------------------------------------------- + +namespace { +void write_into(const fs::path& dir, std::string_view relative, std::string_view content) { + const auto file = dir / fs::path(std::u8string(relative.begin(), relative.end())); + if (file.has_parent_path()) + fs::create_directories(file.parent_path()); + std::ofstream(file, std::ios::binary).write(content.data(), static_cast(content.size())); +} +} // namespace + +project::project(files contents) : project(mcpplibs::mtest::temp_dir() / "project", contents) {} + +project::project(fs::path dir, files contents) : dir_(std::move(dir)) { + fs::create_directories(dir_); + for (const auto& [path, content] : contents) + write_into(dir_, path, content); +} + +fs::path project::at(std::string_view relative) const { + return dir_ / fs::path(std::u8string(relative.begin(), relative.end())); +} +void project::write(std::string_view relative, std::string_view content) const { write_into(dir_, relative, content); } +std::string project::read(std::string_view relative) const { return read_file(at(relative)); } +bool project::exists(std::string_view relative) const { return fs::exists(at(relative)); } +void project::remove(std::string_view relative) const { fs::remove_all(at(relative)); } + +e2e::outcome project::mcpp(std::vector args, const options& o) const { + return support::mcpp(std::move(args), dir_, o); +} +e2e::outcome project::mcpp_in(std::string_view sub, std::vector args, const options& o) const { + return support::mcpp(std::move(args), at(sub), o); +} + +std::optional project::artifact(std::string_view name) const { + return find_file(dir_ / "target", name); +} + +std::optional find_file(const fs::path& root, std::string_view name) { + std::error_code ec; + if (!fs::is_directory(root, ec)) + return std::nullopt; + std::vector found; + for (auto it = fs::recursive_directory_iterator(root, fs::directory_options::skip_permission_denied, ec); + it != fs::recursive_directory_iterator(); it.increment(ec)) + if (it->is_regular_file(ec) && it->path().filename() == name) + found.push_back(it->path()); + if (found.empty()) + return std::nullopt; + std::ranges::sort(found); // the same answer on every run + return found.front(); +} + +std::vector find_files(const fs::path& root, std::string_view pattern) { + std::vector found; + std::error_code ec; + if (!fs::is_directory(root, ec)) + return found; + const std::regex re{std::string(pattern)}; + for (auto it = fs::recursive_directory_iterator(root, fs::directory_options::skip_permission_denied, ec); + it != fs::recursive_directory_iterator(); it.increment(ec)) + if (it->is_regular_file(ec) && std::regex_match(it->path().filename().string(), re)) + found.push_back(it->path()); + std::ranges::sort(found); + return found; +} + +// ---- readings -------------------------------------------------------------------------------------- + +std::string read_file(const fs::path& file) { + std::ifstream in(file, std::ios::binary); + return {std::istreambuf_iterator(in), std::istreambuf_iterator()}; +} + +std::vector lines_with(std::string_view text, std::string_view needle) { + std::vector out; + for (auto line : std::views::split(text, '\n')) { + std::string_view l(line.begin(), line.end()); + if (l.ends_with('\r')) l.remove_suffix(1); + if (l.find(needle) != std::string_view::npos) + out.emplace_back(l); + } + return out; +} + +std::vector file_lines_with(const fs::path& file, std::string_view needle) { + return lines_with(read_file(file), needle); +} + +nlohmann::json json(std::string_view text) { + auto whole = nlohmann::json::parse(text, nullptr, false); + if (!whole.is_discarded()) + return whole; + for (auto line : std::views::split(text, '\n')) { + auto one = nlohmann::json::parse(std::string_view(line.begin(), line.end()), nullptr, false); + if (!one.is_discarded()) + return one; + } + return nlohmann::json{}; +} + +nlohmann::json json_file(const fs::path& file) { return json(read_file(file)); } + +std::vector json_lines(std::string_view text) { + std::vector out; + for (auto line : std::views::split(text, '\n')) { + std::string_view l(line.begin(), line.end()); + if (l.empty() || l.front() != '{') + continue; + auto one = nlohmann::json::parse(l, nullptr, false); + if (!one.is_discarded() && one.is_object()) + out.push_back(std::move(one)); + } + return out; +} + +// ---- executables ------------------------------------------------------------------------------------- + +namespace { + +template +T get(const std::string& b, std::size_t at, bool big = false) { + T v{}; + if (at + sizeof(T) > b.size()) + return v; + std::memcpy(&v, b.data() + at, sizeof(T)); + if (big) { + T r{}; + for (std::size_t i = 0; i < sizeof(T); ++i) + r = static_cast((r << 8) | static_cast(b[at + i])); + return r; + } + return v; +} + +std::string cstring(const std::string& b, std::size_t at) { + if (at >= b.size()) + return {}; + const auto end = b.find('\0', at); + return b.substr(at, end == std::string::npos ? std::string::npos : end - at); +} + +void read_elf(const std::string& b, header& h) { + h.kind = format::elf; + h.wide = b.size() > 4 && b[4] == 2; + h.elf_type = get(b, 16); + h.machine = get(b, 18); + const std::uint64_t phoff = h.wide ? get(b, 32) : get(b, 28); + const std::uint16_t phentsize = get(b, h.wide ? 54 : 42); + const std::uint16_t phnum = get(b, h.wide ? 56 : 44); + std::uint64_t dyn_off = 0, dyn_size = 0; + struct load { std::uint64_t vaddr, offset, filesz; }; + std::vector loads; + for (std::uint16_t i = 0; i < phnum; ++i) { + const std::size_t p = static_cast(phoff) + std::size_t(i) * phentsize; + const auto type = get(b, p); + const std::uint64_t offset = h.wide ? get(b, p + 8) : get(b, p + 4); + const std::uint64_t vaddr = h.wide ? get(b, p + 16) : get(b, p + 8); + const std::uint64_t filesz = h.wide ? get(b, p + 32) : get(b, p + 16); + if (type == 3) { // PT_INTERP + h.elf_interpreter = true; + h.interpreter = cstring(b, static_cast(offset)); + } else if (type == 2) { // PT_DYNAMIC + dyn_off = offset; + dyn_size = filesz; + } else if (type == 1) { + loads.push_back({vaddr, offset, filesz}); + } + } + if (!dyn_size) + return; + const std::size_t entry = h.wide ? 16 : 8; + std::uint64_t strtab = 0; + std::vector needed; + std::uint64_t runpath = 0; + bool has_runpath = false; + for (std::uint64_t at = dyn_off; at + entry <= dyn_off + dyn_size; at += entry) { + const std::int64_t tag = h.wide ? get(b, at) : get(b, at); + const std::uint64_t val = h.wide ? get(b, at + 8) : get(b, at + 4); + if (tag == 0) break; + if (tag == 5) strtab = val; + else if (tag == 1) needed.push_back(val); + else if (tag == 29 || tag == 15) { runpath = val; has_runpath = true; } + } + // The string table's address, as a file offset. + std::uint64_t str_off = strtab; + for (const auto& l : loads) + if (strtab >= l.vaddr && strtab < l.vaddr + l.filesz) + str_off = strtab - l.vaddr + l.offset; + for (auto n : needed) + h.needed.push_back(cstring(b, static_cast(str_off + n))); + if (has_runpath) + h.runpath = cstring(b, static_cast(str_off + runpath)); +} + +void read_pe(const std::string& b, header& h) { + const auto pe = get(b, 0x3c); + if (b.compare(pe, 4, std::string("PE\0\0", 4)) != 0) + return; // an MZ program with no PE image (DOS) + h.kind = format::pe; + h.machine = get(b, pe + 4); + const auto magic = get(b, pe + 24); + h.wide = magic == 0x20b; + h.subsystem = get(b, pe + 24 + 68); +} + +} // namespace + +header read_header(const fs::path& file) { + header h; + std::ifstream in(file, std::ios::binary); + std::string b(64 * 1024, '\0'); + in.read(b.data(), static_cast(b.size())); + b.resize(static_cast(in.gcount())); + if (b.size() >= 4 && b.compare(0, 4, "\x7f" "ELF") == 0) { + // The dynamic section may lie past the first 64 KiB. + h.kind = format::elf; + b = read_file(file); + read_elf(b, h); + } else if (b.size() >= 2 && b[0] == 'M' && b[1] == 'Z') { + read_pe(b, h); + } else if (b.size() >= 4) { + const auto m = get(b, 0); + if (m == 0xfeedface || m == 0xfeedfacf) { + h.kind = format::macho; + h.wide = m == 0xfeedfacf; + h.machine = get(b, 4); + } else if (m == 0xbebafeca || m == 0xbfbafeca) { + h.kind = format::macho_fat; + } else if (b.compare(0, 4, std::string("\0asm", 4)) == 0) { + h.kind = format::wasm; + } else if (b.size() >= 8 && b.compare(0, 8, "!\n") == 0) { + h.kind = format::archive; + } + } + return h; +} + +} // namespace mcpp::test::support diff --git a/tests/support/src/support.cppm b/tests/support/src/support.cppm new file mode 100644 index 000000000..f82d19407 --- /dev/null +++ b/tests/support/src/support.cppm @@ -0,0 +1,133 @@ +// mcpp.test.support: what an end-to-end case of mcpp is made of (design 2026-11-12 §5.9). +// +// E2E build{"build", [] { +// "a project builds and runs"_test = [] { +// auto p = support::project({{"mcpp.toml", manifest}, {"src/main.cpp", main_cpp}}); +// auto r = p.mcpp({"build"}); +// expect(e2e::succeeded(r)); +// expect(e2e::contains(p.mcpp({"run"}).out, "hello")); +// }; +// }}; +// +// mcpp is the program under test (`MCPP_BIN_MCPP`, which `mcpp test` gives; else `MCPP`). Its home +// is the shared one (`MCPP_HOME`, else `~/.mcpp`: the toolchains a machine already holds) unless a +// case asks for a fresh one, which is the case's own and removed with it. +export module mcpp.test.support; + +import std; +export import mcpplibs.mtest; +export import mcpplibs.mtest.e2e; +export import nlohmann.json; + +export namespace mcpp::test::support { + +namespace fs = std::filesystem; +namespace e2e = mcpplibs::mtest::e2e; + +// ---- the machine ------------------------------------------------------------------------------- + +bool on_windows() noexcept; +bool on_macos() noexcept; +bool on_linux() noexcept; +// `name` as this system names an executable: `name.exe` on Windows. +std::string exe(std::string_view name); +// The value of the environment variable `name`, if set. +std::optional env(std::string_view name); + +// ---- mcpp ---------------------------------------------------------------------------------------- + +// The mcpp under test. +std::string mcpp_path(); + +enum class home { shared, fresh }; + +struct options { + home where = home::shared; + // Set (a value) or removed (nothing) for the run, after the home. + std::vector>> env; + std::string input; + std::chrono::milliseconds timeout{std::chrono::minutes{10}}; +}; + +// mcpp with `args`, in `cwd`. +e2e::outcome mcpp(std::vector args, const fs::path& cwd, const options& o = {}); + +// Standard output, then standard error: what a shell's `2>&1` would have collected, for a +// message whose stream is not what the case asserts. +std::string text(const e2e::outcome& r); + +// ---- a project ----------------------------------------------------------------------------------- + +using files = std::initializer_list>; + +// A directory of the case holding `files` (relative path, content); the directories are made. +class project { +public: + explicit project(files contents = {}); + // A project under an existing directory (a second project beside the first). + project(fs::path dir, files contents); + + const fs::path& dir() const noexcept { return dir_; } + fs::path at(std::string_view relative) const; + void write(std::string_view relative, std::string_view content) const; + std::string read(std::string_view relative) const; + bool exists(std::string_view relative) const; + void remove(std::string_view relative) const; + + // mcpp in this project (or in its subdirectory `sub`). + e2e::outcome mcpp(std::vector args, const options& o = {}) const; + e2e::outcome mcpp_in(std::string_view sub, std::vector args, const options& o = {}) const; + + // The first regular file under `target/` named `name` (an executable's name is given as + // `exe(name)`), or nothing. + std::optional artifact(std::string_view name) const; + +private: + fs::path dir_; +}; + +// The first regular file under `root` whose name is `name`. +std::optional find_file(const fs::path& root, std::string_view name); +// Every regular file under `root` whose name matches `pattern` (ECMAScript, the whole name). +std::vector find_files(const fs::path& root, std::string_view pattern); + +// ---- readings -------------------------------------------------------------------------------------- + +std::string read_file(const fs::path& file); +// The lines of `text` (or of a file) that contain `needle`. +std::vector lines_with(std::string_view text, std::string_view needle); +std::vector file_lines_with(const fs::path& file, std::string_view needle); +// A JSON document, or the first line of `text` that is one when `text` holds several. +nlohmann::json json(std::string_view text); +nlohmann::json json_file(const fs::path& file); +// Every line of `text` that is a JSON object (a stream of records). +std::vector json_lines(std::string_view text); + +// ---- executables ------------------------------------------------------------------------------------- + +enum class format { unknown, elf, pe, macho, macho_fat, archive, wasm }; + +struct header { + format kind = format::unknown; + bool wide = false; // ELF class 64, PE32+, a 64-bit Mach-O + std::uint32_t machine = 0; // e_machine, the COFF machine, cputype + std::uint16_t subsystem = 0; // PE: the optional header's subsystem + std::uint16_t elf_type = 0; // ET_EXEC, ET_DYN, ... + bool elf_interpreter = false; // a PT_INTERP segment: a dynamic program + std::string interpreter; // its path + std::vector needed; // ELF DT_NEEDED + std::string runpath; // ELF DT_RUNPATH or DT_RPATH +}; + +// The header of an executable, an object or a library, read by this process (no `file`, +// `readelf` or `llvm-readobj`). +header read_header(const fs::path& file); + +namespace elf_machine { +inline constexpr std::uint32_t x86 = 3, arm = 40, x86_64 = 62, aarch64 = 183, riscv = 243; +} +namespace pe_machine { +inline constexpr std::uint32_t i386 = 0x14c, x86_64 = 0x8664, arm64 = 0xaa64; +} + +} // namespace mcpp::test::support From 7a22f9651a2819c4c1ebf5bfb25c1cb44b5e25a4 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 13 Nov 2026 17:04:45 +0900 Subject: [PATCH 003/115] e2e: the migration inventory of the 752 scripts and the keep baseline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tests/e2e/MIGRATION.tsv gives each script its subsystem (25 directory-style names), its class (sink, merge, keep; design 2026-11-12 §5.9) and its planned destination: e2e/ for a merged case, unit/ for a sunk one, keep for a script that stays. Assigned from each script's name, header and the commands it runs. merge 707, sink 12, keep 33. tests/e2e/keep.tsv is the baseline of the keep class (emulators, a physical device, upgrades and machine-state homes, CI-only and measurement legs); it may only shrink. --- tests/e2e/MIGRATION.tsv | 753 ++++++++++++++++++++++++++++++++++++++++ tests/e2e/keep.tsv | 34 ++ 2 files changed, 787 insertions(+) create mode 100644 tests/e2e/MIGRATION.tsv create mode 100644 tests/e2e/keep.tsv diff --git a/tests/e2e/MIGRATION.tsv b/tests/e2e/MIGRATION.tsv new file mode 100644 index 000000000..274e85c1f --- /dev/null +++ b/tests/e2e/MIGRATION.tsv @@ -0,0 +1,753 @@ +script subsystem class destination note +00_fixture_path_hygiene.sh repo keep keep lints the bash scripts' own fixture paths (Git Bash path forms); retires with the last script +01_help_and_version.sh cli merge e2e/cli +02_new_build_run.sh build merge e2e/basics already has its C++ twin (e2e/basics::02 a new program is scaffolded...); script to be removed with the build batch +03_multi_module.sh modules merge e2e/modules +04_incremental.sh build merge e2e/build +05_errors.sh cli merge e2e/cli +06_emit_xpkg.sh index sink unit/test_emit_xpkg answers a parse/selection rule without a build: Lua entry from a manifest +07_static_library.sh build merge e2e/build +08_shared_library.sh build merge e2e/build +09_path_dependency.sh deps merge e2e/deps +10_env_command.sh self merge e2e/self +11_index_list.sh index merge e2e/index +12_add_command.sh deps merge e2e/deps +13_toolchain_pin.sh toolchain merge e2e/toolchain +14_toolchain_fallback.sh toolchain merge e2e/toolchain +15_test_passing.sh test merge e2e/test +16_test_failing.sh test merge e2e/test +17_test_no_tests.sh test merge e2e/test +18_devdeps_isolation.sh test merge e2e/test +19_bmi_cache_reuse.sh modules merge e2e/modules +20_p1689_scanner.sh modules merge e2e/modules +21_ninja_dyndep.sh modules merge e2e/modules +22_doctor_cache_publish.sh index merge e2e/index +23_remove_update.sh deps sink unit/test_manifest_edit answers a parse/selection rule without a build: remove/update rewrite mcpp.toml +24_git_dependency.sh deps merge e2e/deps +25_convention_mode.sh build merge e2e/build +26_c_language_support.sh build merge e2e/build +26_toolchain_management.sh toolchain merge e2e/toolchain +27_namespace_dependencies.sh deps merge e2e/deps +27_self_contained_home.sh self keep keep home located from the installed binary's own directory (/bin/mcpp): a release-layout install +28_target_static.sh target merge e2e/target +29_toolchain_partial_versions.sh toolchain merge e2e/toolchain +30_dev_binary_home.sh self keep keep a dev binary's home when MCPP_HOME is unset: needs the real ~/.mcpp of the machine +30_pack_modes.sh pack merge e2e/pack +31_transitive_deps.sh deps merge e2e/deps +32_semver_merge.sh deps merge e2e/deps +33_multi_version_mangling.sh deps merge e2e/deps +35_workspace.sh workspace merge e2e/workspace +36_llvm_toolchain.sh toolchain merge e2e/toolchain +37_llvm_import_std.sh toolchain merge e2e/toolchain +38_llvm_modules.sh toolchain merge e2e/toolchain +38_self_config_mirror.sh self merge e2e/self +39_llvm_incremental.sh toolchain merge e2e/toolchain +39_xlings_index_migration.sh self merge e2e/self +40_llvm_bmi_cache.sh toolchain merge e2e/toolchain +41_llvm_std_compat.sh toolchain merge e2e/toolchain +42_custom_local_index.sh index sink unit/test_indices answers a parse/selection rule without a build: the [indices] forms +43_indices_lockfile.sh index sink unit/test_lockfile answers a parse/selection rule without a build: v2 format, v1 migration, pin/unpin +44_indices_e2e_integration.sh index sink unit/test_indices answers a parse/selection rule without a build: workspace inheritance of [indices] +45_install_platform_mapping.sh repo sink unit/test_repo_consistency answers a parse/selection rule without a build: install.sh platform ids vs release assets +46_self_config_mirror_no_bootstrap.sh self merge e2e/self +47_cdb_prebuilt_module_path_abs.sh cdb merge e2e/cdb +47_llvm_atomic_link.sh toolchain merge e2e/toolchain +48_build_error_output.sh cli merge e2e/cli +49_bmi_cache_nested_custom_index.sh modules merge e2e/modules +50_package_owned_build_flags.sh build merge e2e/build +51_package_generated_files.sh actions merge e2e/actions +52_local_path_namespaced_index.sh index merge e2e/index +53_namespaced_cache_label.sh index merge e2e/index +54_package_owned_ldflags.sh build merge e2e/build +55_dependency_shared_artifact.sh runtime merge e2e/runtime +56_transitive_shared_artifact.sh runtime merge e2e/runtime +57_static_dep_shared_artifact.sh runtime merge e2e/runtime +58_preinstall_mcpp_deps_for_hooks.sh build-program merge e2e/build-program +59_cpp_standard_config.sh toolchain merge e2e/toolchain +60_stale_xpkg_cache_reinstall.sh index merge e2e/index +61_dependency_visibility.sh deps merge e2e/deps +62_dotted_dependency_selector_priority.sh deps merge e2e/deps +62_runtime_library_dirs.sh run merge e2e/run +63_bare_dependency_peer_root_priority.sh deps merge e2e/deps +64_shared_soname_runtime_alias.sh runtime merge e2e/runtime +65_toolchain_runtime_dirs_for_run.sh run merge e2e/run +66_runtime_provides.sh runtime merge e2e/runtime +67_features_strict.sh features merge e2e/features +68_profile_passthrough.sh build merge e2e/build +69_package_templates.sh cli merge e2e/cli +70_per_target_flags.sh build merge e2e/build +71_required_features_gate.sh features merge e2e/features +72_test_profile_features.sh test merge e2e/test +73_issue131_per_target_cxxflag.sh build merge e2e/build +74_run_no_loader_env_leak.sh run merge e2e/run +75_index_status_offline.sh index merge e2e/index +76_compile_commands_generated.sh cdb merge e2e/cdb +76_qualified_custom_ns_noncanonical_filename.sh index merge e2e/index +77_cdb_preserves_test_entries.sh cdb merge e2e/cdb +78_test_main_combinations.sh test merge e2e/test +79_gtest_regular_dep_feature_main.sh deps merge e2e/deps +80_feature_defines.sh features merge e2e/features +81_capability_binding.sh features merge e2e/features +82_feature_optional_deps.sh features merge e2e/features +83_feature_defines_propagate.sh features merge e2e/features +84_runtime_dll_deploy.sh windows merge e2e/windows +85_target_cfg_build_flags.sh features merge e2e/features +86_llvm_hermetic_link.sh toolchain merge e2e/toolchain +86_target_cfg_dependencies.sh features merge e2e/features +87_build_default_profile.sh build merge e2e/build +88_xlings_environment.sh self merge e2e/self +89_build_mcpp.sh build-program merge e2e/build-program +90_workspace_test.sh test merge e2e/test +91_target_bare_alias.sh target merge e2e/target +92_build_mcpp_import.sh build-program merge e2e/build-program +93_xpkg_parse.sh index sink unit/test_xpkg_parse answers a parse/selection rule without a build: descriptor grammar, strict keys +94_macos_test_stdlib.sh test merge e2e/test +95_msvc_system_toolchain.sh windows merge e2e/windows +96_msvc_unavailable_nonwindows.sh windows merge e2e/windows +97_mingw_toolchain.sh windows merge e2e/windows +98_reflection_import_std.sh toolchain merge e2e/toolchain +99_msvc_native_build.sh windows merge e2e/windows +100_cppfly_reflection.sh toolchain merge e2e/toolchain +100_feature_sources_test_mode.sh features merge e2e/features +101_cppfly_llvm_soft.sh toolchain merge e2e/toolchain +102_mingw_cross_wine.sh windows keep keep emulator: runs the cross-built PE under wine +103_target_vocabulary.sh target merge e2e/target +104_asm_sources_gas.sh build merge e2e/build +105_asm_sources_nasm.sh build merge e2e/build +106_feature_gated_sources_toml.sh features merge e2e/features +107_generated_files_toml.sh actions merge e2e/actions +108_cfg_conditional_sources.sh features merge e2e/features +109_per_glob_flags.sh build merge e2e/build +110_build_mcpp_env_contract.sh build-program merge e2e/build-program +111_dep_build_mcpp.sh build-program merge e2e/build-program +112_build_mcpp_cross.sh build-program merge e2e/build-program +113_scanner_mcpp_dir_prune.sh modules merge e2e/modules +114_run_scan_scope.sh run merge e2e/run +115_manifest_syntax_aot_brace.sh build merge e2e/build +116_include_family_and_define_spaces.sh build merge e2e/build +117_object_path_collision.sh build merge e2e/build +118_purview_include_rebuild.sh modules merge e2e/modules +119_dep_cfg_sources.sh features merge e2e/features +120_ws_root_indices.sh index merge e2e/index +121_default_ns_redirect.sh index merge e2e/index +122_run_member.sh workspace merge e2e/workspace +123_same_named_main_across_dep.sh build merge e2e/build +124_gen_source_out_of_root_object_path.sh actions merge e2e/actions +125_build_mcpp_dep_dir.sh build-program merge e2e/build-program +126_default_features_opt_out.sh features merge e2e/features +127_transitive_default_features_optout.sh features merge e2e/features +128_feature_forwarding.sh features merge e2e/features +130_freestanding_riscv_build_and_run.sh baremetal keep keep emulator: boots the image in qemu-system-riscv64 +131_freestanding_bsp_supplies_everything.sh baremetal keep keep emulator: boots the image in qemu-system-riscv64 +132_freestanding_test_and_artifacts.sh baremetal keep keep emulator: runs the freestanding tests in qemu-system-riscv64 +133_freestanding_std_subset.sh baremetal keep keep emulator: boots the image in qemu-system-riscv64 +134_target_sysroot_and_queries.sh target merge e2e/target +135_diagnostic_promise_resolves.sh toolchain merge e2e/toolchain +136_aarch64_zero_libc_target.sh baremetal merge e2e/baremetal +137_x86_64_zero_libc_target.sh baremetal merge e2e/baremetal +138_feature_sources_gate_vs_provider.sh features merge e2e/features +139_build_program_advisory.sh build-program merge e2e/build-program +141_include_dirs_after.sh build merge e2e/build +142_generated_files_incremental.sh actions merge e2e/actions +143_build_mcpp_source_directive.sh build-program merge e2e/build-program +144_build_mcpp_include_dir.sh build-program merge e2e/build-program +145_root_build_mcpp_dep_dirs.sh build-program merge e2e/build-program +146_feature_flags.sh features merge e2e/features +147_per_os_features_xpkg.sh index merge e2e/index +148_wide_include_list.sh build merge e2e/build +149_conditional_glob_flags.sh features merge e2e/features +150_clang_module_operator_template.sh toolchain keep keep a state canary of the bundled Clang (expected crash), not an assertion about mcpp +151_index_url_artifact_migration.sh self merge e2e/self +152_test_subdir_names.sh test merge e2e/test +153_test_isolation.sh test merge e2e/test +154_test_filter.sh test merge e2e/test +155_test_json.sh test merge e2e/test +156_nested_ld_library_path.sh run merge e2e/run +157_test_glob_flags.sh test merge e2e/test +158_test_signal_and_duration.sh test merge e2e/test +159_test_list.sh test merge e2e/test +160_test_timeout.sh test merge e2e/test +161_xpkg_name_form.sh deps merge e2e/deps +162_bare_name_namespace_scope.sh deps merge e2e/deps +163_identity_first_resolution.sh deps merge e2e/deps +164_ninja_goal_command_length.sh test merge e2e/test +165_bare_name_cross_namespace_wire_address.sh deps merge e2e/deps +166_run_env_no_private_glibc.sh run merge e2e/run +167_build_defines_module_scan.sh modules merge e2e/modules +168_build_mcpp_musl_host_static.sh build-program merge e2e/build-program +169_semver_project_index.sh index merge e2e/index +170_bmi_staging_no_cascade.sh modules merge e2e/modules +171_bmi_staging_locked_dest.sh modules merge e2e/modules +172_build_cache_cross_project.sh cache merge e2e/cache +173_build_profile_isolation.sh cache merge e2e/cache +173_index_refresh_policy.sh index merge e2e/index +174_cache_modes_and_commands.sh cache merge e2e/cache +175_cpp20_import_std.sh toolchain merge e2e/toolchain +176_cpp20_std_bmi_isolation.sh toolchain merge e2e/toolchain +177_cpp20_msvc.sh windows merge e2e/windows +178_test_observability.sh test merge e2e/test +179_spaced_paths.sh build merge e2e/build +180_msvc_build_mcpp.sh windows merge e2e/windows +181_build_mcpp_import_std.sh build-program merge e2e/build-program +182_windows_no_msvc_fallback.sh windows merge e2e/windows +183_cross_no_relink.sh windows merge e2e/windows +183_cxx_runtime_contract.sh runtime merge e2e/runtime +184_build_cache_object_layout.sh cache merge e2e/cache +185_index_floor_degrades.sh index merge e2e/index +186_build_mcpp_protocol_and_bound.sh build-program merge e2e/build-program +187_dep_host_tool.sh build-program merge e2e/build-program +188_build_actions.sh actions merge e2e/actions +189_host_module_rules.sh build-program merge e2e/build-program +190_link_rspfile_newlines.sh build merge e2e/build +191_link_scale.sh build merge e2e/build +192_install_path_namespace.sh index merge e2e/index +193_provision_reexport.sh build-program merge e2e/build-program +194_build_mcpp_glob_input.sh build-program merge e2e/build-program +195_target_cfg_feature_deps.sh features merge e2e/features +196_version_identity_and_lock.sh deps merge e2e/deps +197_windows_resources.sh windows merge e2e/windows +198_windows_resources_cross.sh windows merge e2e/windows +199_resources_validation.sh windows merge e2e/windows +200_subos_env_reaches_program.sh runtime merge e2e/runtime +201_gcc_no_specs_pollution.sh toolchain merge e2e/toolchain +202_machine_output_contract.sh cli merge e2e/cli +203_exact_selector_lock_migration.sh deps merge e2e/deps +204_new_transactional_scaffold.sh cli merge e2e/cli +205_root_local_subos.sh self merge e2e/self +206_runtime_binding_physics.sh runtime merge e2e/runtime +207_runtime_contract_provenance.sh runtime merge e2e/runtime +208_private_libc_stays_in_the_binary.sh runtime merge e2e/runtime +209_runtime_verdict_never_reruns_green.sh runtime merge e2e/runtime +210_local_index_addressing_on_every_host.sh index merge e2e/index +211_configure_only_cdb.sh cdb merge e2e/cdb +212_cached_dep_std_is_ordered.sh cache merge e2e/cache +213_build_after_test_is_not_the_test_graph.sh test merge e2e/test +214_executable_carries_dt_rpath.sh runtime merge e2e/runtime +215_pack_has_no_build_machine_paths.sh pack merge e2e/pack +216_selfcontained_refuses_host_capability.sh pack merge e2e/pack +217_module_extensions.sh modules merge e2e/modules +218_module_extensions_graph_shape.sh modules merge e2e/modules +219_runtime_search_farm_is_last.sh runtime merge e2e/runtime +220_farm_only_library_runs.sh runtime merge e2e/runtime +221_subos_without_info_still_builds.sh self merge e2e/self +222_shared_library_cxx_runtime_contract.sh runtime merge e2e/runtime +230_bench_harness.sh repo keep keep bench harness; converges with bench/ (design 5.9 bench), not the e2e migration +231_jobs_option.sh cli merge e2e/cli +232_workflow_syntax.sh repo keep keep lints .github/workflows YAML (CI configuration, needs a YAML reader) +233_bench_matrix.sh repo keep keep bench/matrix.json single-source check; converges with bench/ (design 5.9 bench) +234_bmi_schedule_on.sh modules merge e2e/modules +235_std_object_only_when_needed.sh toolchain merge e2e/toolchain +236_module_extensions_default_toolchain.sh modules merge e2e/modules +237_default_subos_without_info.sh self merge e2e/self +238_c_only_unit_links_with_c_driver.sh build merge e2e/build +239_msvc_managed_toolset.sh windows keep keep CI-only: xlings-managed MSVC (opt-in MCPP_E2E_XLINGS_MSVC, ~380 MB download) +240_pack_pe_zip_cross.sh windows merge e2e/windows +241_windows_ucrt_runtime_identity.sh windows merge e2e/windows +242_pack_library_interface_and_headers.sh pack merge e2e/pack +243_pack_library_interface_closure.sh pack merge e2e/pack +244_pack_library_gate.sh pack merge e2e/pack +245_pack_library_fat_target_selection.sh pack merge e2e/pack +246_explicit_empty_sources.sh build merge e2e/build +247_bare_triple_conditional_native.sh features merge e2e/features +248_pack_library_fat_pe_leg.sh windows merge e2e/windows +249_pack_workspace_root_unchanged.sh pack merge e2e/pack +250_pack_names_the_target.sh pack merge e2e/pack +251_pack_library_shared.sh pack merge e2e/pack +252_pack_library_old_client.sh pack merge e2e/pack +253_pack_library_undetermined_partition.sh pack merge e2e/pack +254_pack_library_platform_coverage.sh pack merge e2e/pack +255_pack_library_msvc_archiver.sh windows merge e2e/windows +256_pack_library_fat_windows.sh windows merge e2e/windows +257_shared_library_pe.sh windows keep keep emulator: runs the PE DLL program under wine +258_shared_library_msvc_auto_def.sh windows merge e2e/windows +259_shared_library_macho.sh macos merge e2e/macos +260_module_extension_is_configuration.sh modules merge e2e/modules +261_pack_follows_the_module_extension.sh pack merge e2e/pack +262_pack_consumed_by_native_cl.sh windows merge e2e/windows +263_lib_root_follows_the_extension.sh modules merge e2e/modules +264_pack_library_is_relocatable.sh pack merge e2e/pack +265_pack_strips_but_stays_usable.sh pack merge e2e/pack +266_pack_reads_a_macho_program_without_running_it.sh macos merge e2e/macos +267_target_requested_is_empty_for_a_native_build.sh target merge e2e/target +268_target_side_resolution.sh target merge e2e/target +269_openkal_llvm_spelling_still_resolves.sh openkal merge e2e/openkal +280_target_side_layers.sh target merge e2e/target +281_target_side_rules.sh target merge e2e/target +282_target_is_a_request.sh target merge e2e/target +283_run_target_flag_owns_its_cache_slot.sh target merge e2e/target +284_env_segment_is_optional_everywhere.sh target merge e2e/target +285_kernel_abi_from_graph_keeps_the_payload_c_library.sh openkal merge e2e/openkal +286_the_openkal_stack_still_builds.sh openkal merge e2e/openkal +287_the_openkal_stack_crosses_to_aarch64.sh openkal merge e2e/openkal +288_the_openkal_stack_on_a_machine_with_no_os.sh openkal keep keep emulator: boots the openkal stack in qemu-system-riscv64 +289_one_host_reaches_every_openkal_target.sh openkal merge e2e/openkal +290_the_declaration_puts_an_environment_in_front.sh target merge e2e/target +291_dynamic_linkage_is_refused_only_when_the_c_library_is_the_graphs.sh target merge e2e/target +292_a_package_that_names_a_layer_does_not_lose_the_targets_compiler.sh target merge e2e/target +293_the_requested_target_and_the_resolved_one_name_one_os.sh target merge e2e/target +294_the_list_answers_what_can_be_built_not_what_has_a_payload.sh target merge e2e/target +295_naming_the_host_target_changes_nothing.sh target merge e2e/target +296_what_the_report_names_is_what_the_link_line_uses.sh target merge e2e/target +297_a_capability_pin_is_not_a_preference.sh target merge e2e/target +298_overriding_a_convention_requires_replacing_it.sh target merge e2e/target +299_a_request_that_named_no_c_library_resolves_to_a_row_that_exists.sh target merge e2e/target +300_a_registered_family_is_not_reported_unknown.sh target sink unit/test_target_vocabulary answers a parse/selection rule without a build: registered families +301_the_graphs_compiler_is_taken_and_nothing_is_written.sh target merge e2e/target +302_a_stated_compiler_outranks_the_graph_and_two_requirements_do_not_stack.sh target sink unit/test_toolchain_rank answers a parse/selection rule without a build: stated compiler over the graph +303_the_query_gives_one_answer_for_the_c_library.sh target sink unit/test_why_toolchain answers a parse/selection rule without a build: one C-library answer +304_the_target_side_reaches_every_unit.sh target merge e2e/target +305_built_from_is_not_published.sh openkal merge e2e/openkal +306_dependency_linkage_axis.sh runtime merge e2e/runtime +307_symbol_provision.sh runtime merge e2e/runtime +308_dependency_required_features.sh features merge e2e/features +309_host_module_identity.sh build-program merge e2e/build-program +310_build_dependencies.sh build-program merge e2e/build-program +311_rule_build_dependencies.sh build-program merge e2e/build-program +312_build_rules_example.sh build-program merge e2e/build-program +313_check_stamp_on_every_platform.sh actions merge e2e/actions +314_dependency_action_generated_header.sh actions merge e2e/actions +315_blocking_check_gates_compilation.sh actions merge e2e/actions +316_link_unit_with_no_inputs_is_refused.sh build merge e2e/build +317_project_build_hooks.sh actions merge e2e/actions +321_workspace_inheritance.sh workspace merge e2e/workspace +322_dialect_flag_refusal.sh toolchain merge e2e/toolchain +323_post_link_record_durability.sh runtime merge e2e/runtime +324_fast_path_sees_path_deps.sh cache merge e2e/cache +325_system_toolchain_refused.sh toolchain merge e2e/toolchain +326_dependency_standard_floor.sh deps merge e2e/deps +327_declared_xlings_deps_report_their_own_failure.sh index merge e2e/index +328_layer_predicates_apply_and_unknown_ones_say_so.sh features merge e2e/features +329_exit_codes_match_the_contract.sh cli merge e2e/cli +330_runner_hosted_targets.sh run merge e2e/run +331_xlings_workspace_one_table.sh index merge e2e/index +332_cortex_m_builds_and_boots.sh baremetal keep keep emulator: boots the Cortex-M image in qemu-system-arm +333_named_runners_locked_and_sbom.sh run merge e2e/run +334_dependency_declared_tool_is_found.sh build-program merge e2e/build-program +335_xlings_tool_tiers.sh index merge e2e/index +336_armv7a_builds_and_boots.sh baremetal keep keep emulator: boots the armv7-a image in qemu-system-arm +337_run_takes_features_and_profile.sh run merge e2e/run +338_cortex_m_picolibc_sysroot.sh baremetal keep keep emulator: boots the Cortex-M picolibc image in qemu-system-arm +339_run_reports_the_program_status.sh run merge e2e/run +340_no_orphan_survives_a_killed_mcpp.sh cli merge e2e/cli +600_accel_variant_selection.sh build-program merge e2e/build-program +601_exclusive_capability.sh features merge e2e/features +603_version_floor.sh deps merge e2e/deps +604_offline_skips_sandbox_bootstrap.sh self merge e2e/self +605_probe_channel_and_accel_reach_the_build_program.sh build-program merge e2e/build-program +606_constrained_source_globs_narrow_to_the_build.sh actions merge e2e/actions +607_chained_actions_form_the_device_link.sh actions merge e2e/actions +608_object_actions_reach_a_static_library.sh actions merge e2e/actions +609_clean_stale.sh cache merge e2e/cache +609_shader_sources_are_device_sources.sh actions merge e2e/actions +610_feature_controlled_rule_collection.sh actions merge e2e/actions +611_variant_switch_is_not_replayed.sh actions merge e2e/actions +612_build_program_file_input_fast_path.sh build-program merge e2e/build-program +613_sycl_sources_are_device_sources.sh actions merge e2e/actions +614_feature_xlings_reaches_xpkg_dir.sh index merge e2e/index +615_feature_deps_shared_library_is_linked.sh features merge e2e/features +616_examples_and_their_index_agree.sh repo sink unit/test_repo_consistency answers a parse/selection rule without a build: examples vs their index +617_build_program_knows_the_cxx_stdlib.sh build-program merge e2e/build-program +618_dependency_feature_tools_reach_its_build_program.sh build-program merge e2e/build-program +619_a_tool_may_not_be_conditioned_on_a_layer.sh actions merge e2e/actions +620_link_flag_reaches_the_link_line.sh build merge e2e/build +621_exports_narrows_the_symbol_surface.sh runtime merge e2e/runtime +622_rule_declared_payload_reaches_the_consumers_build_program.sh build-program merge e2e/build-program +623_device_sources_must_reach_an_action.sh actions merge e2e/actions +624_a_missing_host_module_names_the_key.sh build-program merge e2e/build-program +625_a_payload_can_be_gated_on_the_accelerator.sh build-program merge e2e/build-program +626_naming_a_subset_of_backends_is_not_a_mismatch.sh build-program merge e2e/build-program +627_one_package_one_version.sh deps merge e2e/deps +628_a_pin_below_a_stated_floor_is_refused.sh deps merge e2e/deps +629_a_version_range_is_answered_by_xpkg_dir.sh deps merge e2e/deps +630_a_rule_brings_its_own_environment.sh actions merge e2e/actions +631_action_depfile_tracks_a_runtime_discovered_include.sh actions merge e2e/actions +632_a_build_program_is_told_its_package_name.sh build-program merge e2e/build-program +633_host_modules_are_ordered_by_what_they_import.sh build-program merge e2e/build-program +634_a_private_module_fragment_is_not_a_partition.sh modules merge e2e/modules +635_a_byte_order_mark_does_not_hide_a_module.sh modules merge e2e/modules +636_a_module_extension_file_need_not_provide_a_module.sh modules merge e2e/modules +637_a_conditional_runtime_table_alone_is_applied.sh features merge e2e/features +638_pack_format_dispatch.sh pack merge e2e/pack +639_the_scanner_does_not_read_inside_a_comment.sh modules merge e2e/modules +640_a_capability_pin_explains_its_own_row.sh target merge e2e/target +641_the_android_rows_are_wired_and_the_simulator_is_a_row.sh android merge e2e/android +642_windows_subsystem.sh windows merge e2e/windows +643_windows_subsystem_cross.sh windows merge e2e/windows +644_windows_keys_are_inert_elsewhere.sh windows merge e2e/windows +645_the_fast_path_compares_the_toolchain_request.sh cache merge e2e/cache +646_runtime_deploy_places_files_in_a_directory.sh runtime merge e2e/runtime +647_abi_threads_is_one_switch_for_the_graph.sh target merge e2e/target +648_an_install_hook_sees_the_build_target.sh actions merge e2e/actions +649_pack_carries_runtime_deploy_files.sh pack merge e2e/pack +650_wasm_row_names_its_launcher.sh web merge e2e/web +651_a_build_program_deploys_what_it_generated.sh build-program merge e2e/build-program +652_an_application_is_a_kind.sh pack merge e2e/pack +652b_an_application_on_android_is_a_shared_library.sh android merge e2e/android +653_exceptions_is_the_second_abi_member.sh target merge e2e/target +654_a_requirement_on_the_target_axis.sh features merge e2e/features +656_run_hands_the_distributable_to_the_runner.sh run merge e2e/run +657_a_build_program_under_the_web_row_uses_the_host_compiler.sh web merge e2e/web +658_a_program_runs_on_an_attached_android_device.sh android keep keep a physical Android device over adb +659_an_action_declaration_has_no_size_limit.sh actions merge e2e/actions +660_an_action_names_the_engine_through_mcpp_self.sh actions merge e2e/actions +661_one_identity_two_declarations.sh deps merge e2e/deps +662_pack_stages_declared_files_before_the_closure.sh pack merge e2e/pack +663_a_graph_libcxx_over_the_payloads_c_library.sh toolchain merge e2e/toolchain +664_a_universal_apk_is_two_legs_in_one_tree.sh android merge e2e/android +665_a_host_tool_follows_its_source.sh build-program merge e2e/build-program +666_a_macho_program_reaches_a_dispatched_format_with_its_tree.sh macos merge e2e/macos +667_an_android_pack_stages_its_closure.sh android merge e2e/android +668_a_macho_program_is_staged_with_its_closure.sh macos merge e2e/macos +669_an_elf_host_pack_states_its_closure.sh pack merge e2e/pack +670_an_rpath_loader_token_is_left_as_written.sh runtime merge e2e/runtime +671_toolchain_is_an_option_of_run_test_and_pack.sh cli merge e2e/cli +672_a_runner_receives_the_files_the_artifact_carries.sh run merge e2e/run +673_test_discover_names_where_tests_are.sh test merge e2e/test +674_a_format_is_run_by_the_runner_named_after_it.sh run merge e2e/run +675_android_test_programs_carry_their_cxx_runtime.sh android merge e2e/android +677_a_conditional_dependency_replaces_the_unconditional_one.sh features merge e2e/features +678_a_row_states_a_library_form.sh target merge e2e/target +679_a_path_dependency_takes_its_manifests_identity.sh deps merge e2e/deps +680_a_dependency_floor_on_the_android_api_level.sh android merge e2e/android +681_index_repos_reach_an_existing_home.sh self merge e2e/self +682_why_deps_prints_the_resolved_graph.sh deps merge e2e/deps +683_a_build_program_reads_the_payload_pkg_config_view.sh build-program merge e2e/build-program +684_a_runner_supplied_twice_is_refused.sh run merge e2e/run +685_a_nested_test_loads_a_graph_shared_library.sh runtime merge e2e/runtime +686_an_activated_xlings_subos_does_not_redirect_the_registry.sh self merge e2e/self +687_the_vendored_xlings_probe_is_an_argument_vector.sh self merge e2e/self +688_emit_build_database.sh cdb merge e2e/cdb +689_pack_takes_features.sh pack merge e2e/pack +690_a_shared_library_over_a_graph_cxx_runtime.sh runtime merge e2e/runtime +692_a_package_states_its_default_link_form.sh runtime merge e2e/runtime +693_the_root_build_program_reads_dependency_link_forms.sh build-program merge e2e/build-program +696_a_cxx_layer_provider_keeps_its_own_standard.sh toolchain merge e2e/toolchain +698_a_tool_source_under_its_dependency_has_a_bounded_address.sh build-program merge e2e/build-program +700_a_program_over_a_cxx_shared_library_has_one_cxx_runtime.sh runtime merge e2e/runtime +701_symbol_provision_does_not_report_what_the_build_shares_by_construction.sh runtime merge e2e/runtime +702_a_static_package_under_a_shared_library_is_linked_into_it.sh runtime merge e2e/runtime +703_the_llvm_row_on_the_msvc_abi_records_its_static_crt.sh windows merge e2e/windows +704_cxx_identity_across_macho_images.sh macos keep keep a measurement leg (prints a table, asserts nothing) +705_cxx_identity_across_pe_images.sh windows keep keep a measurement leg (prints a table, asserts nothing) +710_a_forward_reaches_every_dependency_table.sh features merge e2e/features +711_a_feature_deps_restatement_names_one_source.sh features merge e2e/features +712_a_tool_package_contributes_nothing_to_its_consumer.sh build-program merge e2e/build-program +713_a_git_dependency_selects_a_repository_member.sh deps merge e2e/deps +714_features_names_a_dependency_feature.sh features merge e2e/features +720_the_root_build_program_reads_the_resolved_graph.sh build-program merge e2e/build-program +721_an_android_row_links_on_a_macos_host.sh macos merge e2e/macos +722_pack_strips_what_the_graph_built.sh pack merge e2e/pack +723_pack_reports_what_it_produced.sh pack merge e2e/pack +730_bare_compat_dependency_is_not_a_refresh_miss.sh index merge e2e/index +731_planning_children_do_not_inherit_the_callers_pipe.sh cdb merge e2e/cdb +732_xlings_refresh_is_bounded_and_owned.sh index merge e2e/index +733_envelope_reports_observed_network_access.sh index merge e2e/index +734_auto_refresh_false_governs_first_index_sync.sh index merge e2e/index +735_offline_missing_package_has_its_own_code.sh index merge e2e/index +736_compile_flag_words_reach_the_compiler_and_the_databases.sh cdb merge e2e/cdb +737_the_declared_runtime_payload_is_installed.sh index merge e2e/index +738_a_graph_supplied_target_closes_the_hosts_own_search.sh target merge e2e/target +739_a_private_feature_dep_does_not_reach_the_consumer.sh features merge e2e/features +740_gcc_over_a_graph_c_library_is_degraded_not_refused.sh toolchain merge e2e/toolchain +741_c_abi_environment_declared_by_the_c_library.sh target merge e2e/target +742_gcc_accepted_when_the_realisation_is_empty.sh toolchain merge e2e/toolchain +743_kernel_abi_interfaces_are_resolved_not_preprocessed.sh openkal merge e2e/openkal +744_a_declared_absence_explains_the_link_that_asked_for_it.sh target merge e2e/target +745_no_run_builds_the_tests_and_says_so.sh test merge e2e/test +746_macos_deployment_target_is_target_keyed_not_host_keyed.sh macos merge e2e/macos +760_msvc_toolset_is_chosen_once.sh windows merge e2e/windows +765_a_consumer_include_directory_stays_in_the_consumer.sh build merge e2e/build +770_workspace_member_as_dependency.sh workspace merge e2e/workspace +772_a_published_member_is_self_contained.sh workspace merge e2e/workspace +773_commands_outside_the_build_read_the_effective_manifest.sh workspace merge e2e/workspace +774_an_index_member_inherits_its_archive_workspace.sh workspace merge e2e/workspace +775_a_host_tool_merges_its_conditional_sections_once.sh build-program merge e2e/build-program +776_a_path_with_no_utf8_spelling_is_named.sh build merge e2e/build +777_c_standard_applies_to_the_package_that_declares_it.sh build merge e2e/build +778_a_graph_link_searches_no_host_directory.sh target merge e2e/target +779_a_build_program_declares_a_runtime_search_dir.sh build-program merge e2e/build-program +780_a_passing_check_moves_its_stamp_past_a_changed_input.sh actions merge e2e/actions +781_deleted_root_cdb_is_restored.sh cdb merge e2e/cdb +782_foreign_root_cdb_entries_are_replaced.sh cdb merge e2e/cdb +783_cdb_switches_whole_with_the_configuration.sh cdb merge e2e/cdb +784_cdb_replays_from_its_directory.sh cdb merge e2e/cdb +785_cdb_interface_flag_module_extensions.sh cdb merge e2e/cdb +786_std_unit_in_the_database_and_build_id.sh cdb merge e2e/cdb +787_emit_plans_every_member_on_its_own.sh cdb merge e2e/cdb +788_emit_defers_an_unbuilt_host_tool.sh cdb merge e2e/cdb +789_emit_describes_a_member_past_a_failed_build_program.sh cdb merge e2e/cdb +790_a_prepare_action_populates_an_unknown_directory.sh actions merge e2e/actions +791_a_prepare_action_that_creates_nothing_fails.sh actions merge e2e/actions +792_a_rerun_input_inside_a_prepare_directory_is_warned.sh actions merge e2e/actions +793_an_unknown_action_role_is_refused.sh actions merge e2e/actions +794_a_windows_program_finds_a_dll_through_runtime_search_dir.sh windows merge e2e/windows +795_a_link_flag_reaches_the_linker_as_written.sh build merge e2e/build +796_runtime_search_dir_on_pe_under_wine.sh windows keep keep emulator: runs the PE program under wine +797_a_windows_program_finds_its_dlls_beside_it.sh windows keep keep emulator: runs the PE program under wine +798_a_rule_applies_through_a_source_it_claims.sh actions merge e2e/actions +799_an_action_runs_with_its_env_and_cwd.sh actions merge e2e/actions +800_a_feature_provides_its_host_tools.sh build-program merge e2e/build-program +801_a_dependency_program_is_shipped_with_the_consumer.sh pack merge e2e/pack +802_a_host_build_applies_its_host_row.sh build-program merge e2e/build-program +803_a_recorded_payload_that_is_gone_is_refused_offline.sh index merge e2e/index +804_a_path_host_tool_builds_with_its_chosen_toolchain.sh build-program merge e2e/build-program +805_rooted_workspace_reaches_its_own_path_dependency.sh workspace merge e2e/workspace +806_dash_p_resolves_the_package_first.sh workspace merge e2e/workspace +807_a_lib_root_imports_its_own_package.sh modules merge e2e/modules +808_device_source_is_not_a_compile_unit.sh actions merge e2e/actions +809_failed_build_program_hides_no_device_source.sh cdb merge e2e/cdb +810_two_packages_deploy_the_same_bytes_to_one_name.sh runtime merge e2e/runtime +811_place_dlls_never_overwrites_a_declared_deploy.sh windows merge e2e/windows +812_an_index_floor_is_a_closing_tip.sh index merge e2e/index +813_conditional_dialect_cxxflags_root_only.sh features merge e2e/features +814_the_llvm_row_defaults_to_the_dynamic_crt.sh windows merge e2e/windows +815_the_database_describes_what_a_rule_generates.sh cdb merge e2e/cdb +816_a_git_dependency_reports_its_fetch.sh deps merge e2e/deps +817_emit_writes_no_xlings_json_under_private_work_dir.sh cdb merge e2e/cdb +818_a_declared_deploy_outranks_a_search_dir_dll_cross.sh windows merge e2e/windows +819_the_crt_is_placed_by_its_rule_not_by_search_order.sh windows sink unit/test_runtime_placement answers a parse/selection rule without a build: CRT placed by its rule +820_a_windows_program_takes_its_runtime_from_one_resolver.sh windows merge e2e/windows +821_an_edge_states_its_advice_on_both_paths.sh deps merge e2e/deps +822_a_package_states_the_oldest_mcpp_it_supports.sh deps merge e2e/deps +823_mcpp_core_and_mcpp_name_one_interface.sh build-program merge e2e/build-program +824_a_build_program_reads_the_resolved_toolchain.sh build-program merge e2e/build-program +825_a_build_program_reads_the_msvc_toolset.sh windows merge e2e/windows +826_plugin_diagnostics_features_and_names.sh build-program merge e2e/build-program +827_many_placements_run_as_one_process.sh actions merge e2e/actions +828_a_library_states_its_interface.sh pack merge e2e/pack +829_pack_takes_a_member.sh pack merge e2e/pack +831_a_path_dependency_host_module_outside_src.sh build-program merge e2e/build-program +832_the_fast_path_resumes_after_an_edit.sh cache merge e2e/cache +833_a_workspace_is_one_graph_per_configuration.sh workspace merge e2e/workspace +834_a_workspace_plans_once_and_replays_in_one_check.sh workspace merge e2e/workspace +835_a_shared_library_is_placed_by_a_link.sh runtime merge e2e/runtime +836_a_members_own_statements_reach_its_build.sh workspace merge e2e/workspace +837_a_members_resources_reach_its_image.sh windows merge e2e/windows +838_a_program_shipped_through_artifacts_links_its_own_closure.sh runtime merge e2e/runtime +839_a_workspaces_build_programs_are_named_ordered_and_cached.sh workspace merge e2e/workspace +840_a_workspace_database_describes_each_package_once.sh cdb merge e2e/cdb +841_pack_summarizes_many_outputs.sh pack merge e2e/pack +842_the_build_reports_each_step_once.sh cli merge e2e/cli +843_a_terminal_names_the_action_that_runs.sh cli merge e2e/cli +844_a_planned_build_keeps_one_lock_entry_per_identity.sh deps merge e2e/deps +845_play_game_restores_the_terminal.sh cli merge e2e/cli +846_the_vendored_xlings_is_replaced_from_the_newest_source.sh self keep keep upgrade: the vendored xlings replaced from the newest source in a home +847_a_module_name_is_unique_within_a_program.sh modules merge e2e/modules +848_msvc_binds_a_module_name_per_program.sh windows merge e2e/windows +849_a_staged_bmi_waits_for_a_module_compiled_here.sh modules merge e2e/modules +850_a_selected_member_declares_as_the_root.sh workspace merge e2e/workspace +851_a_shared_member_is_compiled_once_per_configuration.sh workspace merge e2e/workspace +852_a_repeated_dash_p_selects_every_member_it_names.sh workspace merge e2e/workspace +853_run_keeps_one_member_and_exclude_removes_members.sh workspace merge e2e/workspace +854_a_test_over_several_members_plans_and_builds_once.sh test merge e2e/test +855_a_shared_test_build_is_reported_once_per_group.sh test merge e2e/test +856_a_test_over_several_members_continues_past_a_failing_one.sh test merge e2e/test +857_a_host_module_several_members_import_is_compiled_once.sh modules merge e2e/modules +858_the_engines_module_is_compiled_once_per_home.sh modules merge e2e/modules +859_a_host_module_entry_is_reused_while_its_inputs_agree.sh build-program merge e2e/build-program +860_a_workspaces_programs_compile_at_the_same_time.sh workspace merge e2e/workspace +861_concurrent_compiles_plan_what_serial_compiles_plan.sh build merge e2e/build +862_run_q_writes_exactly_the_programs_stdout.sh run merge e2e/run +863_a_run_whose_build_failed_exits_101.sh run merge e2e/run +864_a_builds_status_is_on_standard_error.sh cli merge e2e/cli +865_mcpp_test_reports_its_steps_on_stderr_and_its_results_on_stdout.sh test merge e2e/test +866_both_streams_on_one_pipe_keep_their_order_and_json_stays_clean.sh cli merge e2e/cli +867_a_pack_over_several_members_plans_builds_and_runs_programs_once.sh pack merge e2e/pack +868_a_pack_over_several_members_is_refused_before_anything_is_compiled.sh pack merge e2e/pack +869_a_pack_over_several_members_names_each_member_in_its_report.sh pack merge e2e/pack +870_a_pack_over_several_members_resolves_and_fails_per_member.sh pack merge e2e/pack +871_a_pack_states_its_build_and_every_drive_takes_the_job_count.sh pack merge e2e/pack +872_a_member_compiles_once_whatever_the_selection.sh workspace merge e2e/workspace +873_payload_overrides.sh build-program merge e2e/build-program +874_payload_on_request.sh build-program merge e2e/build-program +875_toolchain_by_path.sh toolchain merge e2e/toolchain +876_toolchain_phase.sh toolchain merge e2e/toolchain +877_why_sources.sh build-program merge e2e/build-program +878_an_upgraded_engine_does_not_replay_the_old_graph.sh cache merge e2e/cache +879_a_providers_module_extension_reaches_the_fast_path.sh cache merge e2e/cache +880_a_members_executable_keeps_its_own_implementation.sh workspace merge e2e/workspace +881_pe_auto_exports_accept_llvm_bitcode.sh windows merge e2e/windows +882_parent_directory_glob_inputs.sh build merge e2e/build +883_run_hands_the_terminal_to_the_program.sh run merge e2e/run +884_a_shared_library_links_with_its_own_closure.sh runtime merge e2e/runtime +885_workspace_profiles_and_the_root_package.sh workspace merge e2e/workspace +886_std_compat_is_built_when_imported.sh toolchain merge e2e/toolchain +887_msvc_lto_and_export_discovery.sh windows merge e2e/windows +888_the_llvm_row_in_a_developer_environment.sh windows merge e2e/windows +889_windows_driver_target_and_coff_resources.sh windows merge e2e/windows +890_a_toolchain_declared_for_an_unservable_target_is_still_installed.sh toolchain merge e2e/toolchain +891_a_host_header_is_written_not_searched.sh toolchain merge e2e/toolchain +892_coroutines_on_the_32_bit_msvc_abi_are_explained.sh windows merge e2e/windows +893_the_workspace_prefix_carries_the_meaning.sh workspace merge e2e/workspace +894_the_position_reading_is_kept_and_reported.sh build merge e2e/build +895_conditional_configuration_rows_count_as_they_evaluate.sh features merge e2e/features +896_an_action_output_has_one_writer.sh actions merge e2e/actions +897_each_configuration_has_its_own_generated_output.sh actions merge e2e/actions +898_module_units_in_cpp_files.sh modules merge e2e/modules +899_toolchain_flag_replacing_a_declared_toolchain_says_so.sh toolchain merge e2e/toolchain +900_publish_writes_the_workspace_layer_into_the_member.sh workspace merge e2e/workspace +901_the_toolchain_library_path_stays_with_the_tools.sh toolchain keep keep container: a host whose /bin/sh is not the toolchain's (termux-style loader) +902_a_build_dependency_library_is_compiled_as_its_package.sh build-program merge e2e/build-program +903_one_package_in_both_tables_has_two_instances.sh build-program merge e2e/build-program +904_a_build_dependency_feature_stays_out_of_the_target.sh build-program merge e2e/build-program +905_a_build_program_links_its_libraries_whole.sh build-program merge e2e/build-program +906_a_build_program_is_compiled_when_its_graph_changes.sh build-program merge e2e/build-program +907_the_library_interface_is_stated_once.sh build merge e2e/build +908_what_a_build_dependency_asks_for.sh build-program merge e2e/build-program +909_deprecated_readings_are_registered_and_kept.sh build merge e2e/build +910_a_build_plugin_is_a_package_of_the_build_program.sh build-program merge e2e/build-program +911_a_workspace_states_each_table_once.sh workspace merge e2e/workspace +912_two_configuration_groups_share_the_build_programs_graph.sh workspace merge e2e/workspace +913_the_build_database_describes_the_build_programs.sh cdb merge e2e/cdb +914_an_upstream_home_is_linked_never_written.sh self keep keep machine state: a home linked to an upstream (system) home +915_the_system_home_is_maintained_and_read.sh self keep keep machine state: the system home (--system, MCPP_SYSTEM_CONFIG) +916_a_home_from_before_keeps_working.sh self keep keep upgrade: a home laid out by an earlier mcpp is migrated +917_a_long_home_on_windows.sh self keep keep machine state: a long home path on Windows (MAX_PATH) +918_an_older_packaging_is_used_not_replaced.sh self keep keep upgrade: an older packaging of a payload is used, not replaced +919_a_home_selection_is_remembered_once.sh self merge e2e/self +920_the_doctor_checks_resources_without_bootstrap.sh self merge e2e/self +921_emit_watches_inputs_not_host_outputs.sh cdb merge e2e/cdb +922_dependency_images_keep_their_resources.sh windows merge e2e/windows +923_cross_dependency_images_keep_their_resources.sh windows merge e2e/windows +924_visible_imports_resolve_transitive_modules.sh modules merge e2e/modules +925_a_stated_sysroot_is_the_c_library.sh openkal merge e2e/openkal +926_the_hosts_own_root_is_a_base.sh openkal merge e2e/openkal +927_the_runner_contract.sh run merge e2e/run +928_an_apple_sdk_is_located_on_any_host.sh macos merge e2e/macos +929_release_trims_paths_and_defines_ndebug.sh build merge e2e/build +930_a_toolchain_phase_states_the_base.sh openkal merge e2e/openkal +931_a_suggested_command_quotes_its_path.sh self merge e2e/self +932_a_fresh_home_with_a_space_and_non_ascii_builds.sh self merge e2e/self +933_one_cxx_runtime_per_process.sh runtime merge e2e/runtime +934_llvm_with_a_graph_supplied_libstdcxx.sh toolchain merge e2e/toolchain +935_two_builds_of_one_configuration.sh cache merge e2e/cache +936_the_control_contract.sh cli merge e2e/cli +937_the_root_compile_database_is_opt_in.sh cdb merge e2e/cdb +938_a_relink_never_exposes_a_partial_file.sh build merge e2e/build +939_a_lock_written_meanwhile_is_not_overwritten.sh deps merge e2e/deps +940_an_indirect_import_is_found_whatever_map_its_importer_has.sh modules merge e2e/modules +941_a_closed_reader_ends_mcpp_and_no_child_inherits_the_ignore.sh cli merge e2e/cli +942_an_ok_image_is_built_once_and_run_by_okrun.sh openkal merge e2e/openkal +943_a_transitive_git_branch_is_locked.sh deps merge e2e/deps +944_a_root_checkout_answers_a_version_the_index_lacks.sh deps merge e2e/deps +945_a_miss_anywhere_in_the_graph_asks_for_a_refresh.sh deps merge e2e/deps +946_a_program_begins_in_a_dependency_entry.sh deps merge e2e/deps +947_an_image_is_entered_at_one_stated_symbol.sh openkal merge e2e/openkal +948_an_upgraded_dependency_is_restaged_at_the_same_size.sh deps merge e2e/deps +949_a_version_the_index_lacks_refreshes_at_install.sh index merge e2e/index +950_a_kernel_abi_package_over_the_system_c_library_is_in_the_shared_image.sh openkal merge e2e/openkal +951_an_ok_image_needs_no_declaration.sh openkal merge e2e/openkal +952_a_platform_package_states_a_class_of_targets.sh openkal merge e2e/openkal +953_the_host_graph_runs_build_programs_as_images.sh openkal merge e2e/openkal +954_pack_has_three_axes.sh pack merge e2e/pack +955_the_openkal_set_is_one_tree_of_every_architecture.sh openkal merge e2e/openkal +956_a_single_file_runs_without_unpacking.sh openkal merge e2e/openkal +957_packages_merge_and_describe_themselves.sh openkal merge e2e/openkal +958_a_build_program_runs_again_when_its_output_is_gone.sh build-program merge e2e/build-program +959_a_package_is_keyed_by_every_module_it_imports.sh cache merge e2e/cache +960_an_image_of_another_architecture_runs_through_its_execution_host.sh openkal merge e2e/openkal +961_a_single_file_carries_the_image_in_a_program_of_its_architecture.sh openkal merge e2e/openkal +962_a_webassembly_package_is_neither_an_xlings_entry_nor_a_single_file.sh openkal merge e2e/openkal +963_a_target_set_builds_every_member_and_runs_where_it_is_reached.sh openkal merge e2e/openkal +964_tests_built_once_run_from_an_archive.sh openkal merge e2e/openkal +980_an_ambiguous_dependency_name_is_no_packages.sh deps merge e2e/deps +981_a_web_link_has_the_profiles_level_and_the_wasm_exception_model.sh web merge e2e/web +982_an_action_reads_the_builds_contract.sh actions merge e2e/actions +983_a_pack_holds_the_legs_it_names.sh openkal merge e2e/openkal +984_a_set_of_programs_is_a_package_per_leg.sh openkal merge e2e/openkal +985_a_format_list_writes_each_container_of_one_tree.sh openkal merge e2e/openkal +986_single_files_cover_the_legs_and_systems.sh openkal merge e2e/openkal +987_the_universal_spellings_are_migrated.sh openkal merge e2e/openkal +988_the_windows_launcher_finds_its_package_from_a_windows_path.sh openkal keep keep emulator: runs the Windows launcher under wine +989_a_native_legs_tests_run_from_an_archive.sh test merge e2e/test +990_a_lock_key_does_not_depend_on_the_selected_members.sh deps merge e2e/deps +991_a_prebuilt_archive_in_ldflags_is_a_link_input.sh build merge e2e/build +992_a_polyglot_file_runs_on_every_system_it_holds.sh openkal merge e2e/openkal +993_a_test_describes_its_launch.sh test merge e2e/test +994_the_contract_names_the_target_side.sh openkal merge e2e/openkal +995_the_windows_launcher_builds_over_any_platform_release.sh openkal merge e2e/openkal +996_an_ok_image_waits_for_a_prepare_action.sh openkal merge e2e/openkal +997_a_members_dev_dependencies_stay_out_of_a_host_tool.sh build-program merge e2e/build-program +998_a_build_keeps_its_bytes_once.sh cache merge e2e/cache +999_debug_information_is_stored_once.sh cache merge e2e/cache +1000_a_configuration_keeps_one_directory.sh cache merge e2e/cache +1001_a_dependency_carries_its_patches.sh deps merge e2e/deps +1002_a_dependency_is_embedded.sh deps merge e2e/deps +1003_one_program_one_variant.sh build merge e2e/build +1004_an_external_toolchain_finds_mcpps_ninja.sh toolchain merge e2e/toolchain +1005_clean_stale_says_what_it_kept.sh cache merge e2e/cache +1006_a_response_file_and_what_it_names_are_link_inputs.sh build merge e2e/build +1007_a_requirement_a_layer_decides_is_checked.sh features merge e2e/features +1008_a_provided_format_is_told_the_compression.sh pack merge e2e/pack +1009_cache_gc_asks_the_runner.sh openkal merge e2e/openkal +1010_the_std_module_takes_the_platforms_flags.sh toolchain merge e2e/toolchain +1011_accelerator_requests_are_complete_and_canonical.sh build-program merge e2e/build-program +1012_capabilities_bind_in_each_consumer_context.sh features merge e2e/features +1013_optional_action_progress_preserves_outputs.sh actions merge e2e/actions +1014_host_only_layers_do_not_select_the_target_abi.sh target merge e2e/target +1015_action_progress_paths_are_utf8_on_every_host.sh actions merge e2e/actions +1016_a_killed_build_does_not_race_the_next_one.sh cli merge e2e/cli +1017_a_system_msvc_builds_the_build_program_of_a_cross_target.sh windows merge e2e/windows +1018_a_package_and_a_namespaced_package_of_one_name_both_link.sh deps merge e2e/deps +1019_a_later_edge_walks_the_feature_deps_it_activates.sh features merge e2e/features +1020_a_platform_version_group_chooses_the_version.sh openkal merge e2e/openkal +1021_a_platform_series_is_locked_and_skips_newer_engines.sh openkal merge e2e/openkal +1022_a_taken_over_directory_keeps_no_compiler_record_of_another.sh cache merge e2e/cache +1023_msvc_objects_carry_their_own_debug_information.sh windows merge e2e/windows +1024_a_launch_description_takes_the_fast_path.sh run merge e2e/run +1025_the_default_platform_is_one_release.sh openkal merge e2e/openkal +1026_an_archived_member_test_carries_its_members_files.sh test merge e2e/test +1027_a_configuration_group_keeps_the_other_groups_lock_entries.sh deps merge e2e/deps +1028_a_projects_state_directory_ignores_itself.sh cache merge e2e/cache +1029_an_unused_std_module_is_collected_by_age.sh cache merge e2e/cache +1030_the_low_space_warning_knows_what_the_last_build_took.sh cache merge e2e/cache +1031_target_keeps_within_its_budget.sh cache merge e2e/cache +1032_a_quoted_dotted_dependency_key_is_deprecated.sh deps merge e2e/deps +1033_an_ok_image_runs_on_the_system_that_built_it.sh openkal merge e2e/openkal +1034_a_workspace_members_test_finds_its_programs.sh test merge e2e/test +1035_a_test_program_reports_its_cases.sh test merge e2e/test +1036_a_context_states_the_engine_values_in_process.sh build-program merge e2e/build-program +1037_a_skeleton_tests_with_mtest_and_a_test_needs_no_main.sh test merge e2e/test +1038_test_programs_of_a_directory_and_the_tests_a_change_affects.sh test merge e2e/test +1039_two_builds_that_find_the_std_module_missing_compile_it_once.sh modules merge e2e/modules +1040_a_platform_defines_a_target.sh openkal merge e2e/openkal +1041_a_32_bit_x86_bare_target.sh baremetal merge e2e/baremetal +1042_each_flag_names_its_layer_and_a_request_its_selection.sh target merge e2e/target +1043_several_programs_and_a_dotted_name_are_packed_as_stated.sh pack merge e2e/pack +1044_clang_pairs_with_the_mingw_payload.sh windows keep keep emulator: runs the clang/mingw program under wine +1045_a_required_capability_takes_its_default_provider.sh features merge e2e/features +1046_a_patch_made_with_git_diff_from_the_package_root_is_applied.sh deps merge e2e/deps +1047_an_artifact_compiles_its_shared_sources_with_its_own_flags.sh build merge e2e/build +1048_a_git_reference_follows_the_default_branch.sh deps merge e2e/deps +1049_a_platform_skeleton_is_made_and_its_statement_read_as_the_engine_reads_it.sh openkal merge e2e/openkal +1050_a_test_run_is_kept_in_the_mtest_store.sh test merge e2e/test +1051_a_cross_targets_toolchain_section_is_not_the_build_programs.sh build-program merge e2e/build-program +1052_a_framework_that_uses_the_package_under_test_is_built_against_it.sh test merge e2e/test +1053_a_mach_o_images_c_library_is_up_before_its_initialisers.sh macos merge e2e/macos +1054_programs_that_share_an_entry_keep_their_own_flags.sh build merge e2e/build +1055_layer_suppliers_that_require_one_another_are_refused.sh target merge e2e/target +1056_a_runner_reads_the_interfaces_its_implementation_provides.sh openkal merge e2e/openkal +1057_a_platform_row_takes_nothing_of_the_payloads_host.sh openkal merge e2e/openkal +1058_a_gnu_abi_windows_row_compiles_its_resources.sh windows merge e2e/windows +1059_a_base_package_states_the_layers_beside_its_c_library.sh openkal merge e2e/openkal +1060_a_patched_member_of_a_repository_keeps_its_siblings.sh deps merge e2e/deps +1061_an_artifact_for_another_system_is_packed_without_running_it.sh pack merge e2e/pack +1062_link_flags_on_a_compiler_link_are_named_not_dropped.sh build merge e2e/build +1063_a_mingw_base_package_serves_windows_gnu_beside_llvm.sh windows keep keep emulator: runs the mingw base-package program under wine +1064_a_scan_override_of_a_file_this_build_excludes_is_kept.sh modules merge e2e/modules +1065_test_records_carry_parts_live_cases_and_metrics.sh test merge e2e/test +1066_a_merged_version_withdraws_its_requests.sh deps merge e2e/deps +1067_a_borrowed_registry_keeps_its_owners_places.sh index merge e2e/index +1068_a_running_test_holds_no_build_directory.sh test merge e2e/test +1069_an_unchanged_project_tests_without_planning.sh test merge e2e/test +1070_the_test_spec_launches_frameworks_checks_and_output.sh test merge e2e/test +1071_the_root_overrides_a_dependency_for_the_whole_graph.sh deps merge e2e/deps +1072_a_resource_include_nothing_provides_is_the_scripts_own.sh windows merge e2e/windows +1073_the_freebsd_musl_identity_is_served_by_openkal.sh openkal merge e2e/openkal +1074_a_namespace_package_named_like_a_spec_field.sh deps merge e2e/deps +1075_a_graph_supplied_windows_row_finds_its_resource_headers.sh windows merge e2e/windows +1076_an_aarch64_linux_gnu_cross_comes_from_a_sysroot_payload.sh target merge e2e/target +1077_a_program_of_another_system_is_built_not_run.sh target merge e2e/target +1078_systems_all_carries_the_runners_the_platform_declares.sh openkal merge e2e/openkal +1079_an_emscripten_rows_longjmp_model_round_trips.sh web merge e2e/web +1080_gcc_over_the_hosts_payload_links_from_the_payload.sh toolchain merge e2e/toolchain diff --git a/tests/e2e/keep.tsv b/tests/e2e/keep.tsv new file mode 100644 index 000000000..0324dd048 --- /dev/null +++ b/tests/e2e/keep.tsv @@ -0,0 +1,34 @@ +script why +00_fixture_path_hygiene.sh lints the bash scripts' own fixture paths (Git Bash path forms); retires with the last script +27_self_contained_home.sh home located from the installed binary's own directory (/bin/mcpp): a release-layout install +30_dev_binary_home.sh a dev binary's home when MCPP_HOME is unset: needs the real ~/.mcpp of the machine +102_mingw_cross_wine.sh emulator: runs the cross-built PE under wine +130_freestanding_riscv_build_and_run.sh emulator: boots the image in qemu-system-riscv64 +131_freestanding_bsp_supplies_everything.sh emulator: boots the image in qemu-system-riscv64 +132_freestanding_test_and_artifacts.sh emulator: runs the freestanding tests in qemu-system-riscv64 +133_freestanding_std_subset.sh emulator: boots the image in qemu-system-riscv64 +150_clang_module_operator_template.sh a state canary of the bundled Clang (expected crash), not an assertion about mcpp +230_bench_harness.sh bench harness; converges with bench/ (design 5.9 bench), not the e2e migration +232_workflow_syntax.sh lints .github/workflows YAML (CI configuration, needs a YAML reader) +233_bench_matrix.sh bench/matrix.json single-source check; converges with bench/ (design 5.9 bench) +239_msvc_managed_toolset.sh CI-only: xlings-managed MSVC (opt-in MCPP_E2E_XLINGS_MSVC, ~380 MB download) +257_shared_library_pe.sh emulator: runs the PE DLL program under wine +288_the_openkal_stack_on_a_machine_with_no_os.sh emulator: boots the openkal stack in qemu-system-riscv64 +332_cortex_m_builds_and_boots.sh emulator: boots the Cortex-M image in qemu-system-arm +336_armv7a_builds_and_boots.sh emulator: boots the armv7-a image in qemu-system-arm +338_cortex_m_picolibc_sysroot.sh emulator: boots the Cortex-M picolibc image in qemu-system-arm +658_a_program_runs_on_an_attached_android_device.sh a physical Android device over adb +704_cxx_identity_across_macho_images.sh a measurement leg (prints a table, asserts nothing) +705_cxx_identity_across_pe_images.sh a measurement leg (prints a table, asserts nothing) +796_runtime_search_dir_on_pe_under_wine.sh emulator: runs the PE program under wine +797_a_windows_program_finds_its_dlls_beside_it.sh emulator: runs the PE program under wine +846_the_vendored_xlings_is_replaced_from_the_newest_source.sh upgrade: the vendored xlings replaced from the newest source in a home +901_the_toolchain_library_path_stays_with_the_tools.sh container: a host whose /bin/sh is not the toolchain's (termux-style loader) +914_an_upstream_home_is_linked_never_written.sh machine state: a home linked to an upstream (system) home +915_the_system_home_is_maintained_and_read.sh machine state: the system home (--system, MCPP_SYSTEM_CONFIG) +916_a_home_from_before_keeps_working.sh upgrade: a home laid out by an earlier mcpp is migrated +917_a_long_home_on_windows.sh machine state: a long home path on Windows (MAX_PATH) +918_an_older_packaging_is_used_not_replaced.sh upgrade: an older packaging of a payload is used, not replaced +988_the_windows_launcher_finds_its_package_from_a_windows_path.sh emulator: runs the Windows launcher under wine +1044_clang_pairs_with_the_mingw_payload.sh emulator: runs the clang/mingw program under wine +1063_a_mingw_base_package_serves_windows_gnu_beside_llvm.sh emulator: runs the mingw base-package program under wine From 52631a464118a316245a1614d581437daf1a4bec Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 13 Nov 2026 17:24:02 +0900 Subject: [PATCH 004/115] e2e: the test subsystem as C++ cases (38 scripts) The 38 scripts MIGRATION.tsv assigns to `test` (mcpp test: discovery, selection, reports, the JSON stream, timeouts, workspaces, --no-run and archives, the launch description, the mtest protocol, the store, the fast path) are 39 cases of the program e2e/test (tests/e2e/test/*.cpp, 12 files, one process each). Each case is named by its script's sentence and keeps its number; its `# requires:` words are its tags, and a case skips with "missing capability: " where the machine lacks one. The scripts are removed and MIGRATION.tsv names each one's case. Scripts: 15 16 17 18 72 78 90 94 152 153 154 155 157 158 159 160 164 178 213 673 745 854 855 856 865 989 993 1026 1034 1035 1037 1038 1050 1052 1065 1068 1069 1070. Old run (the scripts, against this branch's mcpp, Linux x86_64, gcc and llvm in ~/.mcpp): 37 passed, 1 skipped (94 requires macos; started directly it exits 0 on Linux, its otool leg being conditional). Sequential time 227 s. New run (`mcpp test e2e/test`): 38 passed, 0 failed, 1 skipped (94, missing capability: macos). 72 s of runs, 12 programs at once. tests/support gains: has(word) and needs({...}) (run_all.sh's capability probes; an unknown word fails the case), on_path, glob (a shell's `*` per component), has_line/no_line (grep -E per line), lacks, contains_nocase, count_lines, ninja_log_outputs, mcpp_command (a mcpp for an e2e::session) and project::scaffold (`mcpp new` in the case's directory). Every assertion is kept. Where a reading changed: - 159: `grep -qv '"test":"01-b/0"'` held for any line without the record; the case asserts what it meant, that no line has it (stronger). - 989: the evidence the script printed after a failure (file, xattr, codesign) is a diagnostic, not an assertion; not carried over. - 1070 O8: the script compared the output after the shell had stripped its trailing newline; the case accepts the text with or without it. - 993 is two cases: A-E, and the named-runner leg, which keeps `requires:python3` because its runner is a Python program. - python3 (993 A-E, 1026, 1035, 1037, 1050, 1065, 1070) read JSON for the scripts; the cases read it with nlohmann::json and no longer require it. - 90: its `requires: gcc` stood on line 8, which run_all.sh does not read, so it ran on every host; the case does not require gcc either. - 1052 sourced _toolchain_env.sh without using it; dropped. - 178 and 1068 drive concurrent mcpp processes through e2e::session instead of `&`, a fifo and kill. 1026 still unpacks with tar, 1038 still uses git, 94 still reads otool. --- ...d_member_test_carries_its_members_files.sh | 79 ---- ...rkspace_members_test_finds_its_programs.sh | 44 -- .../1035_a_test_program_reports_its_cases.sh | 188 --------- ...sts_with_mtest_and_a_test_needs_no_main.sh | 231 ----------- ...irectory_and_the_tests_a_change_affects.sh | 147 ------- ...0_a_test_run_is_kept_in_the_mtest_store.sh | 56 --- ..._package_under_test_is_built_against_it.sh | 63 --- ...ords_carry_parts_live_cases_and_metrics.sh | 96 ----- ...a_running_test_holds_no_build_directory.sh | 107 ----- ...nchanged_project_tests_without_planning.sh | 81 ---- ...c_launches_frameworks_checks_and_output.sh | 144 ------- tests/e2e/152_test_subdir_names.sh | 29 -- tests/e2e/153_test_isolation.sh | 53 --- tests/e2e/154_test_filter.sh | 32 -- tests/e2e/155_test_json.sh | 54 --- tests/e2e/157_test_glob_flags.sh | 46 --- tests/e2e/158_test_signal_and_duration.sh | 42 -- tests/e2e/159_test_list.sh | 30 -- tests/e2e/15_test_passing.sh | 25 -- tests/e2e/160_test_timeout.sh | 43 -- tests/e2e/164_ninja_goal_command_length.sh | 67 --- tests/e2e/16_test_failing.sh | 28 -- tests/e2e/178_test_observability.sh | 167 -------- tests/e2e/17_test_no_tests.sh | 17 - tests/e2e/18_devdeps_isolation.sh | 52 --- ..._build_after_test_is_not_the_test_graph.sh | 135 ------ ...673_test_discover_names_where_tests_are.sh | 97 ----- tests/e2e/72_test_profile_features.sh | 58 --- ...745_no_run_builds_the_tests_and_says_so.sh | 184 --------- tests/e2e/78_test_main_combinations.sh | 79 ---- ...r_several_members_plans_and_builds_once.sh | 127 ------ ...d_test_build_is_reported_once_per_group.sh | 107 ----- ...al_members_continues_past_a_failing_one.sh | 152 ------- ...eps_on_stderr_and_its_results_on_stdout.sh | 104 ----- tests/e2e/90_workspace_test.sh | 76 ---- tests/e2e/94_macos_test_stdlib.sh | 49 --- ...a_native_legs_tests_run_from_an_archive.sh | 65 --- tests/e2e/993_a_test_describes_its_launch.sh | 146 ------- tests/e2e/MIGRATION.tsv | 76 ++-- tests/e2e/test/affected.cpp | 142 +++++++ tests/e2e/test/archive.cpp | 202 +++++++++ tests/e2e/test/fast.cpp | 155 +++++++ tests/e2e/test/frameworks.cpp | 86 ++++ tests/e2e/test/graph.cpp | 223 ++++++++++ tests/e2e/test/launch.cpp | 146 +++++++ tests/e2e/test/protocol.cpp | 359 ++++++++++++++++ tests/e2e/test/report.cpp | 137 +++++++ tests/e2e/test/selection.cpp | 123 ++++++ tests/e2e/test/skeleton.cpp | 199 +++++++++ tests/e2e/test/streams.cpp | 137 +++++++ tests/e2e/test/workspace.cpp | 383 ++++++++++++++++++ tests/support/src/support.cpp | 245 ++++++++++- tests/support/src/support.cppm | 34 ++ 53 files changed, 2608 insertions(+), 3339 deletions(-) delete mode 100755 tests/e2e/1026_an_archived_member_test_carries_its_members_files.sh delete mode 100755 tests/e2e/1034_a_workspace_members_test_finds_its_programs.sh delete mode 100755 tests/e2e/1035_a_test_program_reports_its_cases.sh delete mode 100755 tests/e2e/1037_a_skeleton_tests_with_mtest_and_a_test_needs_no_main.sh delete mode 100644 tests/e2e/1038_test_programs_of_a_directory_and_the_tests_a_change_affects.sh delete mode 100755 tests/e2e/1050_a_test_run_is_kept_in_the_mtest_store.sh delete mode 100755 tests/e2e/1052_a_framework_that_uses_the_package_under_test_is_built_against_it.sh delete mode 100755 tests/e2e/1065_test_records_carry_parts_live_cases_and_metrics.sh delete mode 100755 tests/e2e/1068_a_running_test_holds_no_build_directory.sh delete mode 100755 tests/e2e/1069_an_unchanged_project_tests_without_planning.sh delete mode 100755 tests/e2e/1070_the_test_spec_launches_frameworks_checks_and_output.sh delete mode 100755 tests/e2e/152_test_subdir_names.sh delete mode 100755 tests/e2e/153_test_isolation.sh delete mode 100755 tests/e2e/154_test_filter.sh delete mode 100755 tests/e2e/155_test_json.sh delete mode 100755 tests/e2e/157_test_glob_flags.sh delete mode 100755 tests/e2e/158_test_signal_and_duration.sh delete mode 100755 tests/e2e/159_test_list.sh delete mode 100755 tests/e2e/15_test_passing.sh delete mode 100755 tests/e2e/160_test_timeout.sh delete mode 100755 tests/e2e/164_ninja_goal_command_length.sh delete mode 100755 tests/e2e/16_test_failing.sh delete mode 100755 tests/e2e/178_test_observability.sh delete mode 100755 tests/e2e/17_test_no_tests.sh delete mode 100755 tests/e2e/18_devdeps_isolation.sh delete mode 100755 tests/e2e/213_build_after_test_is_not_the_test_graph.sh delete mode 100644 tests/e2e/673_test_discover_names_where_tests_are.sh delete mode 100755 tests/e2e/72_test_profile_features.sh delete mode 100755 tests/e2e/745_no_run_builds_the_tests_and_says_so.sh delete mode 100755 tests/e2e/78_test_main_combinations.sh delete mode 100755 tests/e2e/854_a_test_over_several_members_plans_and_builds_once.sh delete mode 100755 tests/e2e/855_a_shared_test_build_is_reported_once_per_group.sh delete mode 100755 tests/e2e/856_a_test_over_several_members_continues_past_a_failing_one.sh delete mode 100644 tests/e2e/865_mcpp_test_reports_its_steps_on_stderr_and_its_results_on_stdout.sh delete mode 100755 tests/e2e/90_workspace_test.sh delete mode 100755 tests/e2e/94_macos_test_stdlib.sh delete mode 100755 tests/e2e/989_a_native_legs_tests_run_from_an_archive.sh delete mode 100755 tests/e2e/993_a_test_describes_its_launch.sh create mode 100644 tests/e2e/test/affected.cpp create mode 100644 tests/e2e/test/archive.cpp create mode 100644 tests/e2e/test/fast.cpp create mode 100644 tests/e2e/test/frameworks.cpp create mode 100644 tests/e2e/test/graph.cpp create mode 100644 tests/e2e/test/launch.cpp create mode 100644 tests/e2e/test/protocol.cpp create mode 100644 tests/e2e/test/report.cpp create mode 100644 tests/e2e/test/selection.cpp create mode 100644 tests/e2e/test/skeleton.cpp create mode 100644 tests/e2e/test/streams.cpp create mode 100644 tests/e2e/test/workspace.cpp diff --git a/tests/e2e/1026_an_archived_member_test_carries_its_members_files.sh b/tests/e2e/1026_an_archived_member_test_carries_its_members_files.sh deleted file mode 100755 index 492cbfef2..000000000 --- a/tests/e2e/1026_an_archived_member_test_carries_its_members_files.sh +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env bash -# requires: python3 -# 1026 -- an archived member test carries its member's files (SPEC-015 §4). -# -# In a workspace, a member's programs and tests are placed in the member's own -# product directory with the files its `[runtime] deploy` names. `mcpp test -# --no-run --archive` copies each test with the files beside it; on -# 2026.11.7.1 the copy named the files where a plan of one package places -# them (`bin/`), not the member's directory (`bin//`), and -# the archive of a member with a deploy set was refused: "cannot copy … into -# the test archive". -# -# Criteria: -# A. `mcpp test -p tool --no-run --archive` writes the archive, and the -# member's deployed file is beside its test there. -# B. In a checkout without target/, the archived test runs and finds the -# file beside it. -set -e - -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } -MCPP="${MCPP:-mcpp}" - -mkdir -p "$TMP/ws/app/src" "$TMP/ws/tool/src" "$TMP/ws/tool/tests" "$TMP/ws/tool/notices" -cd "$TMP/ws" -printf '[workspace]\nmembers = ["app", "tool"]\n' > mcpp.toml -printf '[package]\nname = "app"\nversion = "0.1.0"\n' > app/mcpp.toml -printf 'int main() { return 0; }\n' > app/src/main.cpp -cat > tool/mcpp.toml <<'TOML' -[package] -name = "tool" -version = "0.1.0" - -[runtime] -deploy = [ { from = "notices/LICENSE", to = "licenses/tool" } ] -TOML -printf 'tool license\n' > tool/notices/LICENSE -printf 'int main() { return 0; }\n' > tool/src/main.cpp -cat > tool/tests/notice.cpp <<'CPP' -import std; -int main(int, char** argv) { - auto here = std::filesystem::absolute(argv[0]).parent_path(); - std::ifstream in(here / "licenses" / "tool" / "LICENSE"); - std::string line; - std::getline(in, line); - return line == "tool license" ? 0 : 1; -} -CPP - -check_archive() { # $1 archive, $2 log - rm -rf "$TMP/x" && mkdir -p "$TMP/x" && tar -xzf "$1" -C "$TMP/x" - python3 - "$TMP/x" <<'PY' || fail "the archive does not carry the member's file" "$2" -import json, pathlib, sys -m = next(pathlib.Path(sys.argv[1]).glob("*/mcpp-tests.json")) -d = json.load(open(m)) -tests = [t for leg in d["legs"] for t in leg["tests"] if t["name"] == "notice"] -assert tests, d -t = tests[0] -files = [f for f in t["files"] if f.endswith("licenses/tool/LICENSE")] -assert files, t -assert (m.parent / files[0]).read_text() == "tool license\n", files -PY -} - -# A -"$MCPP" test -p tool --no-run --archive "$TMP/a.tar.gz" > a.log 2>&1 || fail "A: -p tool --no-run --archive" a.log -check_archive "$TMP/a.tar.gz" a.log -echo "ok: a member's archived test carries the member's file" - -# B -mkdir -p "$TMP/co" && cp -R "$TMP/ws/mcpp.toml" "$TMP/ws/app" "$TMP/ws/tool" "$TMP/co/" -rm -rf "$TMP/co/target" "$TMP/co/app/target" "$TMP/co/tool/target" -cd "$TMP/co" -"$MCPP" test -p tool --archive "$TMP/a.tar.gz" > b.log 2>&1 || fail "B: mcpp test --archive" b.log -grep -q "notice ... ok" b.log || fail "B: the archived test did not find its file" b.log -echo "ok: the archived test finds the file beside it" - -echo "PASS: 1026 an archived member test carries its member's files" diff --git a/tests/e2e/1034_a_workspace_members_test_finds_its_programs.sh b/tests/e2e/1034_a_workspace_members_test_finds_its_programs.sh deleted file mode 100755 index d5776f334..000000000 --- a/tests/e2e/1034_a_workspace_members_test_finds_its_programs.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 1034 -- a workspace member's test finds the member's programs under `--workspace` as under `-p`. -# -# `mcpp test` builds a package's programs before its tests and names each to them as MCPP_BIN_ -# (docs/08). A workspace's test plan holds a member's test programs only, so under `--workspace` the -# member's programs were not built and its tests found no MCPP_BIN_; `-p ` found them. -# (Found by mtest's conformance suite, whose tests run the member's own fixture.) -# -# Criteria: both members' tests pass under `--workspace` and under `-p`; each finds its own program -# of one shared name. -set -e -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } - -cd "$TMP" -printf '[workspace]\nmembers = ["a", "b"]\n' > mcpp.toml -for m in a b; do - mkdir -p "$m/src" "$m/tests" - cat > "$m/mcpp.toml" < "$m/src/tool.cpp" - cat > "$m/tests/finds.cpp" < ws.log 2>&1 || fail "--workspace: a member's test did not find its program" ws.log -"$MCPP" test -p a > a.log 2>&1 || fail "-p a" a.log -grep -q 'finds \.\.\. ok' ws.log || fail "the tests did not run under --workspace" ws.log -echo "PASS: 1034 a workspace member's test finds its programs" diff --git a/tests/e2e/1035_a_test_program_reports_its_cases.sh b/tests/e2e/1035_a_test_program_reports_its_cases.sh deleted file mode 100755 index f18ccc838..000000000 --- a/tests/e2e/1035_a_test_program_reports_its_cases.sh +++ /dev/null @@ -1,188 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 1035 -- a test program that implements the test protocol reports its cases (docs/08 "Cases"). -# -# mcpp runs the runner's side of the mtest test protocol, version 1: a program whose entry states -# `test-protocol = 1`, or whose file carries the marker `mtest-protocol=1`, is told the protocol's -# variables, lists its cases, runs a selection and reports each case; every other program is one -# case judged by its exit status and is told none of the variables. -# -# Criteria: -# A. A run's record names the protocol and each case's result, a failure with its place; a case a -# crash left without an end failed (P5.4); the human report names the failed cases. -# B. `--case PATTERN` runs the matching cases of a protocol program and no other program. -# C. `--cases` lists each protocol program's cases and runs none. -# D. A program that implements no protocol receives none of the variables, though mcpp's own -# environment has them (P1.2). -# E. A program found by its marker is a protocol program. -# F. `--no-run --message-format json` records each built test with its program and sources. -set -e -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } -PY=python3; command -v python3 >/dev/null 2>&1 || PY=python - -cd "$TMP" -printf '[workspace]\nmembers = ["fw", "app", "plain"]\n' > mcpp.toml - -# fw: a framework whose entry implements the protocol; a test file defines `cases()`. -mkdir -p fw/src -cat > fw/mcpp.toml <<'TOML' -[package] -name = "fw" -version = "0.1.0" - -[targets.main] -kind = "entry" -main = "src/main.cpp" -test-protocol = 1 -TOML -cat > fw/src/main.cpp <<'CPP' -import std; -struct test_case { std::string id; int line; int (*body)(); }; -std::vector cases(); -int main() { - const char* mode = std::getenv("MTEST_MODE"); - const char* path = std::getenv("MTEST_REPORT"); - std::ofstream report; - if (path) report.open(path, std::ios::app); - auto event = [&](const std::string& body) { if (report) { report << "{" << body << "}\n"; report.flush(); } }; - event(R"("e":"program","protocol":1,"level":1,"framework":"fw","version":"0.1.0")"); - std::set selected; - bool selecting = false; - if (const char* s = std::getenv("MTEST_SELECT")) { - selecting = true; - std::ifstream in(s); - for (std::string line; std::getline(in, line);) if (!line.empty()) selected.insert(line); - } - int failed = 0; - for (const auto& c : cases()) { - if (mode && std::string(mode) == "list") { - event(std::format(R"("e":"case","id":"{}","name":"{}","file":"t.cpp","line":{})", c.id, c.id, c.line)); - continue; - } - if (selecting && !selected.contains(c.id)) continue; - event(std::format(R"("e":"begin","id":"{}","t":0)", c.id)); - const int r = c.body(); - if (r != 0) { - ++failed; - event(std::format(R"("e":"failure","id":"{}","file":"t.cpp","line":{},"message":"{} != 0")", c.id, c.line, r)); - } - event(std::format(R"("e":"end","id":"{}","status":"{}","ms":1)", c.id, r ? "fail" : "pass")); - } - event(std::format(R"("e":"done","status":"{}","counts":{{}})", failed ? "fail" : "pass")); - return failed ? 1 : 0; -} -CPP - -# app: its tests begin at fw's entry. -mkdir -p app/src app/tests -cat > app/mcpp.toml <<'TOML' -[package] -name = "app" -version = "0.1.0" - -[dev-dependencies] -fw = { path = "../fw" } - -[test] -main = { from = "fw/main" } -TOML -printf 'export module app;\nexport int answer() { return 42; }\n' > app/src/app.cppm -cat > app/tests/math.cpp <<'CPP' -import std; -import app; -struct test_case { std::string id; int line; int (*body)(); }; -std::vector cases() { - return { - {"math/adds", 4, [] { return answer() == 42 ? 0 : 1; }}, - {"math/fails", 5, [] { return 7; }}, - {"math/crashes", 6, [] { std::exit(3); return 0; }}, - }; -} -CPP - -# plain: a program that implements no protocol, and one found by its marker. -mkdir -p plain/tests -printf '[package]\nname = "plain"\nversion = "0.1.0"\n' > plain/mcpp.toml -cat > plain/tests/bare.cpp <<'CPP' -import std; -int main() { - for (const char* v : {"MTEST_PROTOCOL", "MTEST_MODE", "MTEST_REPORT"}) - if (std::getenv(v)) { std::println("{} reached a program that implements no protocol", v); return 3; } - return 0; -} -CPP -cat > plain/tests/marked.cpp <<'CPP' -import std; -constinit const char marker[] = "mtest-protocol=1"; -int main() { - [[maybe_unused]] volatile char keep = marker[0]; - const char* path = std::getenv("MTEST_REPORT"); - if (!path) return 4; - std::ofstream report(path, std::ios::app); - const std::string mode = std::getenv("MTEST_MODE") ? std::getenv("MTEST_MODE") : "run"; - report << R"({"e":"program","protocol":1,"level":1,"framework":"marked","version":"1"})" << "\n"; - if (mode == "list") report << R"({"e":"case","id":"only","name":"only","file":"marked.cpp","line":1})" << "\n"; - else report << R"({"e":"begin","id":"only","t":0})" << "\n" << R"({"e":"end","id":"only","status":"pass","ms":0})" << "\n"; - report << R"({"e":"done","status":"pass","counts":{}})" << "\n"; - return 0; -} -CPP - -record() { # record LOG TEST: the test's record - $PY - "$1" "$2" <<'PY' -import json, sys -for line in open(sys.argv[1]): - line = line.strip() - if not line.startswith("{"): continue - try: r = json.loads(line) - except ValueError: continue - if r.get("test") == sys.argv[2]: print(json.dumps(r)); break -PY -} -field() { # field JSON EXPRESSION - $PY -c "import json,sys; r=json.loads(sys.argv[1]); print(eval(sys.argv[2]))" "$1" "$2" -} - -# A -"$MCPP" test -p app --message-format json > a.json 2> a.err && fail "A: a failing case passed the run" a.json a.err -rec=$(record a.json math) -[ -n "$rec" ] || fail "A: no record of the test" a.json a.err -[ "$(field "$rec" "r['protocol']")" = 1 ] || fail "A: the record does not name the protocol" a.json -[ "$(field "$rec" "[c['id']+':'+c['status'] for c in r['cases']]")" = "['math/adds:pass', 'math/fails:fail', 'math/crashes:fail']" ] \ - || fail "A: the cases' results" a.json -[ "$(field "$rec" "r['cases'][1]['failures'][0]['line']")" = 5 ] || fail "A: the failure's place" a.json -field "$rec" "r['cases'][2]['reason']" | grep -q "status 3 before the case ended" || fail "A: a crashed case's reason (P5.4)" a.json -"$MCPP" test -p app > a.log 2>&1 && fail "A: the human run passed" a.log -grep -qE "^ math/fails t\.cpp:5$" a.log || fail "A: the failures' index does not name the failed case at its place" a.log -grep -qE "cases: 1 passed, 2 failed, 0 skipped" a.log || fail "A: the summary does not count the cases" a.log - -# B -"$MCPP" test --case math/adds --message-format json > b.json 2> b.err || fail "B: the selected case failed" b.json b.err -rec=$(record b.json math) -[ "$(field "$rec" "[c['id'] for c in r['cases']]")" = "['math/adds']" ] || fail "B: the selection" b.json -[ -z "$(record b.json bare)" ] || fail "B: a program with no protocol ran under --case" b.json - -# C -"$MCPP" test --cases > c.log 2>&1 || fail "C: --cases" c.log -grep -q "math: 3 cases" c.log || fail "C: the listing of math" c.log -grep -q "math/fails t.cpp:5" c.log || fail "C: a listed case's place" c.log -grep -q "bare: one case" c.log || fail "C: a program with no protocol" c.log - -# D, E -MTEST_PROTOCOL=1 MTEST_MODE=list MTEST_REPORT="$TMP/outer.ndjson" "$MCPP" test -p plain --message-format json > d.json 2> d.err \ - || fail "D: the plain member's tests" d.json d.err -[ "$(field "$(record d.json bare)" "r['status']")" = pass ] || fail "D: the protocol's variables reached a program with none (P1.2)" d.json -[ "$(field "$(record d.json marked)" "r['protocol']")" = 1 ] || fail "E: the marker" d.json -[ "$(field "$(record d.json marked)" "[c['id'] for c in r['cases']]")" = "['only']" ] || fail "E: the marked program's case" d.json - -# F -"$MCPP" test -p app --no-run --message-format json > f.json 2> f.err || fail "F: --no-run" f.json f.err -rec=$(record f.json math) -[ "$(field "$rec" "r['status']")" = built ] || fail "F: the built record" f.json -field "$rec" "r['program']" | grep -q math || fail "F: the program" f.json -field "$rec" "any(s.endswith('app/tests/math.cpp') for s in r['sources']) and any(s.endswith('app/src/app.cppm') for s in r['sources'])" \ - | grep -q True || fail "F: the sources" f.json - -echo "PASS: 1035 a test program reports its cases" diff --git a/tests/e2e/1037_a_skeleton_tests_with_mtest_and_a_test_needs_no_main.sh b/tests/e2e/1037_a_skeleton_tests_with_mtest_and_a_test_needs_no_main.sh deleted file mode 100755 index b6f90ceec..000000000 --- a/tests/e2e/1037_a_skeleton_tests_with_mtest_and_a_test_needs_no_main.sh +++ /dev/null @@ -1,231 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 1037 -- `mcpp new` scaffolds a program or a library whose test is mtest's, and a test file that -# defines no `main` begins at the entry a dependency offers (docs/08, docs/01). -# -# Criteria: -# A. `--template lib` writes the module, a partition it exports and the partition's implementation, -# named after the package (`acme.my-lib` is the module `acme.my_lib`); the library builds and its -# test, a file of mtest's cases with no `main`, passes as one protocol case. -# B. The program's skeleton test passes as one protocol case. -# C. An entry with `test-main = true` begins a dependent's test that defines no `main`; a test that -# defines one keeps it; the package's own entry begins its own tests; a framework that is a -# dependency as well as a dev-dependency offers its beginning once. -# D. Two offers are refused, naming both; `[test] main` chooses one. -# E. A library whose name has a part that does not begin with a letter is refused. -# F. In one package, a test that imports a framework's module begins at that framework's entry, -# and a test that imports none begins at a dev-dependency's source that defines `main`. -# G. `mcpp test --guide` names each test framework with its guide (`test-guide`), and a failed -# run points to it. -# H. A test that begins at an entry links no source that defines `main` beneath its -# dev-dependency (gtest_main.cc of compat.gtest beneath a GoogleTest adapter). -# I. A test links a dev-dependency that is a shared library (mtest's C ABI) by its library. -# J. In a workspace, a member's test begins by the member's own dev-dependencies: another -# member's dev-dependency that defines `main` is no beginning of it. -# K. mtest reaches the test units only: the skeleton's `mcpp build` and `mcpp test` share one -# build directory, switching between them compiles the program's source once, its command -# carries no `__OPENKAL__`, and the compile database holds the test. -# L. A dev-dependency's source whose `main` is compiled only under a switch no command defines -# (zlib's `#ifdef MAKECRCH`) is linked into a test that has its own `main`; one whose `main` -# is in the branch an unknown condition leaves (gtest_main.cc's `#else`) still begins a test. -set -e -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } -PY=python3; command -v python3 >/dev/null 2>&1 || PY=python - -cd "$TMP" - -# A -"$MCPP" new acme.my-lib --template lib > a.log 2>&1 || fail "A: --template lib" a.log -for f in mcpp.toml src/my-lib.cppm src/hello.cppm src/hello.cpp tests/hello.cpp; do - [ -f "acme.my-lib/$f" ] || fail "A: no $f" a.log -done -grep -q '^export module acme.my_lib;' acme.my-lib/src/my-lib.cppm || fail "A: the module's name" acme.my-lib/src/my-lib.cppm -grep -q '^export module acme.my_lib:hello;' acme.my-lib/src/hello.cppm || fail "A: the partition" acme.my-lib/src/hello.cppm -grep -q 'namespace acme::my_lib' acme.my-lib/src/hello.cpp || fail "A: the namespace" acme.my-lib/src/hello.cpp -grep -q 'int main' acme.my-lib/tests/hello.cpp && fail "A: the test defines main" acme.my-lib/tests/hello.cpp -(cd acme.my-lib && "$MCPP" build > ../a-build.log 2>&1) || fail "A: the library does not build" a-build.log -(cd acme.my-lib && "$MCPP" test > ../a-test.log 2>&1) || fail "A: the library's test failed" a-test.log -grep -q 'hello \.\.\. ok (.*1 case)' a-test.log || fail "A: the test is not one protocol case" a-test.log - -# B -"$MCPP" new prog > b.log 2>&1 || fail "B: mcpp new" b.log -(cd prog && "$MCPP" test > ../b-test.log 2>&1) || fail "B: the program's test failed" b-test.log -grep -q 'test_smoke \.\.\. ok (.*1 case)' b-test.log || fail "B: the smoke test is not one protocol case" b-test.log - -# C: a framework whose entry offers to begin tests, and its own tests. -mkdir -p fw/src fw/tests app/tests -cat > fw/mcpp.toml <<'TOML' -[package] -name = "fw" -version = "0.1.0" - -[targets.main] -kind = "entry" -main = "src/entry.cpp" -test-main = true -TOML -printf 'import std;\nextern "C++" int run_cases();\nint main() { std::println("fw main"); return run_cases(); }\n' > fw/src/entry.cpp -printf 'extern "C++" int run_cases() { return 0; }\n' > fw/tests/own.cpp -cat > app/mcpp.toml <<'TOML' -[package] -name = "app" -version = "0.1.0" - -[dev-dependencies] -fw = { path = "../fw" } -TOML -printf 'extern "C++" int run_cases() { return 0; }\n' > app/tests/begun.cpp -printf 'import std;\nint main() { std::println("own main"); return 0; }\n' > app/tests/own.cpp -(cd app && "$MCPP" test > ../c.log 2>&1) || fail "C: the dependent's tests" c.log -grep -q 'begun \.\.\. ok' c.log && grep -q 'own \.\.\. ok' c.log || fail "C: a test did not run" c.log -(cd fw && "$MCPP" test > ../c-own.log 2>&1) || fail "C: the framework's own test" c-own.log -cp app/mcpp.toml app/mcpp.toml.c -printf '\n[dependencies]\nfw = { path = "../fw" }\n' >> app/mcpp.toml -(cd app && "$MCPP" test > ../c-both.log 2>&1) || fail "C: a framework named in two tables offered twice" c-both.log -mv app/mcpp.toml.c app/mcpp.toml - -# D: a second framework offers too. -cp -R fw fw2 -sed -i.bak 's/name = "fw"/name = "fw2"/' fw2/mcpp.toml -printf 'fw2 = { path = "../fw2" }\n' >> app/mcpp.toml -(cd app && "$MCPP" test > ../d.log 2>&1) && fail "D: two offers were taken" d.log -grep -q "fw/main" d.log && grep -q "fw2/main" d.log || fail "D: the refusal does not name both" d.log -rm app/tests/own.cpp # under [test] main every test begins at the entry -printf '\n[test]\nmain = { from = "fw2/main" }\n' >> app/mcpp.toml -(cd app && "$MCPP" test > ../d2.log 2>&1) || fail "D: [test] main did not choose" d2.log - -# E -"$MCPP" new 3d --template lib > e.log 2>&1 && fail "E: a name beginning with a digit was taken" e.log -grep -q "does not begin with a letter" e.log || fail "E: the refusal" e.log -# F -mkdir -p fwm/src hdr/src mixed/tests -cat > fwm/mcpp.toml <<'TOML' -[package] -name = "fwm" -version = "0.1.0" - -[targets.main] -kind = "entry" -main = "src/entry.cpp" -test-main = true -TOML -printf 'export module fwm;\nexport int fwm_cases() { return 0; }\n' > fwm/src/fwm.cppm -printf 'import std;\nimport fwm;\nint main() { std::println("began at fwm"); return fwm_cases(); }\n' > fwm/src/entry.cpp -printf '[package]\nname = "hdr"\nversion = "0.1.0"\n' > hdr/mcpp.toml -printf 'import std;\nint main() { std::println("began at hdr"); return 0; }\n' > hdr/src/hdr_main.cpp -printf 'int hdr_marker() { return 0; }\n' > hdr/src/hdr.cpp -cat > mixed/mcpp.toml <<'TOML' -[package] -name = "mixed" -version = "0.1.0" - -[dev-dependencies] -fwm = { path = "../fwm" } -hdr = { path = "../hdr" } -TOML -printf 'import fwm;\nint uses_fwm() { return fwm_cases(); }\n' > mixed/tests/modular.cpp -printf 'int uses_headers() { return 0; }\n' > mixed/tests/headers.cpp -(cd mixed && "$MCPP" test > ../f.log 2>&1) || fail "F: the mixed package's tests" f.log -grep -A2 'modular \.\.\. ok' f.log | grep -q "began at fwm" || fail "F: the modular test did not begin at fwm" f.log -grep -A2 'headers \.\.\. ok' f.log | grep -q "began at hdr" || fail "F: the header test did not begin at hdr" f.log - -# G -printf 'test-guide = "GUIDE.md"\n' >> fwm/mcpp.toml -printf '# Writing a test with fwm\n' > fwm/GUIDE.md -(cd mixed && "$MCPP" test --guide > ../g.log 2>&1) || fail "G: --guide" g.log -grep -q "fwm 0.1.0" g.log && grep -q "fwm/GUIDE.md" g.log || fail "G: the framework and its guide" g.log -(cd mixed && "$MCPP" test --guide --message-format json > ../g.json 2>/dev/null) || fail "G: --guide json" g.json -grep -q '"guide":"[^"]*fwm/GUIDE.md"' g.json || fail "G: the guide in JSON" g.json -printf 'import fwm;\nint uses_fwm() { return fwm_cases(); }\nint broken = missing_name;\n' > mixed/tests/modular.cpp -(cd mixed && "$MCPP" test > ../g-fail.log 2>&1) && fail "G: a test that does not compile passed" g-fail.log -grep -q "how these tests are written: mcpp test --guide" g-fail.log || fail "G: the failure does not point to the guide" g-fail.log - -# H -mkdir -p fwh/src deep/tests -cat > fwh/mcpp.toml <<'TOML' -[package] -name = "fwh" -version = "0.1.0" - -[targets.main] -kind = "entry" -main = "src/entry.cpp" -test-main = true - -[dependencies] -hdr = { path = "../hdr" } -TOML -printf 'import std;\nint main() { std::println("began at fwh"); return 0; }\n' > fwh/src/entry.cpp -printf 'int fwh_marker() { return 0; }\n' > fwh/src/fwh.cpp -printf '[package]\nname = "deep"\nversion = "0.1.0"\n\n[dev-dependencies]\nfwh = { path = "../fwh" }\n' > deep/mcpp.toml -printf 'int uses_fwh() { return 0; }\n' > deep/tests/one.cpp -(cd deep && "$MCPP" test > ../h.log 2>&1) || fail "H: a main beneath the dev-dependency was linked" h.log -grep -q "began at fwh" h.log || fail "H: the test did not begin at fwh" h.log - -# I -mkdir -p shlib/src shapp/tests -printf '[package]\nname = "shlib"\nversion = "0.1.0"\n\n[targets.shlib]\nkind = "shared"\n' > shlib/mcpp.toml -printf 'extern "C" int shlib_answer() { return 42; }\n' > shlib/src/shlib.cpp -printf '[package]\nname = "shapp"\nversion = "0.1.0"\n\n[dev-dependencies]\nshlib = { path = "../shlib" }\n' > shapp/mcpp.toml -printf 'extern "C" int shlib_answer();\nint main() { return shlib_answer() == 42 ? 0 : 1; }\n' > shapp/tests/links.cpp -(cd shapp && "$MCPP" test > ../i.log 2>&1) || fail "I: a shared dev-dependency is not linked" i.log - -# J -mkdir -p wsj/one/tests wsj/two/tests -printf '[workspace]\nmembers = ["one", "two"]\n' > wsj/mcpp.toml -printf '[package]\nname = "one"\nversion = "0.1.0"\n\n[dev-dependencies]\nhdr = { path = "../../hdr" }\n' > wsj/one/mcpp.toml -printf 'int uses_headers() { return 0; }\n' > wsj/one/tests/plain.cpp -printf '[package]\nname = "two"\nversion = "0.1.0"\n\n[dev-dependencies]\nfw = { path = "../../fw" }\n' > wsj/two/mcpp.toml -printf 'extern "C++" int run_cases() { return 0; }\n' > wsj/two/tests/begun.cpp -(cd wsj && "$MCPP" test --workspace > ../j.log 2>&1) || fail "J: a member's test took another member's beginning" j.log -grep -q "began at hdr" j.log && grep -q "fw main" j.log || fail "J: each member's test did not begin at its own" j.log - -# K -"$MCPP" new kapp > k.log 2>&1 || fail "K: mcpp new" k.log -(cd kapp && "$MCPP" build && "$MCPP" test && "$MCPP" build && "$MCPP" test) > k-run.log 2>&1 || fail "K: build and test" k-run.log -set -- kapp/target/*/*/build.ninja -[ $# = 1 ] || fail "K: build and test are $# configurations" k-run.log -dir=$(dirname "$1") -[ "$(grep -cE 'obj/main\.(o|obj) ' "$dir/.ninja_log")" = 1 ] || fail "K: the program's source was compiled again" "$dir/.ninja_log" -grep -q 'test_smoke' "$dir/compile_commands.json" || fail "K: the compile database has no test" "$dir/compile_commands.json" -$PY - "$dir/compile_commands.json" <<'PY' || fail "K: the program's source sees the dev-dependency" "$dir/compile_commands.json" -import json, sys -for e in json.load(open(sys.argv[1])): - if e["file"].replace("\\", "/").endswith("kapp/src/main.cpp") and any("__OPENKAL__" in a or "openkal" in a for a in e.get("arguments", [])): - sys.exit(1) -PY - -# L -mkdir -p lib-tools/src lapp/tests -cat > lib-tools/mcpp.toml <<'TOML' -[package] -name = "lib-tools" -version = "0.1.0" - -[build] -sources = ["src/*.c"] -TOML -cat > lib-tools/src/table.c <<'C' -int table_value(void) { return 7; } -#ifdef MAKE_TABLE -#include -int main(void) { printf("%d\n", table_value()); return 0; } -#endif -C -cat > lapp/mcpp.toml <<'TOML' -[package] -name = "lapp" -version = "0.1.0" - -[dev-dependencies] -lib-tools = { path = "../lib-tools" } -TOML -printf 'extern "C" int table_value(void); -int main() { return table_value() == 7 ? 0 : 1; } -' > lapp/tests/own.cpp -(cd lapp && "$MCPP" test > ../l.log 2>&1) || fail "L: a source whose main no command compiles was left out of the link" l.log -grep -q 'own \.\.\. ok' l.log || fail "L: the test did not pass" l.log - -echo "PASS: 1037 a skeleton tests with mtest, and a test needs no main" diff --git a/tests/e2e/1038_test_programs_of_a_directory_and_the_tests_a_change_affects.sh b/tests/e2e/1038_test_programs_of_a_directory_and_the_tests_a_change_affects.sh deleted file mode 100644 index d40766bf9..000000000 --- a/tests/e2e/1038_test_programs_of_a_directory_and_the_tests_a_change_affects.sh +++ /dev/null @@ -1,147 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 1038 -- the files of one directory are one test program, and `mcpp test` runs the tests a change -# affects (docs/08 "Where tests are", "The tests a change affects"). -# -# Criteria: -# A. `[test] programs = "directory"`: the files of a directory below `tests/` are one program, -# named by the directory, whose cases are all of theirs, run as a process per file of its cases -# and, under `split = "none"`, once; a directory whose file defines `main` -# begins there; two files of one directory that define `main` are refused, naming both. -# B. A program built from a file of plain `main` that links a framework's library (it carries the -# protocol's marker and speaks none) is one case, judged by its exit status, run and listed. -# C. `--changed PATH` runs the tests built from PATH (a module a test imports, transitively) and -# not the others; a file no test is built from or names affects none; a file a test names -# affects it; the manifest affects every test. `--since COMMIT` reads the change from git. -# D. Switching between `programs = "directory"` and the default builds without a stale product -# in the way. -# E. Under a shipping profile, `mcpp build` and `mcpp test` are one configuration: the test -# compiles none of the code under test again, which is trimmed as the build trims it, and a -# test program's own file keeps its path (`__FILE__` finds the test's directory). -# F. A case a listed case declares (protocol P3.4) is selected by its name alone; a pattern below -# the listed case runs it whole. -set -e -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } - -cd "$TMP" -"$MCPP" new app > /dev/null 2>&1 -cd app -mkdir -p tests/unit tests/e2e tests/data -cat > src/calc.cppm <<'CPP' -export module app.calc; -export int twice(int x) { return 2 * x; } -CPP -cat > tests/unit/a.cpp <<'CPP' -import mcpplibs.mtest; -import app.calc; -using namespace mcpplibs::mtest; -UTest a{"doubles", [] { expect(eq(twice(2), 4)); }}; -CPP -cat > tests/unit/b.cpp <<'CPP' -import std; -import mcpplibs.mtest; -using namespace mcpplibs::mtest; -UTest b{"reads its data", [] { expect(eq(std::string_view("tests/data/f.txt").size(), 16u)); }}; -CPP -printf 'import std;\nint main() { return 0; }\n' > tests/e2e/plain.cpp -printf 'data\n' > tests/data/f.txt - -# A -printf '\n[test]\nprograms = "directory"\n' >> mcpp.toml -"$MCPP" test > a.log 2>&1 || fail "A: the directory programs" a.log -# A program that speaks the protocol runs as a process per file of its cases (`split = "file"`). -grep -q 'unit/a \.\.\. ok (.*1 case)' a.log && grep -q 'unit/b \.\.\. ok (.*1 case)' a.log \ - || fail "A: unit does not run as a process per file" a.log -# A pattern names a part as the run names it. -"$MCPP" test unit/b > a-part.log 2>&1 || fail "A: a part named by its pattern" a-part.log -grep -q 'unit/b \.\.\. ok' a-part.log && ! grep -q 'unit/a \.\.\.' a-part.log \ - || fail "A: the pattern of a part did not run that part alone" a-part.log -printf 'split = "none"\n' >> mcpp.toml -"$MCPP" test > a.log 2>&1 || fail "A: the directory programs, run once" a.log -grep -q 'unit \.\.\. ok (.*2 cases)' a.log || fail "A: unit is not one program of two cases" a.log -grep -q '^e2e \.\.\. ok' a.log || fail "A: the directory of a file with main" a.log -"$MCPP" test --cases > a-cases.log 2>&1 || fail "A: --cases" a-cases.log -grep -q 'doubles' a-cases.log && grep -q 'reads its data' a-cases.log || fail "A: the program's cases" a-cases.log -# B: a file of plain main beside the library's marker. -grep -q 'e2e: one case, its exit status' a-cases.log || fail "B: a program of plain main is listed as one case" a-cases.log -printf 'int main() { return 0; }\n' > tests/e2e/second.cpp -"$MCPP" test > a2.log 2>&1 && fail "A: two files of one directory that define main were taken" a2.log -grep -q "plain.cpp" a2.log && grep -q "second.cpp" a2.log || fail "A: the refusal does not name both" a2.log -rm tests/e2e/second.cpp - -# C -changed() { # changed LOG PATH...: the tests run - local log=$1; shift - local args=() - for p in "$@"; do args+=(--changed "$p"); done - "$MCPP" test "${args[@]}" > "$log" 2>&1 || fail "C: mcpp test ${args[*]}" "$log" -} -changed c1.log src/calc.cppm -grep -q 'Affected 1 of 3 tests' c1.log && grep -q 'unit \.\.\. ok' c1.log || fail "C: the change of an imported module" c1.log -grep -q 'test_smoke \.\.\.' c1.log && fail "C: a test not built from the change ran" c1.log -changed c2.log README.md -grep -q 'Affected 0 of 3 tests' c2.log || fail "C: a file nothing reads" c2.log -changed c3.log tests/data/f.txt -grep -q 'Affected 1 of 3 tests' c3.log && grep -q 'unit \.\.\. ok' c3.log || fail "C: a file a test names" c3.log -changed c4.log mcpp.toml -grep -q 'Affected 3 of 3 tests' c4.log || fail "C: the manifest affects every test" c4.log -git init -q . && git add -A && git -c user.name=t -c user.email=t@t commit -qm base -printf '\nexport int thrice(int x) { return 3 * x; }\n' >> src/calc.cppm -"$MCPP" test --since HEAD > c5.log 2>&1 || fail "C: --since" c5.log -grep -q 'Affected 1 of 3 tests' c5.log || fail "C: --since reads the working tree's change" c5.log - -# D -sed -i.bak '/^\[test\]/,$d' mcpp.toml -"$MCPP" test > d.log 2>&1 || fail "D: back to a program per file" d.log -grep -q 'unit/a \.\.\. ok' d.log && grep -q 'e2e/plain \.\.\. ok' d.log || fail "D: the programs per file" d.log -printf '\n[test]\nprograms = "directory"\n' >> mcpp.toml -"$MCPP" test > d2.log 2>&1 || fail "D: and back to the directories" d2.log - -# E -cd "$TMP" -"$MCPP" new rel > /dev/null 2>&1 -cd rel -cat > src/where.cppm <<'CPP' -export module rel.where; -import std; -export std::string library_file() { return std::source_location::current().file_name(); } -CPP -cat > tests/where.cpp <<'CPP' -import std; -import rel.where; -int main() { - const std::string here = std::source_location::current().file_name(); - std::println("test {}", here); - std::println("library {}", library_file()); - return std::filesystem::exists(std::filesystem::path(here).parent_path() / "test_smoke.cpp") ? 0 : 1; -} -CPP -"$MCPP" build --release > e.log 2>&1 || fail "E: the release build" e.log -"$MCPP" test --profile release where > e-test.log 2>&1 || fail "E: the release test (its file did not find its directory)" e-test.log -set -- target/*/*/build.ninja -[ $# = 1 ] || fail "E: build and test are $# configurations" e-test.log -grep -q "library rel@0.1.0/src/where.cppm" e-test.log || fail "E: the code under test is not trimmed as built" e-test.log -[ "$(grep -cE 'obj/where\.m\.(o|obj) ' "$(dirname "$1")/.ninja_log")" = 1 ] || fail "E: the test compiled the library again" "$(dirname "$1")/.ninja_log" - -# F -cd "$TMP" -"$MCPP" new grp > /dev/null 2>&1 -cd grp -cat > tests/test_smoke.cpp <<'CPP' -import mcpplibs.mtest; -using namespace mcpplibs::mtest; -UTest g{"g", [] { - "one"_test = [] { expect(true); }; - "two"_test = [] { expect(true); }; -}}; -CPP -"$MCPP" test --cases > f-cases.log 2>&1 || fail "F: --cases" f-cases.log -grep -q 'test_smoke: 1 case' f-cases.log || fail "F: the object is the one listed case" f-cases.log -"$MCPP" test --case 'g/one' > f1.log 2>&1 || fail "F: --case g/one" f1.log -grep -q 'test_smoke \.\.\. ok (.*1 case)' f1.log || fail "F: a declared case is not selected alone" f1.log -"$MCPP" test --case 'g/t*' > f2.log 2>&1 || fail "F: --case g/t*" f2.log -grep -q 'test_smoke \.\.\. ok (.*3 cases)' f2.log || fail "F: a pattern below the listed case does not run it whole" f2.log - -echo "PASS: 1038 test programs of a directory, and the tests a change affects" diff --git a/tests/e2e/1050_a_test_run_is_kept_in_the_mtest_store.sh b/tests/e2e/1050_a_test_run_is_kept_in_the_mtest_store.sh deleted file mode 100755 index 4cfa6c801..000000000 --- a/tests/e2e/1050_a_test_run_is_kept_in_the_mtest_store.sh +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc python3 -# 1050 -- a test run is kept in the mtest store's form (docs/08; mtest spec P12). -# -# `mcpp test` writes `target/mtest/runs//` -- `run.json`, a line per process in -# `programs.ndjson`, a line per case in `cases.ndjson` -- and appends the run to -# `target/mtest/index.ndjson`. `[test] store` names another directory; `"none"` keeps nothing. -# -# Criteria: a run is one record of the index with its counts; each process states its status, -# duration, start and worker; each case its program and status; a second run is a second record; -# the tests of an archive run as the build's do -- their cases reported, the run stored; -# `store = "none"` adds none; `store = ""` writes there. -set -e -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } -PY=python3; command -v python3 >/dev/null 2>&1 || PY=python - -cd "$TMP" -"$MCPP" new prog > new.log 2>&1 || fail "mcpp new" new.log -cd prog -"$MCPP" test > ../t1.log 2>&1 || fail "the first run failed" ../t1.log -[ -f target/mtest/index.ndjson ] || fail "no index" ../t1.log -"$PY" - <<'EOF' || fail "the stored run is not as stated" target/mtest/index.ndjson -import json, pathlib -store = pathlib.Path("target/mtest") -runs = [json.loads(l) for l in (store / "index.ndjson").read_text().splitlines() if l.strip()] -assert len(runs) == 1, runs -run = runs[0] -assert run["schema"] == "mtest.run/1" and run["status"] == "pass", run -assert run["counts"].get("pass", 0) >= 1, run -d = store / "runs" / run["id"] -assert json.loads((d / "run.json").read_text())["id"] == run["id"] -programs = [json.loads(l) for l in (d / "programs.ndjson").read_text().splitlines() if l.strip()] -assert programs and all(p["status"] == "pass" and p["ms"] >= 0 and p["started_ms"] >= 0 - and p["worker"] >= 0 for p in programs), programs -cases = [json.loads(l) for l in (d / "cases.ndjson").read_text().splitlines() if l.strip()] -assert cases and all(c["status"] == "pass" and c["program"] for c in cases), cases -EOF -echo " ok a run is kept with its processes and cases" - -"$MCPP" test > ../t2.log 2>&1 || fail "the second run failed" ../t2.log -[ "$(wc -l < target/mtest/index.ndjson | tr -d ' ')" = 2 ] || fail "a second run is not a second record" target/mtest/index.ndjson -"$MCPP" test --no-run --archive ../tests.tar.gz > ../a1.log 2>&1 || fail "the archive was not written" ../a1.log -"$MCPP" test --archive ../tests.tar.gz > ../a2.log 2>&1 || fail "the archive's tests failed" ../a2.log -grep -q 'test_smoke \.\.\. ok (.*1 case)' ../a2.log || fail "an archived protocol program does not report its case" ../a2.log -[ "$(wc -l < target/mtest/index.ndjson | tr -d ' ')" = 3 ] || fail "the archive's run is not stored" target/mtest/index.ndjson -echo " ok an archive's tests run as the build's: their case reported, the run stored" -printf '\n[test]\nstore = "none"\n' >> mcpp.toml -"$MCPP" test > ../t3.log 2>&1 || fail "the third run failed" ../t3.log -[ "$(wc -l < target/mtest/index.ndjson | tr -d ' ')" = 3 ] || fail "store = none kept a run" target/mtest/index.ndjson -sed -i.bak 's|^store = "none"$|store = "out/test-runs"|' mcpp.toml -"$MCPP" test > ../t4.log 2>&1 || fail "the fourth run failed" ../t4.log -[ -f out/test-runs/index.ndjson ] || fail "store = did not write there" ../t4.log -echo " ok each run is a record; store = none keeps nothing; store = writes there" -echo "PASS: 1050 a test run is kept in the mtest store" diff --git a/tests/e2e/1052_a_framework_that_uses_the_package_under_test_is_built_against_it.sh b/tests/e2e/1052_a_framework_that_uses_the_package_under_test_is_built_against_it.sh deleted file mode 100755 index 3151f4e8e..000000000 --- a/tests/e2e/1052_a_framework_that_uses_the_package_under_test_is_built_against_it.sh +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -source "$(dirname "${BASH_SOURCE[0]}")/_toolchain_env.sh" -# 1052 -- a test framework that itself uses the package under test is built against that package. -# -# mcpplibs.os is tested with mtest, and mtest uses mcpplibs.os. A second copy of the package from -# the framework's own edge would provide every module of the package twice in one program; the -# package being built stands for itself in its own graph (docs/05), whatever version the edge asked -# for, and its tests reach the framework without a cycle of packages. -# -# Criteria: the root's test, written with a framework whose library imports the root's module, -# builds and passes; the root's module is compiled once; a version the edge did not ask for is said. -set -e -t=$(mktemp -d); trap 'rm -rf "$t"' EXIT -fail() { echo "FAIL: $1"; [ -n "${2:-}" ] && printf '%s\n' "$2" | tail -20; exit 1; } -mkdir -p "$t/core/src" "$t/core/tests" "$t/tf/src" -cat > "$t/core/mcpp.toml" <<'EOF' -[package] -namespace = "acme" -name = "core" -version = "0.2.0" - -[targets.core] -kind = "lib" - -[dev-dependencies] -acme.tf = { path = "../tf" } -EOF -cat > "$t/core/src/core.cppm" <<'EOF' -export module acme.core; -export int answer() { return 42; } -EOF -cat > "$t/core/tests/uses_tf.cpp" <<'EOF' -import acme.core; -import acme.tf; -int main() { return tf::check(answer() == 42) ? 0 : 1; } -EOF -cat > "$t/tf/mcpp.toml" <<'EOF' -[package] -namespace = "acme" -name = "tf" -version = "0.1.0" - -[targets.tf] -kind = "lib" - -[dependencies] -acme.core = { path = "../core", version = "0.1.0" } -EOF -cat > "$t/tf/src/tf.cppm" <<'EOF' -export module acme.tf; -import acme.core; -export namespace tf { bool check(bool held) { return held && answer() == 42; } } -EOF -cd "$t/core" -out=$("$MCPP" test 2>&1) || fail "the test of the package under its own framework failed" "$out" -grep -q 'uses_tf \.\.\. ok' <<<"$out" || fail "the test did not pass" "$out" -grep -q 'it is the package being built, 0.2.0, which stands for itself' <<<"$out" \ - || fail "the version the edge did not ask for is not said" "$out" -n=$(find target -name 'core.m.o' -o -name 'core.o' | grep -c 'acme_core\|/core' || true) -[ "$n" -le 1 ] || fail "the root's module was compiled more than once ($n)" "$(find target -name 'core*.o')" -echo " ok the framework is built against the package under test, and the test passes" -echo "PASS: 1052 a framework that uses the package under test is built against it" diff --git a/tests/e2e/1065_test_records_carry_parts_live_cases_and_metrics.sh b/tests/e2e/1065_test_records_carry_parts_live_cases_and_metrics.sh deleted file mode 100755 index 0e0e12150..000000000 --- a/tests/e2e/1065_test_records_carry_parts_live_cases_and_metrics.sh +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc python3 -# 1065 -- `mcpp test` states a program's parts, each case as it happens, and the metrics, bounds, -# kind and tags of its cases, in its records and in the run it keeps (mcpp#917; docs/50 §8; mtest -# spec P5.1, P6, P10.2, P12). -# -# Criteria: -# A. `--cases --message-format json`: a program of several files with cases in two of them has -# `parts`, each `{name, file}` named as the run names it (`unit/a`, `unit/b`). -# B. `--message-format json`: a `case` record for each case's beginning and end comes before -# its program's record; the end carries the case's status, metrics and bounds; a part's record -# states `part_of`; a case in the record's `cases` has its kind, metrics and bounds. -# C. The run kept under target/mtest has, per case, `kind`, `started_ms`, `metrics` and `bounds` -# (P12). -# D. `self env` names the features `test-parts`, `test-case-stream` and `test-metrics`. -set -e -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; tail -40 "$f" 2>/dev/null; done; exit 1; } - -cd "$TMP" -"$MCPP" new app > /dev/null 2>&1 -cd app -mkdir -p tests/unit -cat > tests/unit/a.cpp <<'CPP' -import mcpplibs.mtest; -import mcpplibs.mtest.bench; -using namespace mcpplibs::mtest; -Bench timed{"timed", at_most{"spin", 10.0}, [] { - bench::measure("spin", [] { int x = 0; for (int i = 0; i < 100; ++i) bench::keep(x += i); }, - {.samples = 3, .warmup = 0}); -}}; -CPP -cat > tests/unit/b.cpp <<'CPP' -import mcpplibs.mtest; -using namespace mcpplibs::mtest; -UTest plain{"plain", tag{"quick"}, [] { expect(eq(1 + 1, 2)); }}; -CPP -printf '\n[test]\nprograms = "directory"\n' >> mcpp.toml - -# A -"$MCPP" test --cases --message-format json > a.json 2> a.err || fail "A: --cases" a.json a.err -python3 - a.json <<'PY' || fail "A: the parts" a.json -import json, sys -recs = [json.loads(l) for l in open(sys.argv[1]) if l.startswith("{")] -unit = next(r for r in recs if r.get("test") == "unit") -names = sorted(p["name"] for p in unit["parts"]) -assert names == ["unit/a", "unit/b"], names -assert all(p["file"].endswith(("a.cpp", "b.cpp")) for p in unit["parts"]), unit["parts"] -PY -echo " ok the listing names the parts a run divides the program into" - -# B -"$MCPP" test --message-format json > b.json 2> b.err || fail "B: the run" b.json b.err -python3 - b.json <<'PY' || fail "B: the records" b.json -import json, sys -recs = [json.loads(l) for l in open(sys.argv[1]) if l.startswith("{")] -order = [("case", r["case"]["test"], r["case"]["event"], r["case"]["id"]) if "case" in r - else ("test", r.get("test")) for r in recs if "case" in r or "test" in r] -for part in ("unit/a", "unit/b"): - at = order.index(("test", part)) - events = [o for o in order[:at] if o[0] == "case" and o[1] == part] - assert any(e[2] == "begin" for e in events) and any(e[2] == "end" for e in events), (part, order) -end = next(r["case"] for r in recs if "case" in r and r["case"]["event"] == "end" and r["case"]["id"] == "timed") -assert end["status"] == "pass", end -assert end["metrics"][0]["name"] == "spin" and end["metrics"][0]["unit"] == "s", end -assert end["bounds"] == [{"metric": "spin", "max": 10.0}], end -a = next(r for r in recs if r.get("test") == "unit/a") -assert a["part_of"] == "unit", a -case = next(c for c in a["cases"] if c["id"] == "timed") -assert case["kind"] == "bench" and case["metrics"] and case["bounds"], case -b = next(r for r in recs if r.get("test") == "unit/b") -plain = next(c for c in b["cases"] if c["id"] == "plain") -assert plain.get("tags") == ["quick"], plain -PY -echo " ok cases are records as they happen; parts, metrics, bounds, kind and tags are stated" - -# C -python3 - target/mtest <<'PY' || fail "C: the store" target/mtest/index.ndjson -import json, os, sys -store = sys.argv[1] -run = [json.loads(l) for l in open(os.path.join(store, "index.ndjson"))][-1] -cases = [json.loads(l) for l in open(os.path.join(store, "runs", run["id"], "cases.ndjson"))] -timed = next(c for c in cases if c["id"] == "timed") -assert timed["kind"] == "bench" and timed["started_ms"] >= 0, timed -assert timed["metrics"][0]["name"] == "spin" and timed["bounds"][0]["metric"] == "spin", timed -PY -echo " ok the kept run holds kind, start, metrics and bounds per case" - -# D -"$MCPP" self env --format json > d.json 2>&1 || fail "D: self env" d.json -for f in test-parts test-case-stream test-metrics; do - grep -q "\"$f\"" d.json || fail "D: no feature $f" d.json -done -echo " ok the features are announced" -echo "PASS: 1065 test records carry parts, live cases and metrics" diff --git a/tests/e2e/1068_a_running_test_holds_no_build_directory.sh b/tests/e2e/1068_a_running_test_holds_no_build_directory.sh deleted file mode 100755 index a273cdc2f..000000000 --- a/tests/e2e/1068_a_running_test_holds_no_build_directory.sh +++ /dev/null @@ -1,107 +0,0 @@ -#!/usr/bin/env bash -# requires: -# 1068 — a running test holds no build directory (mcpp#915). -# -# `mcpp test` releases the build directory's lock once its tests are built: a -# `mcpp build` and a second `mcpp test` of the same configuration proceed while -# a test runs, and the second test's protocol files are its own. Before, the -# lock was held until the run ended, and an editor's second test waited for -# the first ("Waiting for the build directory, held by pid ... mcpp test"). -# -# The first test waits for a file that only the commands started beside it -# create; while the lock was held, they waited for it, and it for them, until -# the test's own limit. -set -e -source "$(dirname "$0")/_host_path.sh" - -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -# `timeout` is not on every host (macOS): the command, ended after SECS. -bounded() { - local secs=$1; shift - "$@" & local pid=$! - ( sleep "$secs"; kill "$pid" 2>/dev/null ) & local guard=$! - local rc=0 - wait "$pid" || rc=$? - kill "$guard" 2>/dev/null; wait "$guard" 2>/dev/null || true - return $rc -} -cd "$TMP" -"$MCPP" new app > /dev/null -cd app -rm -f tests/*.cpp -GATE="$(host_path "$TMP")/gate" -cat > tests/waits.cpp < tests/quick.cpp <<'EOF' -int main() { return 0; } -EOF -"$MCPP" test > "$TMP/warm.log" 2>&1 & -warm=$! -sleep 1; touch "$TMP/gate"; wait $warm || { cat "$TMP/warm.log"; echo "FAIL: the tests did not build"; exit 1; } -rm -f "$TMP/gate" - -"$MCPP" test waits > "$TMP/first.log" 2>&1 & -first=$! -# The first test is running once its program has started; give it the time to -# be built (it is built already) and to start. -for _ in $(seq 1 100); do - grep -q 'Running\|waits' "$TMP/first.log" 2>/dev/null && break - sleep 0.1 -done -sleep 1 - -# Beside it: a build of the same configuration, and a second test. -bounded 60 "$MCPP" build > "$TMP/build.log" 2>&1 || { - touch "$TMP/gate"; wait $first || true - cat "$TMP/build.log"; echo "FAIL: mcpp build waited for a running test"; exit 1; } -bounded 60 "$MCPP" test quick > "$TMP/second.log" 2>&1 || { - touch "$TMP/gate"; wait $first || true - cat "$TMP/second.log"; echo "FAIL: a second mcpp test waited for a running one"; exit 1; } -if grep -q 'Waiting for the build directory' "$TMP/build.log" "$TMP/second.log"; then - touch "$TMP/gate"; wait $first || true - cat "$TMP/build.log" "$TMP/second.log"; echo "FAIL: a command waited for the build directory"; exit 1 -fi - -touch "$TMP/gate" -wait $first || { cat "$TMP/first.log"; echo "FAIL: the first test failed"; exit 1; } -grep -q '1 passed; 0 failed' "$TMP/first.log" || { cat "$TMP/first.log"; echo "FAIL: the first test's summary"; exit 1; } -grep -q '1 passed; 0 failed' "$TMP/second.log" || { cat "$TMP/second.log"; echo "FAIL: the second test's summary"; exit 1; } - -# The protocol files were the invocations', and went with them. -if ls -d target/*/*/test-protocol/*/ 2>/dev/null | grep -q .; then - ls -R target/*/*/test-protocol; echo "FAIL: an ended invocation left its protocol directory"; exit 1 -fi - -# The issue's own shape: a test that builds its own project with mcpp. The -# inner build has the test's environment (the subos's `bin` first on PATH); it -# finds the directory free, the configuration the outer command built, and its -# graph current, so it neither waits nor plans. -MCPP_HOST="$(host_path "$(command -v "$MCPP" || echo "$MCPP")")" -INNER_LOG="$(host_path "$TMP")/inner.log" -cat > tests/inner.cpp < -#ifdef _WIN32 -#define WHOLE(s) "\"" s "\"" // cmd /c drops the outer pair of quotes -#else -#define WHOLE(s) s -#endif -int main() { return std::system(WHOLE("\"$MCPP_HOST\" build > \"$INNER_LOG\" 2>&1")) == 0 ? 0 : 1; } -EOF -"$MCPP" test inner > "$TMP/prime.log" 2>&1 || { cat "$TMP/prime.log" "$TMP/inner.log"; echo "FAIL: a test that builds its project failed"; exit 1; } -bounded 120 "$MCPP" test inner > "$TMP/inner-test.log" 2>&1 || { - cat "$TMP/inner-test.log" "$TMP/inner.log"; echo "FAIL: a test that builds its project failed"; exit 1; } -if grep -qE 'Waiting for the build directory|Resolving' "$TMP/inner.log"; then - cat "$TMP/inner.log"; echo "FAIL: the inner build waited or planned"; exit 1 -fi - -echo "PASS: 1068 a running test holds no build directory" diff --git a/tests/e2e/1069_an_unchanged_project_tests_without_planning.sh b/tests/e2e/1069_an_unchanged_project_tests_without_planning.sh deleted file mode 100755 index be7c98206..000000000 --- a/tests/e2e/1069_an_unchanged_project_tests_without_planning.sh +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env bash -# requires: -# resources: machine -# 1069 — the tests of an unchanged project run without planning (the test fast -# path, design 2026-11-10 supplement T7). -# -# Alone on the machine: a fast path declines when the run-time environment's -# description is newer than its graph, and the description is the home's, which -# any mcpp beside this one rewrites when it installs a package. On the day new -# versions of packages many scripts name entered the index, a script that ran -# beside this one installed one between two runs here, and the second was -# planned again (xcode-27, three runs; "the runtime environment's description -# is newer than test.ninja"). -# -# A full `mcpp test` records what it planned for every test of the package -# beside `test.ninja`. The next one admits that record as `mcpp build` admits -# its own and runs the selected tests from it: it resolves no dev-dependency, -# so it prints no `Resolved mcpplibs.` line (the package's programs are built -# as `mcpp build` builds them, which on some hosts plans each time). An edited -# test, a test added or removed, and an edited source each take the full path -# again, and the results are theirs. -set -e - -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -cd "$TMP" -"$MCPP" new app > /dev/null -cd app -rm -f tests/*.cpp -cat >> mcpp.toml <<'EOF' - -[dev-dependencies] -mcpplibs.mtest = "0.5.2" -EOF -write_cases() { - cat > tests/cases.cpp < full.log 2>&1 || fail "the first run failed" full.log -grep -q 'Resolved mcpplibs\.' full.log || fail "the first run did not plan" full.log -"$MCPP" test > fast.log 2>&1 || fail "the second run failed" fast.log -grep -q 'Resolved mcpplibs\.' fast.log && fail "an unchanged project was planned again" fast.log -grep -q 'cases: 2 passed, 0 failed' fast.log || fail "the second run did not run both cases" fast.log - -# A selection runs from the record. -"$MCPP" test cases --case two > case.log 2>&1 || fail "--case two failed" case.log -grep -q 'Resolved mcpplibs\.' case.log && fail "a selection was planned again" case.log -grep -q 'cases: 1 passed, 0 failed' case.log || fail "--case two did not run one case" case.log - -# An edited test is built and its result is the edit's. -sleep 1 -write_cases 3 -"$MCPP" test > edited.log 2>&1 && fail "the edited test passed" edited.log -grep -q 'cases: 1 passed, 1 failed' edited.log || fail "the edit's failure is not reported" edited.log -"$MCPP" test > again.log 2>&1 && fail "the failing test passed from the record" again.log -grep -q 'Resolved mcpplibs\.' again.log && fail "an unchanged failing project was planned again" again.log -grep -q 'cases: 1 passed, 1 failed' again.log || fail "the record's run lost the failure" again.log - -# A test added is planned and run; one removed is no longer run. -sleep 1 -write_cases 2 -printf 'int main() { return 0; }\n' > tests/extra.cpp -"$MCPP" test > added.log 2>&1 || fail "the run with an added test failed" added.log -grep -q 'extra ... ok' added.log || fail "the added test did not run" added.log -rm tests/extra.cpp -"$MCPP" test > removed.log 2>&1 || fail "the run after removing a test failed" removed.log -grep -q 'extra' removed.log && fail "a removed test ran" removed.log - -# A build after the tests keeps its own graph. -"$MCPP" build > build.log 2>&1 || fail "mcpp build failed" build.log -grep -q 'Compiling' build.log && fail "mcpp build after mcpp test compiled again" build.log - -echo "PASS: 1069 an unchanged project tests without planning" diff --git a/tests/e2e/1070_the_test_spec_launches_frameworks_checks_and_output.sh b/tests/e2e/1070_the_test_spec_launches_frameworks_checks_and_output.sh deleted file mode 100755 index ee7e9583a..000000000 --- a/tests/e2e/1070_the_test_spec_launches_frameworks_checks_and_output.sh +++ /dev/null @@ -1,144 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 1070 — the mcpp test spec's B5, B6, O3 and O8 (docs/specs/test.md). -# -# B5 a record of a program built and not run carries how it starts -# (`launch`: argv, env, cwd, via), the arguments after `--` included; -# B6 a framework's entry states the capabilities it implements and its -# session's command, and `mcpp test --guide` reports both; -# O3 a failure's `source` and `fatal`, and a case's `checks`, reach the -# record and the kept run as the program reported them; -# O8 a case's own output: the program's `output` events, or all that a -# program which ran one case wrote; several cases are not divided. -set -e -source "$(dirname "$0")/_host_path.sh" - -TMP=$(mktemp -d) -TMP_HOST="$(host_path "$TMP")" -trap 'rm -rf "$TMP"' EXIT -cd "$TMP" -PY=python3; command -v python3 >/dev/null 2>&1 || PY=python -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f"; cat "$f"; done; exit 1; } - -# The framework: an entry that begins a test and states the spec's keys. -mkdir -p fw/src app/tests -cat > fw/mcpp.toml <<'EOF' -[package] -name = "fw" -version = "0.1.0" - -[targets.main] -kind = "entry" -main = "src/main.cpp" -test-protocol = 1 -test-main = true -test-capabilities = ["check-details", "output"] -test-session = ["fw-serve", "--stdio"] -EOF -cat > fw/src/main.cpp <<'CPP' -import std; -extern bool cases(std::ofstream& report, bool list, const std::set& selected); -int main() { - const char* path = std::getenv("MTEST_REPORT"); - if (!path) return 4; - std::ofstream report(path, std::ios::app); - const std::string mode = std::getenv("MTEST_MODE") ? std::getenv("MTEST_MODE") : "run"; - std::set selected; - if (const char* s = std::getenv("MTEST_SELECT")) { - std::ifstream in(s); - for (std::string line; std::getline(in, line);) selected.insert(line); - } - report << R"({"e":"program","protocol":1,"level":1,"framework":"fw","version":"0.1.0"})" << "\n"; - const bool failed = cases(report, mode == "list", selected); - report << (failed ? R"({"e":"done","status":"fail","counts":{}})" : R"({"e":"done","status":"pass","counts":{}})") << "\n"; - report.flush(); - return failed ? 1 : 0; -} -CPP - -cat > app/mcpp.toml < app/tests/t.cpp <<'CPP' -import std; -bool cases(std::ofstream& r, bool list, const std::set& selected) { - auto on = [&](std::string_view id) { return selected.empty() || selected.contains(std::string(id)); }; - if (list) { - r << R"({"e":"case","id":"stated","name":"stated","file":"t.cpp","line":3})" << "\n"; - r << R"({"e":"case","id":"plain","name":"plain","file":"t.cpp","line":4})" << "\n"; - return false; - } - if (on("stated")) { - r << R"({"e":"begin","id":"stated","t":0})" << "\n"; - r << R"({"e":"output","id":"stated","stream":"out","text":"said by the case\n"})" << "\n"; - r << R"({"e":"failure","id":"stated","file":"t.cpp","line":3,"message":"a == b","expected":"2","actual":"3","source":"expect(eq(a, b));","fatal":true})" << "\n"; - r << R"({"e":"end","id":"stated","status":"fail","ms":0,"checks":{"held":4,"failed":1}})" << "\n"; - } - if (on("plain")) { - std::println("written by plain"); - r << R"({"e":"begin","id":"plain","t":0})" << "\n"; - r << R"({"e":"end","id":"plain","status":"pass","ms":0})" << "\n"; - } - return on("stated"); -} -CPP -cd app - -record() { # record LOG TEST - $PY - "$1" "$2" <<'PY' -import json, sys -for line in open(sys.argv[1]): - line = line.strip() - if not line.startswith("{"): continue - try: r = json.loads(line) - except ValueError: continue - if r.get("test") == sys.argv[2]: print(json.dumps(r)); break -PY -} -field() { $PY -c "import json,sys; r=json.loads(sys.argv[1]); print(eval(sys.argv[2]))" "$1" "$2"; } - -# B6 -"$MCPP" test --guide --message-format json > guide.json 2> guide.err || fail "B6: --guide" guide.json guide.err -$PY - guide.json <<'PY' || fail "B6: the framework's capabilities and session" guide.json -import json, sys -g = json.loads([l for l in open(sys.argv[1]) if l.startswith("{")][0])["guide"] -f = [x for x in g["frameworks"] if x["entry"] == "fw/main"][0] -assert f["capabilities"] == ["check-details", "output"], f -assert f["session"] == ["fw-serve", "--stdio"], f -PY -"$MCPP" test --guide > guide.log 2>&1 || fail "B6: --guide (human)" guide.log -grep -q 'implements check-details, output' guide.log || fail "B6: the human guide does not name the capabilities" guide.log -grep -q 'session fw-serve --stdio' guide.log || fail "B6: the human guide does not name the session" guide.log - -# B5 -"$MCPP" test --no-run --message-format json -- --flag > norun.json 2> norun.err || fail "B5: --no-run" norun.json norun.err -rec=$(record norun.json t) -[ "$(field "$rec" "r['status']")" = built ] || fail "B5: the program is not reported built" norun.json -[ "$(field "$rec" "r['launch']['argv'][-1]")" = --flag ] || fail "B5: the launch omits the arguments after --" norun.json -[ "$(field "$rec" "r['launch']['argv'][0] == r['program'] or r['launch']['argv'][0].replace('\\\\', '/') == r['program']")" = True ] \ - || fail "B5: the launch does not start the program" norun.json -[ "$(field "$rec" "'MTEST_PROTOCOL' in r['launch']['env']")" = False ] || fail "B5: the launch carries the protocol's variables" norun.json -[ -n "$(field "$rec" "r['launch']['cwd']")" ] || fail "B5: the launch names no directory" norun.json - -# O3, O8: both cases -"$MCPP" test --message-format json > all.json 2> all.err && fail "O3: the failing case passed" all.json all.err -rec=$(record all.json t) -[ "$(field "$rec" "r['cases'][0]['failures'][0]['source']")" = 'expect(eq(a, b));' ] || fail "O3: the failure's source" all.json -[ "$(field "$rec" "r['cases'][0]['failures'][0]['fatal']")" = True ] || fail "O3: the failure's fatal" all.json -[ "$(field "$rec" "(r['cases'][0]['checks']['held'], r['cases'][0]['checks']['failed'])")" = "(4, 1)" ] || fail "O3: the case's checks" all.json -[ "$(field "$rec" "r['cases'][0]['output']")" = 'said by the case' ] || fail "O8: the case's stated output" all.json -[ "$(field "$rec" "'output' in r['cases'][1]")" = False ] || fail "O8: the output of a program of two cases was divided" all.json -grep -q '"checks":{"failed":1,"held":4}\|"checks":{"held":4,"failed":1}' target/mtest/runs/*/cases.ndjson \ - || fail "O3: the kept run has no checks" target/mtest/runs/*/cases.ndjson - -# O8: one case run alone -"$MCPP" test --case plain --message-format json > one.json 2> one.err || fail "O8: --case plain" one.json one.err -rec=$(record one.json t) -field "$rec" "r['cases'][0]['output']" | grep -q 'written by plain' || fail "O8: the output of a program that ran one case" one.json - -echo "PASS: 1070 the test spec launches, frameworks, checks and output" diff --git a/tests/e2e/152_test_subdir_names.sh b/tests/e2e/152_test_subdir_names.sh deleted file mode 100755 index 8e1fb34a6..000000000 --- a/tests/e2e/152_test_subdir_names.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash -# requires: -# tests/ subdirectories: same stem in two dirs must coexist, names are relative paths -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT - -cd "$TMP" -mkdir -p pkg/tests/00-a pkg/tests/01-b -cd pkg -cat > mcpp.toml <<'EOF' -[package] -name = "subdirs" -version = "0.1.0" -standard = "c++23" -EOF -cat > tests/00-a/0.cpp <<'EOF' -int main() { return 0; } -EOF -cat > tests/01-b/0.cpp <<'EOF' -int main() { return 0; } -EOF - -out=$("$MCPP" test 2>&1) || { echo "mcpp test failed: $out"; exit 1; } -[[ "$out" == *"00-a/0 ... ok"* ]] || { echo "missing path-based name 00-a/0: $out"; exit 1; } -[[ "$out" == *"01-b/0 ... ok"* ]] || { echo "missing path-based name 01-b/0: $out"; exit 1; } -[[ "$out" == *"2 passed"* ]] || { echo "expected 2 passed: $out"; exit 1; } -echo OK diff --git a/tests/e2e/153_test_isolation.sh b/tests/e2e/153_test_isolation.sh deleted file mode 100755 index 473d64033..000000000 --- a/tests/e2e/153_test_isolation.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env bash -# requires: -# Per-test compile isolation: a non-compiling test fails alone; others build & run. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT - -cd "$TMP" -mkdir -p pkg/tests -cd pkg -cat > mcpp.toml <<'EOF' -[package] -name = "isolation" -version = "0.1.0" -standard = "c++23" -EOF -cat > tests/ok.cpp <<'EOF' -int main() { return 0; } -EOF -cat > tests/runfail.cpp <<'EOF' -int main() { return 1; } -EOF -cat > tests/nocompile.cpp <<'EOF' -int main() { D2X_YOUR_ANSWER x = 1; return x; } -EOF - -set +e -out=$("$MCPP" test 2>&1) -code=$? -set -e -[[ $code -eq 1 ]] || { echo "expected exit 1, got $code: $out"; exit 1; } -[[ "$out" == *"ok ... ok"* ]] || { echo "passing test did not run: $out"; exit 1; } -# Per-test lines now carry the test's own wall time — match the prefix, not the -# whole line, so the duration can be there without pinning its formatting here. -[[ "$out" == *"runfail ... FAIL (exit 1,"* ]] || { echo "runtime failure not reported: $out"; exit 1; } -[[ "$out" == *"nocompile ... FAIL (compile,"* ]] || { echo "compile failure not isolated: $out"; exit 1; } -[[ "$out" == *"1 passed"* && "$out" == *"2 failed"* ]] || { echo "bad summary: $out"; exit 1; } - -# Package-level failure stays a hard error: break a src module, all-red is wrong, -# 'build failed' is right. -mkdir -p src -cat > src/isolation.cppm <<'EOF' -export module isolation; -this is not C++; -EOF -set +e -out2=$("$MCPP" test 2>&1) -code2=$? -set -e -[[ $code2 -ne 0 ]] || { echo "package error must fail: $out2"; exit 1; } -[[ "$out2" != *"nocompile ... FAIL (compile,"* ]] || { echo "package error misattributed to tests: $out2"; exit 1; } -echo OK diff --git a/tests/e2e/154_test_filter.sh b/tests/e2e/154_test_filter.sh deleted file mode 100755 index 53d999ae3..000000000 --- a/tests/e2e/154_test_filter.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env bash -# requires: -# `mcpp test `: substring filter on path-based test names -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT - -cd "$TMP" -mkdir -p pkg/tests/00-a pkg/tests/01-b -cd pkg -cat > mcpp.toml <<'EOF' -[package] -name = "filter" -version = "0.1.0" -standard = "c++23" -EOF -echo 'int main() { return 0; }' > tests/00-a/0.cpp -echo 'int main() { return 0; }' > tests/01-b/0.cpp - -out=$("$MCPP" test 00-a 2>&1) || { echo "filtered run failed: $out"; exit 1; } -[[ "$out" == *"00-a/0 ... ok"* ]] || { echo "filtered test did not run: $out"; exit 1; } -[[ "$out" != *"01-b/0"* ]] || { echo "filter leaked other tests: $out"; exit 1; } -[[ "$out" == *"1 passed"* ]] || { echo "bad summary: $out"; exit 1; } - -set +e -out2=$("$MCPP" test does-not-exist 2>&1) -code2=$? -set -e -[[ $code2 -eq 2 ]] || { echo "no-match should exit 2, got $code2: $out2"; exit 1; } -[[ "$out2" == *"no tests match"* ]] || { echo "missing no-match error: $out2"; exit 1; } -echo OK diff --git a/tests/e2e/155_test_json.sh b/tests/e2e/155_test_json.sh deleted file mode 100755 index 1da63c156..000000000 --- a/tests/e2e/155_test_json.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env bash -# requires: -# --message-format json: NDJSON per test, package error record, pure stdout -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT - -cd "$TMP" -mkdir -p pkg/tests -cd pkg -cat > mcpp.toml <<'EOF' -[package] -name = "jsonout" -version = "0.1.0" -standard = "c++23" -EOF -cat > tests/ok.cpp <<'EOF' -#include -int main() { std::puts("hello from ok"); return 0; } -EOF -echo 'int main() { return 1; }' > tests/runfail.cpp -echo 'int main() { D2X_YOUR_ANSWER x = 1; return x; }' > tests/nocompile.cpp - -set +e -out=$("$MCPP" test --message-format json 2>/dev/null) -code=$? -set -e -[[ $code -eq 1 ]] || { echo "expected exit 1, got $code"; exit 1; } - -# stdout must be pure NDJSON: every line parses as a JSON object. -while IFS= read -r line; do - [[ -z "$line" ]] && continue - [[ "$line" == "{"* ]] || { echo "non-JSON line on stdout: $line"; exit 1; } -done <<< "$out" - -echo "$out" | grep -q '"test":"ok","status":"pass"' || { echo "missing pass record"; exit 1; } -echo "$out" | grep -q '"test":"runfail","status":"run_fail"' || { echo "missing run_fail record"; exit 1; } -echo "$out" | grep -q '"exit_code":1' || { echo "missing exit_code"; exit 1; } -echo "$out" | grep -q '"test":"nocompile","status":"compile_fail"' || { echo "missing compile_fail record"; exit 1; } -echo "$out" | grep -q 'D2X_YOUR_ANSWER' || { echo "compile_output missing diagnostics"; exit 1; } -echo "$out" | grep -q 'hello from ok' || { echo "run_output not captured"; exit 1; } -# The summary now leads with the member it belongs to (empty outside a -# workspace) and carries the build/run split, so match the counts, not the -# whole object prefix. -echo "$out" | grep -q '"passed":1,"failed":2' || { echo "missing summary"; exit 1; } -echo "$out" | grep -qE '"build_ms":[0-9]+,"run_ms":[0-9]+' || { echo "summary missing build/run split"; exit 1; } - -# Invalid format value → usage error -set +e -"$MCPP" test --message-format yaml >/dev/null 2>&1 -[[ $? -eq 2 ]] || { echo "invalid format should exit 2"; exit 1; } -set -e -echo OK diff --git a/tests/e2e/157_test_glob_flags.sh b/tests/e2e/157_test_glob_flags.sh deleted file mode 100755 index 5a25f1b82..000000000 --- a/tests/e2e/157_test_glob_flags.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash -# requires: -# [build].flags globs also cover tests/ TUs (via the per-target flag channel), -# and a glob that matches real files on disk (just not scanned sources) does -# NOT warn "matched no source file". -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT - -cd "$TMP" -mkdir -p pkg/tests -cd pkg -cat > mcpp.toml <<'EOF' -[package] -name = "globtest" -version = "0.1.0" -standard = "c++23" - -[build] -flags = [ - { glob = "tests/tagged*.cpp", cxxflags = ["-DTAG=7"] }, - { glob = "tests/never/**", cxxflags = ["-DNOPE"] }, -] -EOF -cat > tests/tagged.cpp <<'EOF' -#ifndef TAG -#error "per-glob cxxflags did not reach the test TU" -#endif -int main() { return TAG == 7 ? 0 : 1; } -EOF -cat > tests/plain.cpp <<'EOF' -#ifdef TAG -#error "per-glob cxxflags leaked into an unmatched test TU" -#endif -int main() { return 0; } -EOF - -out=$("$MCPP" test 2>&1) || { echo "mcpp test failed: $out"; exit 1; } -[[ "$out" == *"tagged ... ok"* ]] || { echo "tagged test failed: $out"; exit 1; } -[[ "$out" == *"plain ... ok"* ]] || { echo "plain test failed: $out"; exit 1; } -[[ "$out" != *"'tests/tagged*.cpp' matched no source file"* ]] \ - || { echo "spurious dead-glob warning for a tests/ glob: $out"; exit 1; } -[[ "$out" == *"'tests/never/**' matched no source file"* ]] \ - || { echo "genuinely dead glob must still warn: $out"; exit 1; } -echo OK diff --git a/tests/e2e/158_test_signal_and_duration.sh b/tests/e2e/158_test_signal_and_duration.sh deleted file mode 100755 index 84e88d6f4..000000000 --- a/tests/e2e/158_test_signal_and_duration.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env bash -# requires: unix-shell -# Signaled tests report shell-convention exit codes (128+sig) with the signal -# number in JSON, and every JSON test record carries duration_ms. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT - -cd "$TMP" -mkdir -p pkg/tests -cd pkg -cat > mcpp.toml <<'EOF' -[package] -name = "sigdur" -version = "0.1.0" -standard = "c++23" -EOF -cat > tests/crash.cpp <<'EOF' -int main() { int* p = nullptr; return *p; } -EOF -cat > tests/ok.cpp <<'EOF' -int main() { return 0; } -EOF - -set +e -out=$("$MCPP" test --message-format json 2>/dev/null) -code=$? -set -e -[[ $code -eq 1 ]] || { echo "expected exit 1, got $code"; exit 1; } - -echo "$out" | grep -q '"test":"crash","status":"run_fail","exit_code":139,"signal":11' \ - || { echo "signaled test not normalized to 139/11: $out"; exit 1; } -echo "$out" | grep -q '"test":"ok","status":"pass"' || { echo "ok test missing"; exit 1; } -[[ $(echo "$out" | grep -c '"duration_ms":') -eq 2 ]] \ - || { echo "duration_ms missing from test records: $out"; exit 1; } - -# 人读输出同样用 shell 惯例退出码 -out2=$("$MCPP" test crash 2>&1) || true -# Prefix match: the line now also carries the test's wall time. -[[ "$out2" == *"crash ... FAIL (exit 139,"* ]] || { echo "human output not 139: $out2"; exit 1; } -echo OK diff --git a/tests/e2e/159_test_list.sh b/tests/e2e/159_test_list.sh deleted file mode 100755 index 086ce69d2..000000000 --- a/tests/e2e/159_test_list.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env bash -# requires: -# `mcpp test --list`: enumerate (filtered) tests without building anything. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT - -cd "$TMP" -mkdir -p pkg/tests/00-a pkg/tests/01-b -cd pkg -cat > mcpp.toml <<'EOF' -[package] -name = "listing" -version = "0.1.0" -standard = "c++23" -EOF -echo 'int main() { return 0; }' > tests/00-a/0.cpp -echo 'this does not even parse' > tests/01-b/0.cpp # --list 不构建,坏文件也要列出 - -out=$("$MCPP" test --list 2>&1) || { echo "--list failed: $out"; exit 1; } -[[ "$out" == *"00-a/0"* && "$out" == *"01-b/0"* ]] || { echo "missing names: $out"; exit 1; } -[[ ! -d target ]] || { echo "--list must not build (target/ created)"; exit 1; } - -out=$("$MCPP" test --list 00-a --message-format json 2>/dev/null) -echo "$out" | grep -q '"test":"00-a/0","main":".*tests/00-a/0.cpp"' \ - || { echo "json list record missing/bad: $out"; exit 1; } -echo "$out" | grep -qv '"test":"01-b/0"' || { echo "filter leaked into list"; exit 1; } -echo "$out" | grep -q '"summary":{"total":1}' || { echo "missing list summary: $out"; exit 1; } -echo OK diff --git a/tests/e2e/15_test_passing.sh b/tests/e2e/15_test_passing.sh deleted file mode 100755 index 1dc5ad655..000000000 --- a/tests/e2e/15_test_passing.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env bash -# requires: -# `mcpp test` discovers tests/**/*.cpp and runs each as a separate binary. -# All passing → exit 0 + summary "ok. N passed". -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT - -cd "$TMP" -"$MCPP" new myapp > /dev/null -cd myapp - -# mcpp new auto-creates tests/test_smoke.cpp; add a second test for diversity. -cat > tests/test_other.cpp <<'EOF' -import std; -int main() { std::println("test_other: ok"); return 0; } -EOF - -out=$("$MCPP" test 2>&1) || { echo "mcpp test failed: $out"; exit 1; } -echo "$out" | grep -q 'test_smoke ... ok' || { echo "missing smoke pass: $out"; exit 1; } -echo "$out" | grep -q 'test_other ... ok' || { echo "missing other pass: $out"; exit 1; } -echo "$out" | grep -q '2 passed; 0 failed' || { echo "missing summary: $out"; exit 1; } - -echo "OK" diff --git a/tests/e2e/160_test_timeout.sh b/tests/e2e/160_test_timeout.sh deleted file mode 100755 index ea8cf7b72..000000000 --- a/tests/e2e/160_test_timeout.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env bash -# requires: unix-shell -# `mcpp test --timeout `: a hung test is killed and reported as a -# timeout failure; the rest of the suite still runs. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT - -cd "$TMP" -mkdir -p pkg/tests -cd pkg -cat > mcpp.toml <<'EOF' -[package] -name = "hang" -version = "0.1.0" -standard = "c++23" -EOF -cat > tests/sleepy.cpp <<'EOF' -import std; -int main() { std::this_thread::sleep_for(std::chrono::seconds(30)); return 0; } -EOF -cat > tests/quick.cpp <<'EOF' -int main() { return 0; } -EOF - -start=$(date +%s) -set +e -out=$("$MCPP" test --timeout 2 2>&1) -code=$? -set -e -elapsed=$(( $(date +%s) - start )) -[[ $code -eq 1 ]] || { echo "expected exit 1, got $code: $out"; exit 1; } -[[ $elapsed -lt 25 ]] || { echo "timeout did not take effect (took ${elapsed}s)"; exit 1; } -[[ "$out" == *"sleepy ... FAIL (timeout"* ]] || { echo "missing timeout marker: $out"; exit 1; } -[[ "$out" == *"quick ... ok"* ]] || { echo "quick test did not run: $out"; exit 1; } - -set +e -outj=$("$MCPP" test --timeout 2 --message-format json 2>/dev/null) -set -e -echo "$outj" | grep -q '"test":"sleepy","status":"run_fail"' || { echo "json: no run_fail"; exit 1; } -echo "$outj" | grep -q '"timed_out":true' || { echo "json: timed_out missing: $outj"; exit 1; } -echo OK diff --git a/tests/e2e/164_ninja_goal_command_length.sh b/tests/e2e/164_ninja_goal_command_length.sh deleted file mode 100755 index d358cbd15..000000000 --- a/tests/e2e/164_ninja_goal_command_length.sh +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env bash -# requires: -# 164_ninja_goal_command_length.sh — an explicit ninja goal set must travel -# through the manifest, never on the command line. -# -# `mcpp test` names every shared prerequisite as a goal so that a broken package -# source fails once as a package error instead of N times as identical per-test -# compile failures. For a large package that is thousands of object paths: -# FFmpeg's 2281 translation units produced a 50,781-character argv. Windows -# joins argv into one command string for cmd.exe, which caps at 8191 characters -# — the command never ran and a bare 127 came back, from cmd.exe, with no output -# from either ninja or mcpp. mcpp-index's Windows job failed this way on the -# ffmpeg member from 0.0.104 (which introduced explicit goals) onward. -# -# The fix routes the goal set into build.ninja as a phony edge and puts one word -# on the command line. This test asserts that mechanism rather than the 8191 -# limit itself: reproducing the limit needs a package big enough to be far too -# slow here, and the limit does not exist on Linux/macOS at all. What is -# portable — and what actually regressed — is "goals go in the manifest". -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -mkdir -p proj/src proj/tests -cat > proj/mcpp.toml <<'EOF' -[package] -name = "goalset" -version = "0.1.0" -EOF - -# Enough units that a regression would be visible as a long argv, while still -# building in a couple of seconds. -for i in $(seq 1 30); do - echo "int unit_$i() { return $i; }" > "proj/src/unit_$i.cpp" -done -echo 'int main() { return 0; }' > proj/src/main.cpp -cat > proj/tests/sanity.cpp <<'EOF' -extern int unit_1(); -int main() { return unit_1() == 1 ? 0 : 1; } -EOF - -cd proj -"$MCPP" test > test.log 2>&1 || { cat test.log; echo "FAIL: mcpp test"; exit 1; } -grep -q "sanity ... ok" test.log || { cat test.log; exit 1; } - -# The test graph is test.ninja, beside build.ninja (2026.11.10.1). -NINJA=$(find target -name test.ninja -not -path '*/.build-mcpp/*' | head -1) -[ -n "$NINJA" ] || { echo "FAIL: no test.ninja"; exit 1; } - -# The phony edge is how the goal set reaches ninja. -grep -q "^build mcpp-requested-goals : phony " "$NINJA" || { - echo "FAIL: explicit goals must be aggregated into a phony edge" - grep -n "phony" "$NINJA" | head; exit 1; } - -# And the goals themselves must not have been left on a command line. mcpp -# records the ninja program it ran in target/.build_cache; the goal set never -# belongs there either. -if [ -f target/.build_cache ]; then - if grep -qE 'obj/.*obj/.*obj/' target/.build_cache; then - echo "FAIL: goal paths leaked into the recorded ninja invocation" - cat target/.build_cache; exit 1 - fi -fi - -echo "PASS 164_ninja_goal_command_length" diff --git a/tests/e2e/16_test_failing.sh b/tests/e2e/16_test_failing.sh deleted file mode 100755 index 9be4c7d87..000000000 --- a/tests/e2e/16_test_failing.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -# requires: scan-deps -# 1 ok + 1 fail → mcpp test exits 1, summary lists failures. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT - -cd "$TMP" -"$MCPP" new myapp > /dev/null -cd myapp - -# Replace bundled smoke with one that fails. -cat > tests/test_fail.cpp <<'EOF' -int main() { return 1; } -EOF - -# Run; expect non-zero exit -rc=0 -out=$("$MCPP" test 2>&1) || rc=$? - -[[ $rc -ne 0 ]] || { echo "expected non-zero exit, got 0"; exit 1; } -echo "$out" | grep -q 'test_fail ... FAIL' || { echo "missing FAIL marker: $out"; exit 1; } -echo "$out" | grep -q 'test_smoke ... ok' || { echo "smoke should still pass: $out"; exit 1; } -echo "$out" | grep -q 'failures:' || { echo "missing failures list: $out"; exit 1; } -echo "$out" | grep -q '1 passed; 1 failed' || { echo "missing summary: $out"; exit 1; } - -echo "OK" diff --git a/tests/e2e/178_test_observability.sh b/tests/e2e/178_test_observability.sh deleted file mode 100755 index 74c29ae40..000000000 --- a/tests/e2e/178_test_observability.sh +++ /dev/null @@ -1,167 +0,0 @@ -#!/usr/bin/env bash -# requires: unix-shell -# 178_test_observability.sh — `mcpp test` is streamed, timed and bounded. -# -# Four properties, each of which used to be absent and each of which cost a real -# CI investigation: -# -# 1. stdout is line-buffered even into a pipe. It used to be block-buffered -# with a libc-dependent block SIZE (musl 1 KB, Apple libc 64 KB on a pipe), -# so the same ~13 KB of status output flushed 13 times on Linux and zero -# times on macOS — and when the job timeout then killed the process, the -# whole buffer went with it: a 45-minute step with no attributable output. -# 2. Per-test and per-member timings are printed, split into build vs run. -# The old `finished in` started its clock AFTER the package build, so a -# member that spent 87s linking reported 6.53s. -# 3. `--timeout` bounds a hung test RUN and the fan-out continues past it. -# 4. `--build-timeout` bounds a hung compile/link — the half `--timeout` -# never covered, and the one that actually fires in practice. -# -# The `# requires:` line above must stay on line 2 — run_all.sh reads it with -# `sed -n '2p'`, so a requires buried in this block is silently inert (which is -# how an earlier revision of this file ran on Windows and failed there). -# unix-shell, not gcc: macOS is the platform this whole file exists for, and it -# has no GCC. Windows is excluded on purpose — the deadline runner has no -# kill-by-handle path there (mcpp.platform.process), so --timeout and -# --build-timeout are documented POSIX-only and cannot be asserted. -# -# See .agents/docs/2026-07-31-test-workspace-observability-analysis.md. -set -uo pipefail - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -fail() { echo "FAIL: $*"; exit 1; } - -# ── fixture: a fast member and a slow-to-RUN member ─────────────────────── -mkdir -p fast/tests slow/tests -cat > mcpp.toml <<'EOF' -[workspace] -members = ["fast", "slow"] -EOF -for m in fast slow; do - printf '[package]\nname = "%s"\nversion = "0.1.0"\n' "$m" > "$m/mcpp.toml" -done -cat > fast/tests/quick.cpp <<'EOF' -int main() { return 0; } -EOF -cat > slow/tests/hang.cpp <<'EOF' -#include -#include -int main() { std::this_thread::sleep_for(std::chrono::seconds(30)); return 0; } -EOF - -# ── 1. streaming: output arrives while the process is still running ─────── -# The fan-out does `fast` first, then blocks 30s inside `slow`. Reading the -# fast member's result out of a pipe within a few seconds is only possible if -# mcpp flushed it; under block buffering nothing appears until exit. -# `<>` opens the fifo read-write, which never blocks and keeps both a reader and -# a writer alive for the whole test. Opening it read-only (or write-only) blocks -# until the other end appears, which is a deadlock waiting to happen — and a -# test written to make hangs debuggable must not be able to hang the suite. -mkfifo pipe -exec 3<>pipe -"$MCPP" test --workspace --timeout 25 >pipe 2>&1 & -mcpp_pid=$! -streamed="" -while IFS= read -r -t 25 line <&3; do - case "$line" in - *"quick ... ok"*) streamed=yes; break ;; - esac -done -kill "$mcpp_pid" 2>/dev/null -wait "$mcpp_pid" 2>/dev/null -exec 3>&- -[ -n "$streamed" ] || fail "no output reached the pipe before the process ended (stdout not flushed)" -echo " ok: stdout streams line-by-line into a pipe" - -# ── 2. timings: per-test duration + build/run split + per-member progress ─ -out=$("$MCPP" test -p fast 2>&1) || fail "test -p fast errored: $out" -echo "$out" | grep -qE "quick \.\.\. ok \([0-9]+\.[0-9]+s\)" \ - || { echo "$out"; fail "per-test line carries no duration"; } -echo "$out" | grep -qE "finished in [0-9]+\.[0-9]+s \(build [0-9]+\.[0-9]+s \+ run [0-9]+\.[0-9]+s\)" \ - || { echo "$out"; fail "summary carries no build/run split"; } -echo " ok: per-test duration + build/run split" - -# ── 3. --timeout bounds a hung RUN, and the fan-out continues ───────────── -out=$("$MCPP" test --workspace --timeout 2 2>&1) -rc=$? -echo "$out" | grep -q "hang ... FAIL (timeout after 2s)" \ - || { echo "$out"; fail "hung test was not killed by --timeout"; } -echo "$out" | grep -qE "member 'fast' \(1/2\) ok" \ - || { echo "$out"; fail "no per-member progress line"; } -echo "$out" | grep -q "failed members: slow" \ - || { echo "$out"; fail "workspace summary does not name the failed member"; } -[ "$rc" -ne 0 ] || fail "workspace test returned 0 despite a failed member" -echo " ok: --timeout bounds a hung run; fan-out continues; member attributed" - -# `--timeout 0` still means "no limit" — it just has to be asked for now. -"$MCPP" test -p fast --timeout 0 >/dev/null 2>&1 || fail "--timeout 0 rejected" - -# ── 4. --build-timeout bounds a hung COMPILE ───────────────────────────── -# 20k distinct class templates at the default (optimized) profile: seconds to -# compile everywhere, so a 1s ceiling fires with a wide margin. The assertion is -# on the *reported reason*, which must name the build timeout rather than being -# folded into a generic compile failure. -mkdir -p heavy/tests -printf '[package]\nname = "heavy"\nversion = "0.1.0"\n' > heavy/mcpp.toml -{ - i=0 - while [ "$i" -lt 20000 ]; do - printf 'template struct S%d { static constexpr long v = A*%d+1; };\n' "$i" "$i" - printf 'long f%d(long x) { return x + S%d<%d>::v; }\n' "$i" "$i" "$((i % 97))" - i=$((i + 1)) - done - echo 'int main() { return 0; }' -} > heavy/tests/slowbuild.cpp - -out=$(cd heavy && "$MCPP" test --build-timeout 1 2>&1) -echo "$out" | grep -q "build timeout after 1s" \ - || { echo "$out" | head -20; fail "--build-timeout did not stop the slow compile"; } -echo " ok: --build-timeout bounds a slow compile and names itself" - -# Argument validation is shared by all three deadlines. -out=$("$MCPP" test -p fast --build-timeout abc 2>&1) -echo "$out" | grep -q "invalid --build-timeout" || fail "bad --build-timeout accepted" - -# ── 5. --workspace-timeout stops the fan-out and reports what did not run ─ -# Its own fixture, with the slow member FIRST: the deadline is checked before -# each member starts, so it can only skip anything if time has already been -# spent. (In the main fixture `fast` runs first and finishes in milliseconds.) -mkdir -p wst/one/tests wst/two/tests -cat > wst/mcpp.toml <<'EOF' -[workspace] -members = ["one", "two"] -EOF -for m in one two; do - printf '[package]\nname = "wst_%s"\nversion = "0.1.0"\n' "$m" > "wst/$m/mcpp.toml" -done -cp slow/tests/hang.cpp wst/one/tests/hang.cpp -cp fast/tests/quick.cpp wst/two/tests/quick.cpp - -out=$(cd wst && "$MCPP" test --workspace --timeout 3 --workspace-timeout 1 2>&1) -rc=$? -echo "$out" | grep -q "member 'two'" \ - && { echo "$out"; fail "--workspace-timeout did not stop the fan-out"; } -echo "$out" | grep -q "not run (--workspace-timeout 1s reached)" \ - || { echo "$out"; fail "--workspace-timeout did not report skipped members"; } -[ "$rc" -ne 0 ] || fail "workspace timeout returned 0" -echo " ok: --workspace-timeout stops the fan-out and names what did not run" - -# ── 6. JSON contract: no stray lines, member-qualified, workspace summary ─ -out=$("$MCPP" test --workspace --timeout 2 --message-format json 2>/dev/null) -first=$(printf '%s\n' "$out" | head -1) -case "$first" in - '{'*) ;; - *) echo "$out" | head -3; fail "first NDJSON line is not JSON (stdout polluted)" ;; -esac -printf '%s\n' "$out" | grep -q '"member":"fast"' \ - || { echo "$out"; fail "test records are not member-qualified"; } -printf '%s\n' "$out" | grep -q '"workspace_summary"' \ - || { echo "$out"; fail "no workspace_summary record"; } -printf '%s\n' "$out" | grep -q '"failed_members":\["slow"\]' \ - || { echo "$out"; fail "workspace_summary does not name the failed member"; } -echo " ok: JSON stream is clean, member-qualified and has a workspace summary" - -echo "PASS: 178_test_observability" diff --git a/tests/e2e/17_test_no_tests.sh b/tests/e2e/17_test_no_tests.sh deleted file mode 100755 index e3390445a..000000000 --- a/tests/e2e/17_test_no_tests.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash -# requires: -# Project with no tests/ → `mcpp test` says "no tests found" and exits 0. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT - -cd "$TMP" -"$MCPP" new myapp > /dev/null -cd myapp -rm -rf tests # remove the auto-generated test_smoke.cpp - -out=$("$MCPP" test 2>&1) -echo "$out" | grep -q 'no tests found' || { echo "missing 'no tests found': $out"; exit 1; } - -echo "OK" diff --git a/tests/e2e/18_devdeps_isolation.sh b/tests/e2e/18_devdeps_isolation.sh deleted file mode 100755 index 550a23d83..000000000 --- a/tests/e2e/18_devdeps_isolation.sh +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env bash -# requires: -# Verify dev-deps are NOT pulled by `mcpp build` but ARE pulled by `mcpp test`. -# We don't actually fetch a real dev-dep (would need network); we just verify -# that the dev-deps section in mcpp.toml does not appear in the build path's -# announcement output. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT - -cd "$TMP" -"$MCPP" new myapp > /dev/null -cd myapp - -# Add a (path-based, doesn't actually exist) dev-dep — mcpp build should -# ignore it entirely; mcpp test would TRY to load it (and fail because path -# is fake). We don't actually run the test path; just verify build doesn't -# even mention it. -cat > mcpp.toml <<'EOF' -[package] -name = "myapp" -version = "0.1.0" -[language] -standard = "c++23" -modules = true -import_std = true -[modules] -sources = ["src/**/*.cppm", "src/**/*.cpp"] -[targets.myapp] -kind = "bin" -main = "src/main.cpp" -[dev-dependencies."ghost-test-framework"] -path = "/nonexistent/ghost-package" -EOF - -# `mcpp build` must succeed and NOT mention ghost-test-framework -out=$("$MCPP" build 2>&1) -echo "$out" | grep -q 'Finished' || { echo "build did not finish: $out"; exit 1; } -if echo "$out" | grep -q 'ghost-test-framework'; then - echo "build path leaked dev-dep: $out"; exit 1 -fi - -# `mcpp test` should fail (because dev-dep path is bogus) — proving the -# dev-dep IS resolved in the test path. -rc=0 -out=$("$MCPP" test 2>&1) || rc=$? -[[ $rc -ne 0 ]] || { echo "test should have failed (bogus dev-dep)"; exit 1; } -echo "$out" | grep -q 'ghost-test-framework' || { - echo "test path didn't reference dev-dep: $out"; exit 1; } - -echo "OK" diff --git a/tests/e2e/213_build_after_test_is_not_the_test_graph.sh b/tests/e2e/213_build_after_test_is_not_the_test_graph.sh deleted file mode 100755 index 57cae78b1..000000000 --- a/tests/e2e/213_build_after_test_is_not_the_test_graph.sh +++ /dev/null @@ -1,135 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 213_build_after_test_is_not_the_test_graph.sh — mcpp#407. -# -# `mcpp build`, `mcpp test` and `mcpp build --configure-only` all wrote -# `target///build.ninja` (`mcpp test` writes `test.ninja` beside it -# since 2026.11.10.1), and they land in the same directory -# because the fingerprint covers neither dev-dependencies nor test targets. -# A test-mode plan's `default` line names the TEST binaries and does not -# contain the package's own target at all. -# -# The fast path used to check build.ninja's mtime against the SOURCES and -# nothing else, so: -# -# mcpp build → default bin/ -# mcpp test → default bin/ (same file, rewritten) -# mcpp build → Finished in 0.00s (asked ninja for bin/) -# -# reported success for a build that never linked the target. And because -# `tests/` is not in the source sweep, breaking a test file made a plain -# `mcpp build` FAIL with src/ untouched. -# -# DO NOT DELETE ARTIFACTS ANYWHERE IN THIS FILE. A missing output makes ninja -# fail in the shape of a stale graph, the fast path falls back to a full -# prepare, and an unfixed binary goes green. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT - -mkdir -p "$TMP/proj/src" "$TMP/proj/tests" -cd "$TMP/proj" - -cat > mcpp.toml <<'EOF' -[package] -name = "tst" -version = "0.1.0" -EOF -cat > src/main.cpp <<'EOF' -int main() { return 0; } -EOF -cat > tests/smoke.cpp <<'EOF' -int main() { return 0; } -EOF - -find_ninja() { find "$TMP/proj/target" -name build.ninja | head -1; } -default_line() { grep -E '^default ' "$1" | head -1; } - -# ── 1. plain build ────────────────────────────────────────────────────────── -"$MCPP" build > b1.log 2>&1 || { cat b1.log; exit 1; } -N="$(find_ninja)" -[[ -n "$N" ]] || { echo "FAIL: no build.ninja after the first build"; exit 1; } - -BIN="$(dirname "$(dirname "$N")")" # unused guard; the real path is globbed below -target_bin() { ls target/*/*/bin/tst 2>/dev/null | head -1; } - -[[ -n "$(target_bin)" ]] || { - echo "FAIL: plain build produced no bin/tst" - cat b1.log - exit 1 -} - -# The graph must say what it is. Without this line there is nothing for the -# fast path to check, and the rest of this test would pass for the wrong -# reason on a binary that simply always does a full prepare. -grep -q '^# mcpp:graph=normal' "$N" || { - echo "FAIL: a plain build's build.ninja does not declare graph=normal" - head -3 "$N" - exit 1 -} - -# ── 2. mcpp test writes its graph beside the build's ──────────────────────── -# Since 2026.11.10.1 the test graph is `test.ninja` in the same directory: the -# objects, `.ninja_log` and `.ninja_deps` are shared, and build.ninja keeps the -# build's graph. The read-side check below still has to hold for the graphs -# `mcpp build --configure-only` writes into build.ninja. -"$MCPP" test > t1.log 2>&1 || { cat t1.log; exit 1; } -T2="$(dirname "$N")/test.ninja" -[[ -f "$T2" ]] || { echo "FAIL: mcpp test wrote no test.ninja beside $N"; ls "$(dirname "$N")"; exit 1; } -grep -q '^# mcpp:graph=test' "$T2" || { - echo "FAIL: mcpp test's test.ninja does not declare graph=test" - head -3 "$T2" - exit 1 -} -grep -q '^# mcpp:graph=normal' "$N" || { - echo "FAIL: mcpp test rewrote build.ninja" - head -3 "$N" - exit 1 -} - -# ── 3. THE assertion: a plain build after a test must not replay that graph ─ -# -# NOTHING IS TOUCHED HERE. Not the sources, not the outputs. An earlier draft -# ran `touch src/main.cpp` to make the relink observable — which invalidates -# the fast path by mtime and makes the test pass on an unfixed binary. The -# defect only exists while the fast path is ELIGIBLE, so the test has to leave -# every input alone and read the graph instead. -"$MCPP" build > b2.log 2>&1 || { - echo "FAIL: plain build after mcpp test failed" - cat b2.log - exit 1 -} -N3="$(find_ninja)" -grep -q '^# mcpp:graph=normal' "$N3" || { - echo "FAIL: plain build replayed the test graph (build.ninja still says test)" - echo " this is mcpp#407: the fast path never checked what graph it had" - head -3 "$N3" - cat b2.log - exit 1 -} -case "$(default_line "$N3")" in - *bin/tst*) ;; - *) - echo "FAIL: default line still names the test graph: $(default_line "$N3")" - exit 1 - ;; -esac -[[ -n "$(target_bin)" ]] || { - echo "FAIL: bin/tst is gone after a plain build" - exit 1 -} - -# ── 4. a broken TEST file must not fail a plain build ─────────────────────── -# src/ is untouched here. Before the fix this failed with a compile error in -# tests/smoke.cpp, from `mcpp build`. -cat > tests/smoke.cpp <<'EOF' -this is not valid C++ at all -EOF -"$MCPP" build > b3.log 2>&1 || { - echo "FAIL: a broken tests/*.cpp failed a plain mcpp build" - cat b3.log - exit 1 -} - -echo "PASS: a plain build never replays the test graph, and tests/ cannot break it" diff --git a/tests/e2e/673_test_discover_names_where_tests_are.sh b/tests/e2e/673_test_discover_names_where_tests_are.sh deleted file mode 100644 index b0b5649dc..000000000 --- a/tests/e2e/673_test_discover_names_where_tests_are.sh +++ /dev/null @@ -1,97 +0,0 @@ -#!/usr/bin/env bash -# requires: -# 673_test_discover_names_where_tests_are.sh -- `[test] discover` (#634 A5). -# -# The test set was the literal pattern `tests/**/*.cpp`, one program per file. -# A project whose `tests/` belongs to another build system could not point -# discovery elsewhere. `discover` takes globs in the vocabulary of `[build] -# sources`, `!` exclusions included; a test's name is its path relative to the -# fixed directory of the glob that found it, so the default names every test -# as every earlier release did. -# -# Criteria: -# 1. negative direction first: without the key, `--list` names -# `tests/a_ok.cpp` and `tests/unit/b_ok.cpp` as `a_ok` and `unit/b_ok`; -# 2. `discover = ["checks/**/*.cpp"]` runs `checks/c_ok.cpp` and not the -# failing `tests/fails.cpp`, and names it `c_ok`; -# 3. a `!` glob excludes a file another glob found; -# 4. `discover = []` discovers nothing and says where it looked; -# 5. two files that map to one name are refused, naming both; -# 6. a value that is not an array of strings is refused, naming the key. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } - -"$MCPP" new disc > /dev/null -cd disc -rm -f tests/*.cpp -mkdir -p tests/unit checks/deep -printf 'int main() { return 0; }\n' > tests/a_ok.cpp -printf 'int main() { return 0; }\n' > tests/unit/b_ok.cpp -printf 'int main() { return 1; }\n' > tests/fails.cpp -printf 'int main() { return 0; }\n' > checks/c_ok.cpp -printf 'int main() { return 1; }\n' > checks/deep/skipped.cpp -cp mcpp.toml mcpp.toml.base - -# ── 1. no key: today's names ─────────────────────────────────────────────── -"$MCPP" test --list > l1.log 2>&1 || fail "mcpp test --list failed" l1.log -grep -qx "a_ok" l1.log || fail "the default does not name tests/a_ok.cpp 'a_ok'" l1.log -grep -qx "unit/b_ok" l1.log || fail "the default does not name tests/unit/b_ok.cpp 'unit/b_ok'" l1.log -grep -qx "c_ok" l1.log && fail "the default discovered a file outside tests/" l1.log -echo "without [test] the names are unchanged OK" - -# ── 2 and 3. discover elsewhere, with an exclusion ──────────────────────── -cat mcpp.toml.base > mcpp.toml -cat >> mcpp.toml <<'EOF' - -[test] -discover = ["checks/**/*.cpp", "!checks/deep/**"] -EOF -"$MCPP" test --list > l2.log 2>&1 || fail "mcpp test --list with discover failed" l2.log -grep -qx "c_ok" l2.log || fail "discover did not name checks/c_ok.cpp 'c_ok'" l2.log -grep -q "fails\|a_ok" l2.log && fail "discover still discovered tests/" l2.log -grep -q "skipped" l2.log && fail "the ! glob did not exclude checks/deep/skipped.cpp" l2.log -"$MCPP" test > t2.log 2>&1 || fail "mcpp test with discover failed (a file outside the set ran?)" t2.log -grep -q "c_ok ... ok" t2.log || fail "checks/c_ok.cpp did not run" t2.log -grep -q "fails" t2.log && fail "tests/fails.cpp ran" t2.log -echo "discover runs checks/c_ok.cpp, excludes checks/deep, ignores tests/ OK" - -# ── 4. an empty list discovers nothing, and says where it looked ────────── -cat mcpp.toml.base > mcpp.toml -printf '\n[test]\ndiscover = []\n' >> mcpp.toml -"$MCPP" test > t4.log 2>&1 || fail "mcpp test with discover = [] failed" t4.log -grep -q "no tests found (\[test\] discover = \[\])" t4.log \ - || fail "an empty discover did not say where it looked" t4.log -echo "discover = [] discovers nothing and names the key OK" - -# ── 5. two files, one name ──────────────────────────────────────────────── -cat mcpp.toml.base > mcpp.toml -printf '\n[test]\ndiscover = ["tests/*.cpp", "checks/*.cpp"]\n' >> mcpp.toml -printf 'int main() { return 0; }\n' > checks/a_ok.cpp -set +e -"$MCPP" test --list > t5.log 2>&1; rc=$? -set -e -[ "$rc" -ne 0 ] || fail "two files mapping to 'a_ok' were accepted" t5.log -grep -q "duplicate test name 'a_ok'" t5.log || fail "the refusal does not name the test" t5.log -grep -q "tests/a_ok.cpp" t5.log && grep -q "checks/a_ok.cpp" t5.log \ - || fail "the refusal does not name both files" t5.log -rm -f checks/a_ok.cpp -echo "two files mapping to one name are refused, naming both OK" - -# ── 6. a wrong type ──────────────────────────────────────────────────────── -cat mcpp.toml.base > mcpp.toml -printf '\n[test]\ndiscover = "tests/**/*.cpp"\n' >> mcpp.toml -# `mcpp test`, not `--list`: the listing is best effort over a manifest that -# does not load, so it lists the default set rather than failing. -set +e -"$MCPP" test > t6.log 2>&1; rc=$? -set -e -[ "$rc" -ne 0 ] || fail "a string discover was accepted" t6.log -grep -q "\[test\] discover must be an array" t6.log || fail "the refusal does not name the key" t6.log -echo "a discover that is not an array is refused OK" - -echo "PASS: 673_test_discover_names_where_tests_are" diff --git a/tests/e2e/72_test_profile_features.sh b/tests/e2e/72_test_profile_features.sh deleted file mode 100755 index d62b7517b..000000000 --- a/tests/e2e/72_test_profile_features.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# `mcpp test` honors --profile / --features (issue #131 scenario B). The flag -# selects a whole-build mode for the test build, so it reaches the CODE-UNDER- -# TEST (a shared src/ unit), not just the test entry. Plain `mcpp test` must -# fail; `mcpp test --profile observe` must pass. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" -mkdir -p proj/src proj/tests -cd proj - -# Code under test — a shared library source, compiled once and linked into the -# test binary. Its behavior depends on a macro supplied only by the profile. -cat > src/probe.cpp <<'EOF' -extern "C" int probe_mode(void) { -#ifdef OBSERVE_MODE - return 1; -#else - return 0; -#endif -} -EOF - -cat > tests/test_mode.cpp <<'EOF' -extern "C" int probe_mode(void); -int main() { return probe_mode() == 1 ? 0 : 1; } -EOF - -cat > mcpp.toml <<'EOF' -[package] -name = "proj" -version = "0.1.0" - -[build] -sources = ["src/**/*.cpp"] - -[profile.observe] -cxxflags = ["-DOBSERVE_MODE=1"] -EOF - -# Default profile: the code-under-test is built WITHOUT the macro → test fails. -if "$MCPP" test > plain.log 2>&1; then - cat plain.log - echo "expected default 'mcpp test' to fail (macro should be absent)" - exit 1 -fi - -# --profile observe: the macro reaches src/probe.cpp → test passes. -"$MCPP" test --profile observe > observe.log 2>&1 || { - cat observe.log - echo "expected 'mcpp test --profile observe' to pass" - exit 1 -} - -echo "OK" diff --git a/tests/e2e/745_no_run_builds_the_tests_and_says_so.sh b/tests/e2e/745_no_run_builds_the_tests_and_says_so.sh deleted file mode 100755 index 5a1866e91..000000000 --- a/tests/e2e/745_no_run_builds_the_tests_and_says_so.sh +++ /dev/null @@ -1,184 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 745 -- `mcpp test --no-run` builds the tests and reports that, and the -# report is distinguishable from the one mcpp gives when it tried to run them -# and could not. -# -# WHY THE TWO HAVE TO DIFFER. A target this host cannot execute leaves every -# test `not run` and the command exits 2, which is the correct answer to "do -# these tests pass": mcpp did not find out. But 2 is also what a missing or -# broken runner returns, so a caller that wanted only the build could not tell -# the two apart and had to use `mcpp build` instead -- which builds the -# PACKAGE, and for a package whose only sources are under `tests/` compiles -# nothing of it whatsoever. Measured on mcpp-index's `archive` member, whose -# sources are two files under `tests/`: `mcpp build --target aarch64-macos` -# exits 0 having compiled the member's dependencies and not one line of the -# member, and a compatibility sweep reading that exit code records the member -# as building on macOS. -# -# THE RUNNER HERE IS A NAME THAT IS NOT A PROGRAM, AND THAT IS THE POINT. -# Using an unexecutable target would make this test need a cross toolchain and -# a host that cannot run it; a declared runner that does not exist produces the -# same situation -- tests built, nothing run -- on every host, for the native -# target, with nothing installed. -# -# Five legs: -# A without `--no-run`: exit 2, and the tests are reported `not run`. -# B with `--no-run`: exit 0, and the count is reported as built. -# C a test that does not COMPILE is still a failure under `--no-run`. -# Without C, an implementation that reported everything as built whether -# or not it built would pass A and B. -# D `--no-run` with `--no-runner` is refused. The two names differ by one -# character and mean opposite things, and neither is a weaker form of the -# other, so there is no reading of the pair to prefer. -# E `--workspace` totals the built count. A count that stops at the member -# level leaves the same false reading one level up. -set -e - -MCPP="${MCPP:-mcpp}" -work="$(mktemp -d)" -trap 'rm -rf "$work"' EXIT -cd "$work" - -# NO `src/`: the package's only sources are its tests, which is the shape the -# motivating case has and the shape `mcpp build` compiles nothing of. -mkdir -p tests -cat > mcpp.toml <<'EOF' -[package] -name = "norun" -version = "0.1.0" - -# A name, not a path, and not a program: the runner lookup fails, which is the -# situation this test is about. -[target.HOST_TRIPLE] -runner = ["mcpp-no-such-runner-exists"] -EOF -cat > tests/alpha.cpp <<'EOF' -int main() { return 0; } -EOF -cat > tests/beta.cpp <<'EOF' -int main() { return 0; } -EOF - -host="$("$MCPP" --print-target 2>/dev/null || true)" -if [ -z "$host" ]; then - # `--print-target` postdates some clients; take the triple from a build - # directory instead, which every version writes. - "$MCPP" build >/dev/null 2>&1 || true - host="$(ls target 2>/dev/null | grep -v '^\.' | head -1)" -fi -[ -n "$host" ] || { echo "FAIL: could not determine the host triple"; exit 1; } -sed -i.bak "s/HOST_TRIPLE/$host/" mcpp.toml && rm -f mcpp.toml.bak - -# --- A: it tried to run them and could not ------------------------------- -set +e -out_a="$("$MCPP" test --target "$host" 2>&1)" -rc_a=$? -set -e -printf '%s\n' "$out_a" | tail -3 -if [ "$rc_a" != 2 ]; then - echo "FAIL: A expected exit 2 from a runner that does not exist, got $rc_a" - exit 1 -fi -case "$out_a" in - *"not run"*) ;; - *) echo "FAIL: A did not report the tests as not run"; exit 1 ;; -esac - -# --- B: it was asked not to ---------------------------------------------- -set +e -out_b="$("$MCPP" test --target "$host" --no-run 2>&1)" -rc_b=$? -set -e -printf '%s\n' "$out_b" | tail -3 -if [ "$rc_b" != 0 ]; then - echo "FAIL: B expected exit 0 under --no-run, got $rc_b" - exit 1 -fi -case "$out_b" in - *"2 built, not run"*) ;; - *) echo "FAIL: B did not report two tests as built"; exit 1 ;; -esac -# The word that distinguishes B from A must not appear in B: "N not run" is -# the reading B exists to replace, and a summary carrying both says neither. -case "$out_b" in - *"2 not run"*) echo "FAIL: B reported the tests as not run as well as built"; exit 1 ;; -esac - -# --- C: --no-run does not make a broken test pass ------------------------- -cat > tests/gamma.cpp <<'EOF' -int main() { this_function_does_not_exist(); } -EOF -set +e -out_c="$("$MCPP" test --target "$host" --no-run 2>&1)" -rc_c=$? -set -e -if [ "$rc_c" = 0 ]; then - echo "FAIL: C a test that does not compile was reported as built" - printf '%s\n' "$out_c" | tail -5 - exit 1 -fi -# Non-zero is not enough: it must be non-zero BECAUSE `gamma` did not build, -# and the other two must still be reported as built. -case "$out_c" in - *gamma*) ;; - *) echo "FAIL: C failed without naming the test that did not compile"; exit 1 ;; -esac -case "$out_c" in - *"2 built, not run"*) ;; - *) echo "FAIL: C stopped reporting the tests that did build"; printf '%s\n' "$out_c" | tail -4; exit 1 ;; -esac - -# --- D: the two spellings are not a preference ---------------------------- -set +e -out_d="$("$MCPP" test --target "$host" --no-run --no-runner 2>&1)" -rc_d=$? -set -e -if [ "$rc_d" = 0 ]; then - echo "FAIL: D --no-run with --no-runner was accepted" - exit 1 -fi -case "$out_d" in - *"cannot be combined"*) ;; - *) echo "FAIL: D did not explain why the pair is refused"; printf '%s\n' "$out_d" | tail -3; exit 1 ;; -esac - -# --- E: the workspace total says it too ---------------------------------- -# A count that stops at the member level is the same defect one level up: a -# workspace summary reading "0 passed; 0 failed" is what a workspace with no -# tests reports. -rm -f tests/gamma.cpp -mkdir -p members/one/tests members/two/tests -cat > mcpp.toml < "members/$m/mcpp.toml" < "members/$m/tests/t.cpp" <<'EOF' -int main() { return 0; } -EOF -done -set +e -out_e="$("$MCPP" test --workspace --target "$host" --no-run 2>&1)" -rc_e=$? -set -e -printf '%s -' "$out_e" | tail -3 -if [ "$rc_e" != 0 ]; then - echo "FAIL: E expected exit 0 from a --workspace --no-run run, got $rc_e" - exit 1 -fi -case "$out_e" in - *"2 built, not run"*) ;; - *) echo "FAIL: E the workspace total did not report the built tests"; exit 1 ;; -esac - -echo "PASS: --no-run builds the tests and says so, and says nothing else" diff --git a/tests/e2e/78_test_main_combinations.sh b/tests/e2e/78_test_main_combinations.sh deleted file mode 100755 index 2a6f24d64..000000000 --- a/tests/e2e/78_test_main_combinations.sh +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env bash -# requires: -# 78_test_main_combinations.sh — `mcpp test` must handle every combination of -# {test defines its own main, test relies on the framework's main} × -# {test uses gtest, test doesn't} when gtest is a dev-dependency. -# -# Regression for `ld.lld: duplicate symbol: main`: gtest's gtest_main.cc carries -# its own main(), and mcpp used to inline ALL of a dev-dep's objects into every -# test binary — so a test that defined its own main() collided with gtest_main. -# The fix links kind="lib" dependencies as a static archive (lib.a) placed -# AFTER the test's objects, so the linker pulls gtest_main.o ONLY when main is -# still undefined. Driven by the dep's declared kind — no gtest special-casing. -# -# No `requires:` capability → runs on all three CI platforms (mirrors 15/16/17). -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT - -cd "$TMP" -"$MCPP" new combo > /dev/null -cd combo -# The scaffold's tests use mtest; this test's do not. -perl -0pi -e 's/\n\[dev-dependencies\]\nmcpplibs\.mtest = "[^"]*"\n//' mcpp.toml - -cat >> mcpp.toml <<'EOF' - -[dev-dependencies.compat] -gtest = "1.15.2" -EOF - -rm -f tests/test_smoke.cpp - -# (1) own main + uses gtest → gtest_main.o must NOT be pulled (no dup main) -cat > tests/t_own_main_gtest.cpp <<'EOF' -#include -import std; -TEST(A, ok) { EXPECT_EQ(1 + 1, 2); } -int main(int argc, char** argv) { - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} -EOF - -# (2) no main + uses gtest macros → gtest_main.o provides main -cat > tests/t_framework_main.cpp <<'EOF' -#include -TEST(B, ok) { EXPECT_TRUE(true); } -EOF - -# (3) own main + does NOT use gtest (but gtest is still a dev-dep) → archive -# contributes nothing; previously this collided with gtest_main's main. -cat > tests/t_own_main_no_gtest.cpp <<'EOF' -import std; -int main() { std::println("ok"); return 0; } -EOF - -out=$("$MCPP" test 2>&1) || { echo "FAIL: mcpp test exited non-zero"; echo "$out"; exit 1; } - -for t in t_own_main_gtest t_framework_main t_own_main_no_gtest; do - echo "$out" | grep -q "$t ... ok" || { echo "FAIL: $t did not pass"; echo "$out"; exit 1; } -done -echo "$out" | grep -q '3 passed; 0 failed' || { echo "FAIL: summary mismatch"; echo "$out"; exit 1; } - -# A test that defines its own main must NOT also link gtest_main.o (that would be -# `duplicate symbol: main`); a test relying on the framework's main MUST link it. -# Match the ninja link target with an optional .exe suffix (Windows) and the -# object with either .o / .obj — the gtest_main substring covers both. -# Match by target name + `cxx_link` so path-separator (/, on Windows \) and the -# .exe suffix don't matter. The link line is `build bin[.exe] : cxx_link ...`. -nj=$(find target -name test.ninja -not -path '*/.build-mcpp/*' | head -1) # the test graph (2026.11.10.1) -own_link="$(grep -E 't_own_main_gtest(\.exe)? : cxx_link' "$nj" || true)" -fw_link="$(grep -E 't_framework_main(\.exe)? : cxx_link' "$nj" || true)" -[[ -n "$own_link" ]] || { echo "FAIL: no link line for t_own_main_gtest"; cat "$nj"; exit 1; } -[[ -n "$fw_link" ]] || { echo "FAIL: no link line for t_framework_main"; cat "$nj"; exit 1; } -echo "$own_link" | grep -q 'gtest_main' && { echo "FAIL: own-main test links gtest_main (dup main)"; exit 1; } -echo "$fw_link" | grep -q 'gtest_main' || { echo "FAIL: framework-main test missing gtest_main"; exit 1; } - -echo "OK" diff --git a/tests/e2e/854_a_test_over_several_members_plans_and_builds_once.sh b/tests/e2e/854_a_test_over_several_members_plans_and_builds_once.sh deleted file mode 100755 index a87d83c9b..000000000 --- a/tests/e2e/854_a_test_over_several_members_plans_and_builds_once.sh +++ /dev/null @@ -1,127 +0,0 @@ -#!/usr/bin/env bash -# requires: unix-shell -# 854 -- `mcpp test` over several members plans once per configuration group -# and builds what the members share once. -# -# `test --workspace` used to plan each member alone (`workspace_fanout_members` -# and `run_tests` once per member), so a member two members use had its build -# program run once per member that reached it, and its features were those of -# one member's closure, not the union across the selection. `build` and `emit` -# already planned a selection once per configuration group and gave each -# member's tests to the plan in `member_targets`; `test` now does the same -# (member selection design 2026-09-30, S4). -# -# Criteria: -# F. `a` and `b` share `core`, whose build program appends a line to a file -# under its OUT_DIR on each run; `a` asks for `core`'s feature `extra`. -# After `mcpp test --workspace` the file has one line (it had two), the -# plan states its members once, and both members' tests, each named -# `main`, run and pass. -# G. After `mcpp build --workspace`, `mcpp test --workspace` adds no compile -# edge for `core`'s sources and runs `core`'s program no more. The -# fixture has no dev-dependency, so the test build does not change -# `core`'s features. -set -e - -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } -cd "$TMP" - -mkdir -p core/src a/src a/tests b/src b/tests -cat > mcpp.toml <<'EOF' -[workspace] -members = ["core", "a", "b"] -EOF -cat > core/mcpp.toml <<'EOF' -[package] -name = "core" -version = "0.1.0" - -[features] -extra = [] - -[targets.core] -kind = "lib" -EOF -cat > core/src/core.cppm <<'EOF' -export module sel854_core; -export int core_v() { return 1; } -export int core_extra() { -#ifdef MCPP_FEATURE_EXTRA - return 1; -#else - return 0; -#endif -} -EOF -cat > core/build.mcpp <<'EOF' -import std; -import mcpp; -int main() { - std::ofstream(std::string(mcpp::out_dir()) + "/runs.log", std::ios::app) << "ran\n"; - return 0; -} -EOF -cat > a/mcpp.toml <<'EOF' -[package] -name = "a" -version = "0.1.0" - -[dependencies] -core = { path = "../core", features = ["extra"] } - -[targets.a] -kind = "lib" -EOF -cat > b/mcpp.toml <<'EOF' -[package] -name = "b" -version = "0.1.0" - -[dependencies] -core = { path = "../core" } - -[targets.b] -kind = "lib" -EOF -for m in a b; do - printf 'export module sel854_%s;\nimport sel854_core;\nexport int %s_v() { return core_v(); }\n' $m $m > $m/src/$m.cppm - # Both members name their test `main`: discovery is scoped to each member. - printf 'import sel854_%s;\nint main() { return %s_v() == 1 ? 0 : 1; }\n' $m $m > $m/tests/main.cpp -done - -runs() { find . -name runs.log -exec cat {} + | wc -l | tr -d ' '; } -core_edges() { awk -F'\t' '$4 ~ /(^|\/)obj\/core\/src\/core\.m\.o$/' "$1/.ninja_log" | wc -l | tr -d ' '; } -# A build directory holds build.ninja, test.ninja (a test's graph), or both. -graph_dirs() { find target \( -name build.ninja -o -name test.ninja \) -not -path '*/.build-mcpp/*' -exec dirname {} \; | sort -u; } -build_dir() { graph_dirs | head -1; } -reset() { rm -rf target core/target a/target b/target; } - -# ── F ────────────────────────────────────────────────────────────────────── -reset -"$MCPP" test --workspace > f.log 2>&1 || fail "F: test --workspace failed" f.log -[ "$(runs)" = 1 ] || fail "F: core's build program ran $(runs) times; one plan runs it once" f.log -[ "$(grep -c 'Workspace building' f.log)" = 1 ] || fail "F: the members were planned more than once" f.log -grep -q "Workspace building 2 members: a, b" f.log || fail "F: the plan does not state its two members" f.log -grep -q "member 'a' (2/3) ok — 1 passed" f.log && grep -q "member 'b' (3/3) ok — 1 passed" f.log \ - || fail "F: a and b did not both run their test" f.log -grep -q "workspace result ok. 3 member(s); 2 passed" f.log || fail "F: the workspace result is not that of the two tests" f.log -[ "$(graph_dirs | wc -l | tr -d ' ')" = 1 ] || fail "F: the members were built in more than one directory" f.log -[ "$(core_edges "$(build_dir)")" = 1 ] || fail "F: core was compiled more than once" "$(build_dir)/.ninja_log" -echo "ok: F, the members' shared build program ran once, in one plan, and each member's tests ran" - -# ── G ────────────────────────────────────────────────────────────────────── -reset -"$MCPP" build --workspace > g1.log 2>&1 || fail "G: build --workspace failed" g1.log -dir=$(build_dir) -before=$(core_edges "$dir") -[ "$before" = 1 ] && [ "$(runs)" = 1 ] || fail "G: the build did not compile and run core once" g1.log -"$MCPP" test --workspace > g2.log 2>&1 || fail "G: test --workspace failed" g2.log -[ "$(build_dir)" = "$dir" ] || fail "G: the test build used another directory" g2.log -[ "$(core_edges "$dir")" = "$before" ] || fail "G: test --workspace compiled core again" "$dir/.ninja_log" -[ "$(runs)" = 1 ] || fail "G: test --workspace ran core's build program again" g2.log -grep -q "workspace result ok. 3 member(s); 2 passed" g2.log || fail "G: the tests did not run" g2.log -echo "ok: G, a test after a build compiles nothing of core and runs its program no more" - -echo "PASS: 854_a_test_over_several_members_plans_and_builds_once" diff --git a/tests/e2e/855_a_shared_test_build_is_reported_once_per_group.sh b/tests/e2e/855_a_shared_test_build_is_reported_once_per_group.sh deleted file mode 100755 index 085d13aae..000000000 --- a/tests/e2e/855_a_shared_test_build_is_reported_once_per_group.sh +++ /dev/null @@ -1,107 +0,0 @@ -#!/usr/bin/env bash -# requires: unix-shell -# 855 -- a build that several members' tests share is reported once, by -# addition. -# -# A `test` over several members builds each configuration group once, so the -# time the members' tests waited for their build is the group's, and it cannot -# be split per member. docs/50 §7 forbids changing the meaning of a field, so -# the stream only gains (member selection design 2026-09-30, D6): -# -# - a `{"group_build": {"group": N, "members": [...], "build_ms": M}}` -# record, before the group's first test record; -# - a `build_group` field on each member's summary, naming the group; -# - each member's `build_ms`, which keeps its definition, the wall time its -# tests waited for their build, and is now its group's build time; -# - `elapsed_ms` and each test's `duration_ms`, which keep their meaning. -# -# The human report states each group once: its members and its build time. -# -# Criteria: -# H. The JSON stream of `mcpp test --workspace` over `a` and `b`, which are -# one configuration, has one `group_build` record, before every test -# record, naming both; both members' summaries carry its `build_group` -# and the same `build_ms`; every line of the stream is one JSON object; -# the `workspace_summary` record still ends it. -# I. The human report has one line for the group, naming both members and -# the build time, and each member's line states the time of its run. -# J. A member a `-p` does not name is in no record of the stream. -set -e - -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } -cd "$TMP" - -cat > mcpp.toml <<'EOF' -[workspace] -members = ["a", "b", "c"] -EOF -for m in a b c; do - mkdir -p $m/src $m/tests - printf '[package]\nname = "%s"\nversion = "0.1.0"\n\n[targets.%s]\nkind = "lib"\n' $m $m > $m/mcpp.toml - printf 'export module sel855_%s;\nexport int %s_v() { return 1; }\n' $m $m > $m/src/$m.cppm - printf 'import sel855_%s;\nint main() { return %s_v() == 1 ? 0 : 1; }\n' $m $m > $m/tests/test_$m.cpp -done - -# ── H ────────────────────────────────────────────────────────────────────── -# Records are read from stdout alone; the human narration is not part of it. -"$MCPP" test -p a -p b --message-format json > h.json 2> h.err || fail "H: test -p a -p b failed" h.json h.err -while IFS= read -r line; do - case "$line" in - '{'*'}') ;; - *) fail "H: a line of the stream is not one JSON object: $line" h.json ;; - esac -done < h.json - -[ "$(grep -c '"group_build"' h.json)" = 1 ] || fail "H: the stream does not have exactly one group_build record" h.json -first=$(head -1 h.json) -case "$first" in - '{"group_build":'*) ;; - *) fail "H: group_build is not before the first test record" h.json ;; -esac -echo "$first" | grep -q '"group":0' || fail "H: the group is not numbered from 0" h.json -echo "$first" | grep -q '"members":\["a","b"\]' || fail "H: group_build does not name both members" h.json -gms=$(echo "$first" | sed -n 's/.*"build_ms":\([0-9][0-9]*\).*/\1/p') -[ -n "$gms" ] || fail "H: group_build has no build_ms" h.json - -for m in a b; do - s=$(grep '"summary"' h.json | grep "\"member\":\"$m\"") || fail "H: no summary for $m" h.json - echo "$s" | grep -q '"build_group":0' || fail "H: $m's summary does not name its group" h.json - ms=$(echo "$s" | sed -n 's/.*"build_ms":\([0-9][0-9]*\).*/\1/p') - [ "$ms" = "$gms" ] || fail "H: $m's build_ms ($ms) is not its group's ($gms)" h.json - echo "$s" | grep -q '"elapsed_ms":' && echo "$s" | grep -q '"run_ms":' || fail "H: $m's summary lost a field" h.json - grep -q "\"member\":\"$m\",\"test\":\"test_$m\",\"status\":\"pass\"" h.json || fail "H: no passing record for $m's test" h.json - # duration_ms keeps its meaning, the run of a test that ran; the test - # binary's own build is the added build_ms, which a first build makes - # non-zero. - rec=$(grep "\"member\":\"$m\",\"test\":" h.json) - d=$(echo "$rec" | sed -n 's/.*"duration_ms":\([0-9][0-9]*\).*/\1/p') - [ -n "$d" ] || fail "H: no duration_ms for $m's test" h.json - b=$(echo "$rec" | sed -n 's/.*"build_ms":\([0-9][0-9]*\).*/\1/p') - [ -n "$b" ] || fail "H: no build_ms for $m's test" h.json -done -last=$(tail -1 h.json) -case "$last" in - '{"workspace_summary":'*) ;; - *) fail "H: the stream does not end with workspace_summary" h.json ;; -esac -echo "$last" | grep -q '"members":2,"passed":2,"failed":0' || fail "H: workspace_summary does not count a and b" h.json -echo "ok: H, one group_build record before the tests, and the summaries name their group" - -# ── J ────────────────────────────────────────────────────────────────────── -! grep -q '"member":"c"' h.json || fail "J: member c, which no -p named, is in the stream" h.json -[ -z "$(find target -path '*/obj/c*' 2>/dev/null)" ] || fail "J: c was built" h.err -echo "ok: J, a member no -p names is in no record" - -# ── I ────────────────────────────────────────────────────────────────────── -"$MCPP" test -p a -p b > i.log 2>&1 || fail "I: test -p a -p b failed" i.log -[ "$(grep -cE 'Workspace +built members a, b in [0-9]+\.[0-9]+s' i.log)" = 1 ] \ - || fail "I: the report does not have one line for the group, with its members and build time" i.log -for m in a b; do - grep -qE "member '$m' \([12]/2\) ok — 1 passed, run [0-9]+\.[0-9]+s" i.log \ - || fail "I: member $m's line does not state its run time" i.log -done -echo "ok: I, the human report states the group once and each member's run" - -echo "PASS: 855_a_shared_test_build_is_reported_once_per_group" diff --git a/tests/e2e/856_a_test_over_several_members_continues_past_a_failing_one.sh b/tests/e2e/856_a_test_over_several_members_continues_past_a_failing_one.sh deleted file mode 100755 index ce6dc8eef..000000000 --- a/tests/e2e/856_a_test_over_several_members_continues_past_a_failing_one.sh +++ /dev/null @@ -1,152 +0,0 @@ -#!/usr/bin/env bash -# requires: unix-shell -# 856 -- `mcpp test -p a -p b` tests both members (mcpp#750), and a member that -# fails does so alone. -# -# `mcpp test -p a -p b` built and tested `b` alone, reported one member, and -# exited 0 (mcpp#750). Now every member a `-p` names is tested, in one plan, -# and the report counts each. A test over several members continues past a -# failing member, whatever failed in it: a test, its package's build, or its -# plan (member selection design 2026-09-30, D1 and S4); and a member's tests -# run with that member's own runtime, never another member's. -# -# Criteria: -# I. #750's reproduction: `mcpp test -p a -p b` runs the tests of `a` and of -# `b`, in either order, the workspace summary counts both members, and -# `c` is untouched. -# K. A member whose test fails is reported FAILED; the others run, pass, and -# are reported; the command exits 1. -# L. A member whose package does not compile is reported FAILED, naming -# why; the others still run and pass. -# M. A member that fails to plan is reported FAILED; the others are planned -# without it and run. -# N. (Linux) A member's tests see the runtime directories of that member's -# closure, and not another member's: two members in one plan, each -# declaring a `[runtime] library_dirs`, each test seeing its own -# directory in LD_LIBRARY_PATH and not the other's. -set -e - -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } -cd "$TMP" - -# A workspace of libraries a, b and c, each with one test. -mk() { # mk - local ws=$1; shift - mkdir -p "$ws" - local list="" m - for m in "$@"; do list="$list${list:+, }\"$m\""; done - printf '[workspace]\nmembers = [%s]\n' "$list" > "$ws/mcpp.toml" - for m in "$@"; do - mkdir -p "$ws/$m/src" "$ws/$m/tests" - printf '[package]\nname = "%s"\nversion = "0.1.0"\n\n[targets.%s]\nkind = "lib"\n' $m $m > "$ws/$m/mcpp.toml" - printf 'export module sel856_%s;\nexport int %s_v() { return 1; }\n' $m $m > "$ws/$m/src/$m.cppm" - printf 'import sel856_%s;\nint main() { return %s_v() == 1 ? 0 : 1; }\n' $m $m > "$ws/$m/tests/test_$m.cpp" - done -} - -# ── I ────────────────────────────────────────────────────────────────────── -mk w1 a b c -cd w1 -"$MCPP" test -p a -p b > i1.log 2>&1 || fail "I: test -p a -p b failed" i1.log -grep -q "test_a ... ok" i1.log || fail "I: a's test did not run" i1.log -grep -q "test_b ... ok" i1.log || fail "I: b's test did not run (the repeated -p kept one value)" i1.log -! grep -q "test_c" i1.log || fail "I: c's test ran although no -p named it" i1.log -grep -q "workspace result ok. 2 member(s); 2 passed; 0 failed" i1.log || fail "I: the workspace summary does not count both members" i1.log -"$MCPP" test -p b -p a > i2.log 2>&1 || fail "I: test -p b -p a failed" i2.log -grep -q "workspace result ok. 2 member(s); 2 passed; 0 failed" i2.log || fail "I: the reverse order counts differently" i2.log -"$MCPP" test -p a -p b --message-format json > i3.json 2> i3.err || fail "I: json test -p a -p b failed" i3.json i3.err -grep -q '"workspace_summary":{"members":2,"passed":2,"failed":0' i3.json || fail "I: the JSON summary does not count both members" i3.json -[ -z "$(find target -path '*/obj/c*' 2>/dev/null)" ] || fail "I: c was built" i1.log -cd .. -echo "ok: I, test -p a -p b tests both members and counts both" - -# ── K ────────────────────────────────────────────────────────────────────── -mk w2 a b c -printf 'int main() { return 1; }\n' > w2/c/tests/test_c.cpp -cd w2 -rc=0 -"$MCPP" test --workspace > k.log 2>&1 || rc=$? -[ "$rc" = 1 ] || fail "K: a failing test did not exit 1 (got $rc)" k.log -grep -q "member 'a' (1/3) ok" k.log && grep -q "member 'b' (2/3) ok" k.log || fail "K: the passing members were not reported" k.log -grep -q "member 'c' (3/3) FAILED" k.log || fail "K: the failing member was not reported" k.log -grep -q "failed members: c" k.log || fail "K: the failing member is not named in the summary" k.log -[ "$(grep -c 'Workspace building' k.log)" = 1 ] || fail "K: the members were planned more than once" k.log -cd .. -echo "ok: K, a failing test fails its member alone, and the others are reported" - -# ── L ────────────────────────────────────────────────────────────────────── -mk w3 a b c -printf 'export module sel856_c;\nexport int c_v() { return "not an int"; }\n' > w3/c/src/c.cppm -cd w3 -rc=0 -"$MCPP" test --workspace > l.log 2>&1 || rc=$? -[ "$rc" -ne 0 ] || fail "L: a member whose package does not compile exited 0" l.log -grep -q "test_a ... ok" l.log && grep -q "test_b ... ok" l.log || fail "L: the members whose packages compile did not run" l.log -grep -q "member 'c' (3/3) FAILED" l.log || fail "L: the member whose package does not compile was not reported" l.log -grep -q "member 'c': its package did not build" l.log || fail "L: the report does not say why c did not run" l.log -grep -q "failed members: c" l.log || fail "L: c is not named in the summary" l.log -! grep -q "test_c ... ok" l.log || fail "L: c's test ran" l.log -cd .. -echo "ok: L, a member whose package does not compile fails alone, and the others run" - -# ── M ────────────────────────────────────────────────────────────────────── -mk w4 a b d c -cat > w4/d/mcpp.toml <<'EOF' -[package] -name = "d" -version = "0.1.0" - -[dependencies] -absent = { path = "../no-such-directory" } - -[targets.d] -kind = "lib" -EOF -cd w4 -rc=0 -"$MCPP" test --workspace > m.log 2>&1 || rc=$? -[ "$rc" -ne 0 ] || fail "M: a member that cannot be planned exited 0" m.log -for m in a b c; do - grep -q "test_$m ... ok" m.log || fail "M: member $m did not run although only d cannot be planned" m.log -done -grep -q "member 'd' (3/4) FAILED" m.log || fail "M: d was not reported as failed" m.log -grep -q "failed members: d" m.log || fail "M: d is not named in the summary" m.log -grep -q "workspace test: 1/4 member(s) failed; 3 passed" m.log || fail "M: the summary does not count one failed member" m.log -cd .. -echo "ok: M, a member that cannot be planned fails alone, and the rest are planned without it" - -# ── N ────────────────────────────────────────────────────────────────────── -if [ "$(uname -s)" = Linux ]; then - mkdir -p w5 - printf '[workspace]\nmembers = ["x", "y"]\n' > w5/mcpp.toml - for m in x y; do - other=$([ $m = x ] && echo y || echo x) - mkdir -p w5/$m/src w5/$m/tests w5/$m/rt856$m - : > w5/$m/rt856$m/keep - printf '[package]\nname = "%s"\nversion = "0.1.0"\n\n[runtime]\nlibrary_dirs = ["rt856%s"]\n\n[targets.%s]\nkind = "lib"\n' $m $m $m > w5/$m/mcpp.toml - printf 'export module sel856_%s;\nexport int %s_v() { return 1; }\n' $m $m > w5/$m/src/$m.cppm - cat > w5/$m/tests/env.cpp < -#include -int main() { - const char* p = std::getenv("LD_LIBRARY_PATH"); - if (!p) return 2; - if (!std::strstr(p, "rt856$m")) return 3; // its own runtime directory - if (std::strstr(p, "rt856$other")) return 4; // another member's - return 0; -} -EOF - done - cd w5 - "$MCPP" test --workspace > n.log 2>&1 || fail "N: a member's test saw the wrong runtime directories (exit 3: own missing, 4: another's present)" n.log - [ "$(grep -c 'Workspace building' n.log)" = 1 ] || fail "N: the two members were planned more than once" n.log - grep -q "workspace result ok. 2 member(s); 2 passed" n.log || fail "N: both members' tests did not pass" n.log - cd .. - echo "ok: N, each member's tests run with that member's runtime directories alone" -else - echo "skip: N, LD_LIBRARY_PATH is the Linux runtime search variable" -fi - -echo "PASS: 856_a_test_over_several_members_continues_past_a_failing_one" diff --git a/tests/e2e/865_mcpp_test_reports_its_steps_on_stderr_and_its_results_on_stdout.sh b/tests/e2e/865_mcpp_test_reports_its_steps_on_stderr_and_its_results_on_stdout.sh deleted file mode 100644 index 709a8b3e0..000000000 --- a/tests/e2e/865_mcpp_test_reports_its_steps_on_stderr_and_its_results_on_stdout.sh +++ /dev/null @@ -1,104 +0,0 @@ -#!/usr/bin/env bash -# requires: unix-shell -# 865 -- `mcpp test` narrates on standard error and reports its results on -# standard output (output streams plan 2026-10-01, §6 R3, and §11 item 5). -# -# The steps of a test run (`Resolving`, `Compiling`, `Running`) are narration, -# like a build's. The report is the command's result: each test's verdict, the -# output of a test that failed, the `test result` line and, for a workspace, -# the `workspace result` line. Cargo prints libtest's report on standard output -# for the same reason, and a script that redirects a test run to a file -# expects the verdicts in it. -# -# Criteria: -# A. A run whose tests pass: standard output has each verdict and the -# `test result ok.` line, and none of the steps; standard error has the -# steps, and none of the verdicts. -# B. A run with a failing test exits 1: standard output has its verdict, its -# output and the `failures:` list; standard error has the failed summary. -# C. `mcpp test --list` prints the names on standard output alone. -# D. `mcpp test --workspace`: `workspace result ok.` is on standard output -# and the members' steps are on standard error. -set -e - -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } -cd "$TMP" - -mkdir -p p/src p/tests -cat > p/mcpp.toml <<'EOF' -[package] -name = "p" -version = "0.1.0" - -[targets.p] -kind = "bin" -main = "src/main.cpp" -EOF -printf 'int main() { return 0; }\n' > p/src/main.cpp -cat > p/tests/t_ok.cpp <<'EOF' -#include -int main() { std::puts("output of the passing test"); return 0; } -EOF -cd p - -# ── A ────────────────────────────────────────────────────────────────────── -"$MCPP" test > a.out 2> a.err || fail "A: mcpp test failed" a.out a.err -grep -q 't_ok ... ok' a.out || fail "A: the verdict is not on standard output" a.out -grep -q 'test result ok\.' a.out || fail "A: the test result line is not on standard output" a.out -grep -qE 'Compiling|Resolving|Resolved' a.out && fail "A: a step is on standard output" a.out -grep -q 'Compiling' a.err || fail "A: the steps are not on standard error" a.err -grep -q 'test result' a.err && fail "A: the test result line is on standard error" a.err -grep -q '\.\.\. ok' a.err && fail "A: a verdict is on standard error" a.err - -# ── B ────────────────────────────────────────────────────────────────────── -cat > tests/t_bad.cpp <<'EOF' -#include -int main() { std::puts("output of the failing test"); return 1; } -EOF -set +e -"$MCPP" test > b.out 2> b.err; rc=$? -set -e -[ "$rc" = 1 ] || fail "B: mcpp test with a failing test exited $rc, not 1" b.out b.err -grep -q 't_bad ... FAIL' b.out || fail "B: the failing verdict is not on standard output" b.out -grep -q 'output of the failing test' b.out || fail "B: the failing test's output is not on standard output" b.out -grep -q 'failures:' b.out || fail "B: the failures list is not on standard output" b.out -grep -q 'test result: FAILED' b.err || fail "B: the failed summary is not on standard error" b.err -grep -qE 'Compiling|Resolving' b.out && fail "B: a step is on standard output" b.out -rm tests/t_bad.cpp - -# ── C ────────────────────────────────────────────────────────────────────── -"$MCPP" test --list > c.out 2> c.err || fail "C: mcpp test --list failed" c.err -grep -q '^t_ok$' c.out || fail "C: the name is not on standard output" c.out -grep -q 't_ok' c.err && fail "C: a name is on standard error" c.err - -# ── D ────────────────────────────────────────────────────────────────────── -cd "$TMP" -mkdir -p ws/a/src ws/a/tests ws/b/src ws/b/tests -cat > ws/mcpp.toml <<'EOF' -[workspace] -members = ["a", "b"] -EOF -for m in a b; do - cat > ws/$m/mcpp.toml < ws/$m/src/main.cpp - printf 'int main() { return 0; }\n' > ws/$m/tests/t_$m.cpp -done -cd ws -"$MCPP" test --workspace > d.out 2> d.err || fail "D: mcpp test --workspace failed" d.out d.err -grep -q 'workspace result ok\.' d.out || fail "D: the workspace result line is not on standard output" d.out -grep -q 'test result ok\.' d.out || fail "D: a member's test result line is not on standard output" d.out -grep -qE 'Compiling|Resolving|Resolved|Workspace' d.out && fail "D: a step is on standard output" d.out -grep -q 'Compiling' d.err || fail "D: the steps are not on standard error" d.err -grep -q 'workspace result' d.err && fail "D: the workspace result line is on standard error" d.err - -echo "OK: mcpp test narrates on standard error and reports on standard output" diff --git a/tests/e2e/90_workspace_test.sh b/tests/e2e/90_workspace_test.sh deleted file mode 100755 index 650b5ed8e..000000000 --- a/tests/e2e/90_workspace_test.sh +++ /dev/null @@ -1,76 +0,0 @@ -#!/usr/bin/env bash -# 90_workspace_test.sh — workspace-aware `mcpp test`: `-p `, -# `--workspace`, and bare `mcpp test` at a virtual workspace root. The headline -# fix: discovery is scoped to the selected member, so two members may each have a -# `tests/main.cpp` (same stem) without the old "duplicate test name 'main'" error. -# See .agents/docs/2026-06-30-workspace-test-and-zero-shell-index-design.md. -# -# requires: gcc -set -euo pipefail - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -mkdir -p liba/src liba/tests libb/src libb/tests - -cat > mcpp.toml <<'EOF' -[workspace] -members = ["liba", "libb"] -EOF - -# Each member is a library (so its own source doesn't collide with tests/main.cpp); -# the test binary is the standalone tests/main.cpp. -for L in liba libb; do -cat > "$L/mcpp.toml" < "$L/src/$L.cppm" -done - -# Both members name their test the SAME stem ('main') — this is exactly what used -# to collide at a workspace root. A real (passing) assertion in each. -cat > liba/tests/main.cpp <<'EOF' -int main() { return (1 + 1 == 2) ? 0 : 1; } -EOF -cat > libb/tests/main.cpp <<'EOF' -int main() { return (2 * 2 == 4) ? 0 : 1; } -EOF - -# 1. -p selects one member; its test runs, the other does not. -out=$("$MCPP" test -p liba 2>&1) || { echo "$out"; echo "FAIL: test -p liba errored"; exit 1; } -echo "$out" | grep -q "1 passed" || { echo "$out"; echo "FAIL: liba test did not pass"; exit 1; } -echo "$out" | grep -qi "testing member 'libb'" && { echo "FAIL: -p liba also ran libb"; exit 1; } - -# 2. --workspace runs BOTH members with no duplicate-stem error. -out=$("$MCPP" test --workspace 2>&1) || { echo "$out"; echo "FAIL: test --workspace errored"; exit 1; } -echo "$out" | grep -qi "duplicate test name" && { echo "$out"; echo "FAIL: duplicate-stem bug not fixed"; exit 1; } -echo "$out" | grep -qi "testing member 'liba'" || { echo "$out"; echo "FAIL: liba not tested"; exit 1; } -echo "$out" | grep -qi "testing member 'libb'" || { echo "$out"; echo "FAIL: libb not tested"; exit 1; } - -# 3. Bare `mcpp test` at a virtual workspace root fans out the same way. -out=$("$MCPP" test 2>&1) || { echo "$out"; echo "FAIL: bare test at ws root errored"; exit 1; } -echo "$out" | grep -qi "duplicate test name" && { echo "FAIL: bare test still collides"; exit 1; } -echo "$out" | grep -qi "testing member 'libb'" || { echo "FAIL: bare test did not fan out"; exit 1; } - -# 4. A failing member fails the workspace run (non-zero) but the other still runs. -echo 'int main(){return 1;}' > libb/tests/main.cpp -set +e -out=$("$MCPP" test --workspace 2>&1); rc=$? -set -e -[ "$rc" -ne 0 ] || { echo "$out"; echo "FAIL: failing member did not fail the run"; exit 1; } -# The failure summary now carries counts and wall time as well as the names, so -# the names moved to their own line (`failed members: libb`) instead of being -# appended to the header. Assert both halves — the counts are what tell a reader -# whether "1 member failed" meant one test or four hundred. -echo "$out" | grep -qiE "workspace test: 1/2 member\(s\) failed" \ - || { echo "$out"; echo "FAIL: no workspace failure header"; exit 1; } -echo "$out" | grep -qi "failed members: libb" || { echo "$out"; echo "FAIL: no per-member failure summary"; exit 1; } -echo "$out" | grep -qi "testing member 'liba'" || { echo "FAIL: did not continue past failure"; exit 1; } - -echo "OK" diff --git a/tests/e2e/94_macos_test_stdlib.sh b/tests/e2e/94_macos_test_stdlib.sh deleted file mode 100755 index 42a8a5f5a..000000000 --- a/tests/e2e/94_macos_test_stdlib.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env bash -# requires: macos -# 94_macos_test_stdlib.sh — A1 regression: test binaries must link the -# TOOLCHAIN's libc++, not the system one. The old system -lc++ exception -# split headers (toolchain libc++) from the dylib (Apple's older libc++) -# and broke on libc++ 22's out-of-line __hash_memory — exercised here via -# std::unordered_map string hashing. -# Design: .agents/docs/2026-07-08-root-cause-remediation-design.md A1. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -"$MCPP" new hashapp > /dev/null -cd hashapp - -mkdir -p tests -cat > tests/hash_test.cpp <<'EOF' -// Forces the string-hash path (__hash_memory-class symbols on libc++ >= 22). -import std; - -int main() { - std::unordered_map m; - for (int i = 0; i < 100; ++i) m["key" + std::to_string(i)] = i; - return (m.size() == 100 && m.at("key42") == 42) ? 0 : 1; -} -EOF - -"$MCPP" test > test.log 2>&1 || { cat test.log; echo "FAIL: mcpp test failed"; exit 1; } -grep -Eq "[0-9]+ passed; 0 failed" test.log # template ships its own test_smoke too - -# The link assertion only applies to clang/llvm toolchains (gcc on macOS is -# not a supported config; if resolution picked something else, the behavior -# test above already covered the regression). -TEST_BIN=$(find target -path "*/bin/hash_test" | head -1) -test -x "$TEST_BIN" -if grep -q "Resolved llvm@" test.log || otool -L "$TEST_BIN" | grep -q "libc++"; then - echo "otool -L:" - otool -L "$TEST_BIN" - if otool -L "$TEST_BIN" | grep -q "/usr/lib/libc++"; then - echo "FAIL: test binary links the SYSTEM libc++ (header/dylib split)" - exit 1 - fi - # Either the toolchain's dylib (rpath link) or no libc++ dylib at all - # (static fallback) is acceptable — never Apple's. -fi - -echo "PASS 94_macos_test_stdlib" diff --git a/tests/e2e/989_a_native_legs_tests_run_from_an_archive.sh b/tests/e2e/989_a_native_legs_tests_run_from_an_archive.sh deleted file mode 100755 index de88a089a..000000000 --- a/tests/e2e/989_a_native_legs_tests_run_from_an_archive.sh +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env bash -# 989 -- a native leg's tests run from an archive (SPEC-015 §4; design -# 2026-10-29 rev 2 §3.4). -# -# e2e 964 archives the legs of the openkal set, which run through the -# platform's runner and need llvm; a leg of the host's own system, whose test -# programs the system starts directly, was archived and run on no runner. On -# macOS such a program, unpacked from an archive written on Linux, was not -# started ("invalid argument", mcppls CI 2026-10-30). This test needs nothing -# but the host's toolchain, so every system's shards run it. -# -# Criteria: -# A. `mcpp test --no-run --archive t.tar.gz` writes the host leg's tests. -# B. In a copy of the project without target/, `mcpp test --archive` starts -# them, passes, and builds nothing. -set -e - -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } - -# What the system says about an archived program it would not start. -evidence() { - local program - program=$(find "$TMP/co/target/.test-archive" -type f -name 'smoke*' ! -name '*.tsv' 2>/dev/null | head -1) - [ -n "$program" ] || { echo "--- no unpacked program ---"; return; } - echo "--- $program ---" - file "$program" 2>&1 || true - ls -l "$program" 2>&1 || true - if [ "$(uname -s)" = Darwin ]; then - ls -l@ "$program" 2>&1 || true - xattr -l "$program" 2>&1 || true - codesign -dvvv "$program" 2>&1 | head -20 || true - fi - echo "--- started directly ---" - "$program"; echo "exit $?" - cp "$program" "$TMP/copy" && "$TMP/copy"; echo "a copy: exit $?" -} - -mkdir -p "$TMP/app/src" "$TMP/app/tests" -cd "$TMP/app" -printf '[package]\nname = "app"\nversion = "0.1.0"\n' > mcpp.toml -printf 'import std;\nint main() { std::println("app"); }\n' > src/main.cpp -cat > tests/smoke.cpp <<'CPP' -import std; -int main() { std::vector v{2, 1}; std::ranges::sort(v); return v.front() == 1 ? 0 : 1; } -CPP - -# A -"$MCPP" test --no-run --archive "$TMP/t.tar.gz" > a.log 2>&1 || fail "A: --no-run --archive" a.log -[ -s "$TMP/t.tar.gz" ] || fail "A: no archive written" a.log -echo "ok: the host leg's tests are archived" - -# B: a checkout with nothing built. -mkdir -p "$TMP/co" && cp -R "$TMP/app/src" "$TMP/app/tests" "$TMP/app/mcpp.toml" "$TMP/co/" -cd "$TMP/co" -if ! "$MCPP" test --archive "$TMP/t.tar.gz" > b.log 2>&1 || ! grep -q "smoke ... ok" b.log; then - evidence > ev.log 2>&1 - fail "B: the host leg's archived test did not pass" b.log ev.log -fi -[ -z "$(find target -path '*/.test-archive' -prune -o -name 'smoke*' -print 2>/dev/null)" ] \ - || fail "B: the archive's tests were built again" b.log -echo "ok: the host leg's tests run from the archive" - -echo "PASS: 989 a native leg's tests run from an archive" diff --git a/tests/e2e/993_a_test_describes_its_launch.sh b/tests/e2e/993_a_test_describes_its_launch.sh deleted file mode 100755 index 4aa5ebc3d..000000000 --- a/tests/e2e/993_a_test_describes_its_launch.sh +++ /dev/null @@ -1,146 +0,0 @@ -#!/usr/bin/env bash -# requires: python3 -# 993 -- `mcpp test --print-launch` (docs/50 §9, feature `test-launch`; -# design 2026-10-30 part 2 §7.4): one test, built and described as `mcpp test` -# would start it, for a debugger to start. -# -# Criteria: -# A. The `mcpp.launch` envelope names the test (`data.test`), its program -# (`argv[0]` is `program`, which exists), `env` and `cwd`, and ends its argv -# with the arguments after `--`; the test is not started. -# B. What the envelope describes runs: the argv with the environment exits 0 -# and the test's own output appears. -# C. A pattern several tests contain is resolved to the test named exactly. -# D. Refused, exit 2: no pattern; a pattern naming several tests; -# `--message-format json`; `--no-run`. -# E. `--protocol-version` states `test-launch`. -set -e -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } -PY=python3 - -cd "$TMP" -"$MCPP" new app > /dev/null -cd app -mkdir -p tests/unit -cat > tests/unit/math.cpp <<'EOF' -import std; -int main(int argc, char** argv) { - std::ofstream("started-math") << argc; - std::println("math ran with {} argument(s), last [{}]", argc - 1, argc > 1 ? argv[argc - 1] : ""); - return 0; -} -EOF -cat > tests/unit/math2.cpp <<'EOF' -import std; -int main() { std::ofstream("started-math2") << 1; return 0; } -EOF - -# A -"$MCPP" test unit/math --print-launch -- --flag x > a.out 2> a.err || fail "A: --print-launch failed" a.err -[ ! -e started-math ] && [ ! -e target/started-math ] || fail "A: the test was started" -$PY - a.out <<'EOF' || fail "A: not the test's mcpp.launch envelope" a.out -import json, sys, os -same = lambda a, b: os.path.normcase(os.path.normpath(a)) == os.path.normcase(os.path.normpath(b)) -d = json.load(open(sys.argv[1])) -assert d["kind"] == "mcpp.launch", d["kind"] -data = d["data"] -assert data["test"] == "unit/math", data -assert os.path.isfile(data["program"]), data["program"] -argv = data["argv"] -assert any(same(a, data["program"]) for a in argv), data -assert argv[-2:] == ["--flag", "x"], argv -assert "env" in data and "cwd" in data -last = json.load(open("target/.last-result.json"))["data"] -record = json.load(open(last["result"]))["data"]["configuration"] -assert all(data["configuration"][key] == record[key] for key in ("triple", "profile", "platform")), data -assert isinstance(data["configuration"]["runners"], list), data -EOF -echo "ok: the envelope describes the test" - -# B -$PY - a.out > b.out 2> b.err <<'EOF' || fail "B: what the envelope describes did not run" b.out b.err -import json, sys, os, subprocess -d = json.load(open(sys.argv[1]))["data"] -env = dict(os.environ) -env.update(d.get("env") or {}) -r = subprocess.run(d["argv"], cwd=d["cwd"], env=env, capture_output=True, text=True) -sys.stdout.write(r.stdout) -sys.exit(r.returncode) -EOF -grep -q "math ran with 2 argument(s), last \[x\]" b.out || fail "B: the test's output" b.out -echo "ok: the description runs" - -# C -- `unit/math` is contained in `unit/math2` too -grep -q '"unit/math"' a.out || fail "C: the exact name was not chosen" a.out -echo "ok: the exact name is chosen" - -# D -set +e -"$MCPP" test --print-launch > d1.out 2> d1.err; rc1=$? -"$MCPP" test unit/ --print-launch > d2.out 2> d2.err; rc2=$? -"$MCPP" test unit/math --print-launch --message-format json > d3.out 2> d3.err; rc3=$? -"$MCPP" test unit/math --print-launch --no-run > d4.out 2> d4.err; rc4=$? -set -e -[ "$rc1" = 2 ] && grep -q "names no test" d1.err || fail "D: no pattern ($rc1)" d1.err -[ "$rc2" = 2 ] && grep -q "describes one test" d2.err || fail "D: several tests ($rc2)" d2.err -[ "$rc3" = 2 ] && grep -q "message-format json" d3.err || fail "D: json ($rc3)" d3.err -[ "$rc4" = 2 ] && grep -q -- "--no-run" d4.err || fail "D: --no-run ($rc4)" d4.err -echo "ok: the refusals" - -# E -"$MCPP" --protocol-version > e.out 2>&1 || fail "E: --protocol-version" e.out -grep -q '"test-launch"' e.out || fail "E: test-launch is not stated" e.out -echo "ok: the feature is stated" - -# A named runner is the same executable transformation for a test and a run. -# Python is a real host runner here, so the generated argv can also be executed. -$PY - <<'PY' -import json,sys -from pathlib import Path -Path('runner.py').write_text('import json,subprocess,sys\nwith open("named-invocations.txt","a") as f: f.write(json.dumps(sys.argv[1:])+"\\n")\nprint("NAMED RUNNER", flush=True)\nsys.exit(subprocess.call(sys.argv[1:]))\n') -with Path('mcpp.toml').open('a') as f: - os_name={'linux':'linux','darwin':'macos','win32':'windows'}[sys.platform] - f.write('\n[target.\'cfg(os = "'+os_name+'")\'.runners]\ndebuggee = '+json.dumps([sys.executable,str(Path('runner.py').resolve()),'{}'])+'\n') -PY -"$MCPP" test unit/math --runner debuggee --print-launch -- 'with spaces' > named.out 2> named.err \ - || fail "named test launch" named.err -$PY - named.out > named-run.out 2> named-run.err <<'PY' || fail "named launch execution" named-run.out named-run.err -import json,sys,os,subprocess -d=json.load(open(sys.argv[1]))['data'] -assert d['via']=='runner', d -assert d['configuration']['runners']==['debuggee'], d -assert d['argv'][-1]=='with spaces', d -assert d['argv'][1].endswith('runner.py'), d -sys.exit(subprocess.call(d['argv'],cwd=d['cwd'],env={**os.environ,**d['env']})) -PY -grep -q 'NAMED RUNNER' named-run.out || fail "named runner did not run" named-run.out -grep -q 'last \[with spaces\]' named-run.out || fail "named runner changed arguments" named-run.out -rm named-invocations.txt -"$MCPP" test unit/math2 --runner debuggee -- direct > named-direct.out 2> named-direct.err \ - || fail "named runner direct test" named-direct.out named-direct.err -$PY - <<'PY' || fail "direct test skipped named runner" named-direct.out -import json -rows=[json.loads(line) for line in open('named-invocations.txt')] -assert len(rows)==1 and 'math2' in rows[0][0] and rows[0][-1]=='direct', rows -PY -"$MCPP" test unit/math2 --no-run --archive named-tests.tar.gz > named-archive-build.out 2> named-archive-build.err \ - || fail "test archive build" named-archive-build.out named-archive-build.err -rm named-invocations.txt -"$MCPP" test --archive named-tests.tar.gz --runner debuggee -- archived > named-archive.out 2> named-archive.err \ - || fail "named archive runner" named-archive.out named-archive.err -$PY - <<'PY' || fail "archive skipped named runner" named-archive.out -import json -rows=[json.loads(line) for line in open('named-invocations.txt')] -assert len(rows)==1 and 'math2' in rows[0][0] and rows[0][-1]=='archived', rows -PY -set +e -"$MCPP" test unit/math --runner absent --print-launch > named-missing.out 2> named-missing.err; rc1=$? -"$MCPP" test unit/math --runner debuggee --no-runner > named-conflict.out 2> named-conflict.err; rc2=$? -set -e -[ "$rc1" = 2 ] && grep -q "no runner named 'absent'" named-missing.err || fail "missing test runner ($rc1)" named-missing.err -[ "$rc2" = 2 ] && grep -q -- '--runner and --no-runner' named-conflict.err || fail "contradictory test runners ($rc2)" named-conflict.err -echo "ok: named test launch, execution, archive, argument preservation and refusal" - -echo "OK" diff --git a/tests/e2e/MIGRATION.tsv b/tests/e2e/MIGRATION.tsv index 274e85c1f..14396c66c 100644 --- a/tests/e2e/MIGRATION.tsv +++ b/tests/e2e/MIGRATION.tsv @@ -14,10 +14,10 @@ script subsystem class destination note 12_add_command.sh deps merge e2e/deps 13_toolchain_pin.sh toolchain merge e2e/toolchain 14_toolchain_fallback.sh toolchain merge e2e/toolchain -15_test_passing.sh test merge e2e/test -16_test_failing.sh test merge e2e/test -17_test_no_tests.sh test merge e2e/test -18_devdeps_isolation.sh test merge e2e/test +15_test_passing.sh test merge e2e/test::report/15 test passing migrated (batch 1); script removed +16_test_failing.sh test merge e2e/test::report/16 test failing migrated (batch 1); script removed +17_test_no_tests.sh test merge e2e/test::report/17 test no tests migrated (batch 1); script removed +18_devdeps_isolation.sh test merge e2e/test::graph/18 devdeps isolation migrated (batch 1); script removed 19_bmi_cache_reuse.sh modules merge e2e/modules 20_p1689_scanner.sh modules merge e2e/modules 21_ninja_dyndep.sh modules merge e2e/modules @@ -77,14 +77,14 @@ script subsystem class destination note 69_package_templates.sh cli merge e2e/cli 70_per_target_flags.sh build merge e2e/build 71_required_features_gate.sh features merge e2e/features -72_test_profile_features.sh test merge e2e/test +72_test_profile_features.sh test merge e2e/test::graph/72 test profile features migrated (batch 1); script removed 73_issue131_per_target_cxxflag.sh build merge e2e/build 74_run_no_loader_env_leak.sh run merge e2e/run 75_index_status_offline.sh index merge e2e/index 76_compile_commands_generated.sh cdb merge e2e/cdb 76_qualified_custom_ns_noncanonical_filename.sh index merge e2e/index 77_cdb_preserves_test_entries.sh cdb merge e2e/cdb -78_test_main_combinations.sh test merge e2e/test +78_test_main_combinations.sh test merge e2e/test::graph/78 test main combinations migrated (batch 1); script removed 79_gtest_regular_dep_feature_main.sh deps merge e2e/deps 80_feature_defines.sh features merge e2e/features 81_capability_binding.sh features merge e2e/features @@ -97,11 +97,11 @@ script subsystem class destination note 87_build_default_profile.sh build merge e2e/build 88_xlings_environment.sh self merge e2e/self 89_build_mcpp.sh build-program merge e2e/build-program -90_workspace_test.sh test merge e2e/test +90_workspace_test.sh test merge e2e/test::workspace/90 workspace test migrated (batch 1); script removed 91_target_bare_alias.sh target merge e2e/target 92_build_mcpp_import.sh build-program merge e2e/build-program 93_xpkg_parse.sh index sink unit/test_xpkg_parse answers a parse/selection rule without a build: descriptor grammar, strict keys -94_macos_test_stdlib.sh test merge e2e/test +94_macos_test_stdlib.sh test merge e2e/test::frameworks/94 macos test stdlib migrated (batch 1); script removed; skips without macos, as the script was excluded 95_msvc_system_toolchain.sh windows merge e2e/windows 96_msvc_unavailable_nonwindows.sh windows merge e2e/windows 97_mingw_toolchain.sh windows merge e2e/windows @@ -158,19 +158,19 @@ script subsystem class destination note 149_conditional_glob_flags.sh features merge e2e/features 150_clang_module_operator_template.sh toolchain keep keep a state canary of the bundled Clang (expected crash), not an assertion about mcpp 151_index_url_artifact_migration.sh self merge e2e/self -152_test_subdir_names.sh test merge e2e/test -153_test_isolation.sh test merge e2e/test -154_test_filter.sh test merge e2e/test -155_test_json.sh test merge e2e/test +152_test_subdir_names.sh test merge e2e/test::selection/152 test subdir names migrated (batch 1); script removed +153_test_isolation.sh test merge e2e/test::report/153 test isolation migrated (batch 1); script removed +154_test_filter.sh test merge e2e/test::selection/154 test filter migrated (batch 1); script removed +155_test_json.sh test merge e2e/test::report/155 test json migrated (batch 1); script removed 156_nested_ld_library_path.sh run merge e2e/run -157_test_glob_flags.sh test merge e2e/test -158_test_signal_and_duration.sh test merge e2e/test -159_test_list.sh test merge e2e/test -160_test_timeout.sh test merge e2e/test +157_test_glob_flags.sh test merge e2e/test::graph/157 test glob flags migrated (batch 1); script removed +158_test_signal_and_duration.sh test merge e2e/test::report/158 test signal and duration migrated (batch 1); script removed +159_test_list.sh test merge e2e/test::selection/159 test list migrated (batch 1); script removed +160_test_timeout.sh test merge e2e/test::report/160 test timeout migrated (batch 1); script removed 161_xpkg_name_form.sh deps merge e2e/deps 162_bare_name_namespace_scope.sh deps merge e2e/deps 163_identity_first_resolution.sh deps merge e2e/deps -164_ninja_goal_command_length.sh test merge e2e/test +164_ninja_goal_command_length.sh test merge e2e/test::graph/164 ninja goal command length migrated (batch 1); script removed 165_bare_name_cross_namespace_wire_address.sh deps merge e2e/deps 166_run_env_no_private_glibc.sh run merge e2e/run 167_build_defines_module_scan.sh modules merge e2e/modules @@ -185,7 +185,7 @@ script subsystem class destination note 175_cpp20_import_std.sh toolchain merge e2e/toolchain 176_cpp20_std_bmi_isolation.sh toolchain merge e2e/toolchain 177_cpp20_msvc.sh windows merge e2e/windows -178_test_observability.sh test merge e2e/test +178_test_observability.sh test merge e2e/test::streams/178 test observability migrated (batch 1); script removed 179_spaced_paths.sh build merge e2e/build 180_msvc_build_mcpp.sh windows merge e2e/windows 181_build_mcpp_import_std.sh build-program merge e2e/build-program @@ -221,7 +221,7 @@ script subsystem class destination note 210_local_index_addressing_on_every_host.sh index merge e2e/index 211_configure_only_cdb.sh cdb merge e2e/cdb 212_cached_dep_std_is_ordered.sh cache merge e2e/cache -213_build_after_test_is_not_the_test_graph.sh test merge e2e/test +213_build_after_test_is_not_the_test_graph.sh test merge e2e/test::graph/213 build after test is not the test graph migrated (batch 1); script removed 214_executable_carries_dt_rpath.sh runtime merge e2e/runtime 215_pack_has_no_build_machine_paths.sh pack merge e2e/pack 216_selfcontained_refuses_host_capability.sh pack merge e2e/pack @@ -402,7 +402,7 @@ script subsystem class destination note 670_an_rpath_loader_token_is_left_as_written.sh runtime merge e2e/runtime 671_toolchain_is_an_option_of_run_test_and_pack.sh cli merge e2e/cli 672_a_runner_receives_the_files_the_artifact_carries.sh run merge e2e/run -673_test_discover_names_where_tests_are.sh test merge e2e/test +673_test_discover_names_where_tests_are.sh test merge e2e/test::selection/673 test discover names where tests are migrated (batch 1); script removed 674_a_format_is_run_by_the_runner_named_after_it.sh run merge e2e/run 675_android_test_programs_carry_their_cxx_runtime.sh android merge e2e/android 677_a_conditional_dependency_replaces_the_unconditional_one.sh features merge e2e/features @@ -453,7 +453,7 @@ script subsystem class destination note 742_gcc_accepted_when_the_realisation_is_empty.sh toolchain merge e2e/toolchain 743_kernel_abi_interfaces_are_resolved_not_preprocessed.sh openkal merge e2e/openkal 744_a_declared_absence_explains_the_link_that_asked_for_it.sh target merge e2e/target -745_no_run_builds_the_tests_and_says_so.sh test merge e2e/test +745_no_run_builds_the_tests_and_says_so.sh test merge e2e/test::archive/745 no run builds the tests and says so migrated (batch 1); script removed 746_macos_deployment_target_is_target_keyed_not_host_keyed.sh macos merge e2e/macos 760_msvc_toolset_is_chosen_once.sh windows merge e2e/windows 765_a_consumer_include_directory_stays_in_the_consumer.sh build merge e2e/build @@ -539,9 +539,9 @@ script subsystem class destination note 851_a_shared_member_is_compiled_once_per_configuration.sh workspace merge e2e/workspace 852_a_repeated_dash_p_selects_every_member_it_names.sh workspace merge e2e/workspace 853_run_keeps_one_member_and_exclude_removes_members.sh workspace merge e2e/workspace -854_a_test_over_several_members_plans_and_builds_once.sh test merge e2e/test -855_a_shared_test_build_is_reported_once_per_group.sh test merge e2e/test -856_a_test_over_several_members_continues_past_a_failing_one.sh test merge e2e/test +854_a_test_over_several_members_plans_and_builds_once.sh test merge e2e/test::workspace/854 a test over several members plans and builds once migrated (batch 1); script removed +855_a_shared_test_build_is_reported_once_per_group.sh test merge e2e/test::workspace/855 a shared test build is reported once per group migrated (batch 1); script removed +856_a_test_over_several_members_continues_past_a_failing_one.sh test merge e2e/test::workspace/856 a test over several members continues past a failing one migrated (batch 1); script removed 857_a_host_module_several_members_import_is_compiled_once.sh modules merge e2e/modules 858_the_engines_module_is_compiled_once_per_home.sh modules merge e2e/modules 859_a_host_module_entry_is_reused_while_its_inputs_agree.sh build-program merge e2e/build-program @@ -550,7 +550,7 @@ script subsystem class destination note 862_run_q_writes_exactly_the_programs_stdout.sh run merge e2e/run 863_a_run_whose_build_failed_exits_101.sh run merge e2e/run 864_a_builds_status_is_on_standard_error.sh cli merge e2e/cli -865_mcpp_test_reports_its_steps_on_stderr_and_its_results_on_stdout.sh test merge e2e/test +865_mcpp_test_reports_its_steps_on_stderr_and_its_results_on_stdout.sh test merge e2e/test::streams/865 mcpp test reports its steps on stderr and its results on stdout migrated (batch 1); script removed 866_both_streams_on_one_pipe_keep_their_order_and_json_stays_clean.sh cli merge e2e/cli 867_a_pack_over_several_members_plans_builds_and_runs_programs_once.sh pack merge e2e/pack 868_a_pack_over_several_members_is_refused_before_anything_is_compiled.sh pack merge e2e/pack @@ -659,11 +659,11 @@ script subsystem class destination note 986_single_files_cover_the_legs_and_systems.sh openkal merge e2e/openkal 987_the_universal_spellings_are_migrated.sh openkal merge e2e/openkal 988_the_windows_launcher_finds_its_package_from_a_windows_path.sh openkal keep keep emulator: runs the Windows launcher under wine -989_a_native_legs_tests_run_from_an_archive.sh test merge e2e/test +989_a_native_legs_tests_run_from_an_archive.sh test merge e2e/test::archive/989 a native legs tests run from an archive migrated (batch 1); script removed 990_a_lock_key_does_not_depend_on_the_selected_members.sh deps merge e2e/deps 991_a_prebuilt_archive_in_ldflags_is_a_link_input.sh build merge e2e/build 992_a_polyglot_file_runs_on_every_system_it_holds.sh openkal merge e2e/openkal -993_a_test_describes_its_launch.sh test merge e2e/test +993_a_test_describes_its_launch.sh test merge e2e/test::launch/993 a test describes its launch ; e2e/test::launch/993 a test describes its launch (a named runner) migrated (batch 1); script removed 994_the_contract_names_the_target_side.sh openkal merge e2e/openkal 995_the_windows_launcher_builds_over_any_platform_release.sh openkal merge e2e/openkal 996_an_ok_image_waits_for_a_prepare_action.sh openkal merge e2e/openkal @@ -696,7 +696,7 @@ script subsystem class destination note 1023_msvc_objects_carry_their_own_debug_information.sh windows merge e2e/windows 1024_a_launch_description_takes_the_fast_path.sh run merge e2e/run 1025_the_default_platform_is_one_release.sh openkal merge e2e/openkal -1026_an_archived_member_test_carries_its_members_files.sh test merge e2e/test +1026_an_archived_member_test_carries_its_members_files.sh test merge e2e/test::archive/1026 an archived member test carries its members files migrated (batch 1); script removed 1027_a_configuration_group_keeps_the_other_groups_lock_entries.sh deps merge e2e/deps 1028_a_projects_state_directory_ignores_itself.sh cache merge e2e/cache 1029_an_unused_std_module_is_collected_by_age.sh cache merge e2e/cache @@ -704,11 +704,11 @@ script subsystem class destination note 1031_target_keeps_within_its_budget.sh cache merge e2e/cache 1032_a_quoted_dotted_dependency_key_is_deprecated.sh deps merge e2e/deps 1033_an_ok_image_runs_on_the_system_that_built_it.sh openkal merge e2e/openkal -1034_a_workspace_members_test_finds_its_programs.sh test merge e2e/test -1035_a_test_program_reports_its_cases.sh test merge e2e/test +1034_a_workspace_members_test_finds_its_programs.sh test merge e2e/test::workspace/1034 a workspace members test finds its programs migrated (batch 1); script removed +1035_a_test_program_reports_its_cases.sh test merge e2e/test::protocol/1035 a test program reports its cases migrated (batch 1); script removed 1036_a_context_states_the_engine_values_in_process.sh build-program merge e2e/build-program -1037_a_skeleton_tests_with_mtest_and_a_test_needs_no_main.sh test merge e2e/test -1038_test_programs_of_a_directory_and_the_tests_a_change_affects.sh test merge e2e/test +1037_a_skeleton_tests_with_mtest_and_a_test_needs_no_main.sh test merge e2e/test::skeleton/1037 a skeleton tests with mtest and a test needs no main migrated (batch 1); script removed +1038_test_programs_of_a_directory_and_the_tests_a_change_affects.sh test merge e2e/test::affected/1038 test programs of a directory and the tests a change affects migrated (batch 1); script removed 1039_two_builds_that_find_the_std_module_missing_compile_it_once.sh modules merge e2e/modules 1040_a_platform_defines_a_target.sh openkal merge e2e/openkal 1041_a_32_bit_x86_bare_target.sh baremetal merge e2e/baremetal @@ -720,9 +720,9 @@ script subsystem class destination note 1047_an_artifact_compiles_its_shared_sources_with_its_own_flags.sh build merge e2e/build 1048_a_git_reference_follows_the_default_branch.sh deps merge e2e/deps 1049_a_platform_skeleton_is_made_and_its_statement_read_as_the_engine_reads_it.sh openkal merge e2e/openkal -1050_a_test_run_is_kept_in_the_mtest_store.sh test merge e2e/test +1050_a_test_run_is_kept_in_the_mtest_store.sh test merge e2e/test::archive/1050 a test run is kept in the mtest store migrated (batch 1); script removed 1051_a_cross_targets_toolchain_section_is_not_the_build_programs.sh build-program merge e2e/build-program -1052_a_framework_that_uses_the_package_under_test_is_built_against_it.sh test merge e2e/test +1052_a_framework_that_uses_the_package_under_test_is_built_against_it.sh test merge e2e/test::frameworks/1052 a framework that uses the package under test is built against it migrated (batch 1); script removed 1053_a_mach_o_images_c_library_is_up_before_its_initialisers.sh macos merge e2e/macos 1054_programs_that_share_an_entry_keep_their_own_flags.sh build merge e2e/build 1055_layer_suppliers_that_require_one_another_are_refused.sh target merge e2e/target @@ -735,12 +735,12 @@ script subsystem class destination note 1062_link_flags_on_a_compiler_link_are_named_not_dropped.sh build merge e2e/build 1063_a_mingw_base_package_serves_windows_gnu_beside_llvm.sh windows keep keep emulator: runs the mingw base-package program under wine 1064_a_scan_override_of_a_file_this_build_excludes_is_kept.sh modules merge e2e/modules -1065_test_records_carry_parts_live_cases_and_metrics.sh test merge e2e/test +1065_test_records_carry_parts_live_cases_and_metrics.sh test merge e2e/test::protocol/1065 test records carry parts live cases and metrics migrated (batch 1); script removed 1066_a_merged_version_withdraws_its_requests.sh deps merge e2e/deps 1067_a_borrowed_registry_keeps_its_owners_places.sh index merge e2e/index -1068_a_running_test_holds_no_build_directory.sh test merge e2e/test -1069_an_unchanged_project_tests_without_planning.sh test merge e2e/test -1070_the_test_spec_launches_frameworks_checks_and_output.sh test merge e2e/test +1068_a_running_test_holds_no_build_directory.sh test merge e2e/test::fast/1068 a running test holds no build directory migrated (batch 1); script removed +1069_an_unchanged_project_tests_without_planning.sh test merge e2e/test::fast/1069 an unchanged project tests without planning migrated (batch 1); script removed +1070_the_test_spec_launches_frameworks_checks_and_output.sh test merge e2e/test::protocol/1070 the test spec launches frameworks checks and output migrated (batch 1); script removed 1071_the_root_overrides_a_dependency_for_the_whole_graph.sh deps merge e2e/deps 1072_a_resource_include_nothing_provides_is_the_scripts_own.sh windows merge e2e/windows 1073_the_freebsd_musl_identity_is_served_by_openkal.sh openkal merge e2e/openkal diff --git a/tests/e2e/test/affected.cpp b/tests/e2e/test/affected.cpp new file mode 100644 index 000000000..0179985f2 --- /dev/null +++ b/tests/e2e/test/affected.cpp @@ -0,0 +1,142 @@ +// Test programs of a directory, and the tests a change affects (formerly tests/e2e/1038). +import std; +import mcpplibs.mtest; +import mcpp.test.support; + +using namespace mcpplibs::mtest; +namespace s = mcpp::test::support; +namespace fs = std::filesystem; +using nlohmann::json; + +namespace { + +E2E affected{"affected", [] { + + "1038 test programs of a directory and the tests a change affects"_test[tag{"requires:gcc"}] = [] { + s::needs({"gcc"}); + auto p = s::project::scaffold("app"); + p.write("src/calc.cppm", "export module app.calc;\nexport int twice(int x) { return 2 * x; }\n"); + p.write("tests/unit/a.cpp", R"(import mcpplibs.mtest; +import app.calc; +using namespace mcpplibs::mtest; +UTest a{"doubles", [] { expect(eq(twice(2), 4)); }}; +)"); + p.write("tests/unit/b.cpp", R"(import std; +import mcpplibs.mtest; +using namespace mcpplibs::mtest; +UTest b{"reads its data", [] { expect(eq(std::string_view("tests/data/f.txt").size(), 16u)); }}; +)"); + p.write("tests/e2e/plain.cpp", "import std;\nint main() { return 0; }\n"); + p.write("tests/data/f.txt", "data\n"); + // A + p.write("mcpp.toml", p.read("mcpp.toml") + "\n[test]\nprograms = \"directory\"\n"); + auto a = p.mcpp({"test"}); + expect(fatal(e2e::succeeded(a))); + // A program that speaks the protocol runs as a process per file of its cases. + expect(s::has_line(s::text(a), R"(unit/a \.\.\. ok \(.*1 case\))")); + expect(s::has_line(s::text(a), R"(unit/b \.\.\. ok \(.*1 case\))")); + auto ap = p.mcpp({"test", "unit/b"}); + expect(e2e::succeeded(ap)); + expect(e2e::contains(s::text(ap), "unit/b ... ok")); + expect(s::lacks(s::text(ap), "unit/a ...")) << "the pattern of a part did not run that part alone"; + p.write("mcpp.toml", p.read("mcpp.toml") + "split = \"none\"\n"); + auto a1 = p.mcpp({"test"}); + expect(fatal(e2e::succeeded(a1))); + expect(s::has_line(s::text(a1), R"(unit \.\.\. ok \(.*2 cases\))")) << "unit is not one program of two cases"; + expect(s::has_line(s::text(a1), R"(^e2e \.\.\. ok)")) << "the directory of a file with main"; + auto ac = p.mcpp({"test", "--cases"}); + expect(fatal(e2e::succeeded(ac))); + expect(e2e::contains(s::text(ac), "doubles")); + expect(e2e::contains(s::text(ac), "reads its data")); + // B + expect(e2e::contains(s::text(ac), "e2e: one case, its exit status")) << "a program of plain main is listed as one case"; + p.write("tests/e2e/second.cpp", "int main() { return 0; }\n"); + auto a2 = p.mcpp({"test"}); + expect(a2.started && a2.status != 0) << "two files of one directory that define main were taken"; + expect(e2e::contains(s::text(a2), "plain.cpp")); + expect(e2e::contains(s::text(a2), "second.cpp")); + p.remove("tests/e2e/second.cpp"); + // C + auto changed = [&](std::initializer_list paths) { + std::vector args{"test"}; + for (auto x : paths) { args.emplace_back("--changed"); args.emplace_back(x); } + auto r = p.mcpp(args); + expect(e2e::succeeded(r)); + return s::text(r); + }; + auto c1 = changed({"src/calc.cppm"}); + expect(e2e::contains(c1, "Affected 1 of 3 tests")); + expect(e2e::contains(c1, "unit ... ok")); + expect(s::lacks(c1, "test_smoke ...")) << "a test not built from the change ran"; + expect(e2e::contains(changed({"README.md"}), "Affected 0 of 3 tests")); + auto c3 = changed({"tests/data/f.txt"}); + expect(e2e::contains(c3, "Affected 1 of 3 tests")); + expect(e2e::contains(c3, "unit ... ok")); + expect(e2e::contains(changed({"mcpp.toml"}), "Affected 3 of 3 tests")); + auto git = [&](std::vector args) { + args.insert(args.begin(), "git"); + return e2e::run({.argv = std::move(args), .cwd = p.dir()}); + }; + expect(fatal(e2e::succeeded(git({"init", "-q", "."})))); + expect(fatal(e2e::succeeded(git({"add", "-A"})))); + expect(fatal(e2e::succeeded(git({"-c", "user.name=t", "-c", "user.email=t@t", "commit", "-qm", "base"})))); + p.write("src/calc.cppm", p.read("src/calc.cppm") + "\nexport int thrice(int x) { return 3 * x; }\n"); + auto c5 = p.mcpp({"test", "--since", "HEAD"}); + expect(e2e::succeeded(c5)); + expect(e2e::contains(s::text(c5), "Affected 1 of 3 tests")) << "--since reads the working tree's change"; + // D: back to a program per file, and to the directories. + auto manifest = p.read("mcpp.toml"); + p.write("mcpp.toml", manifest.substr(0, manifest.find("[test]"))); + auto d = p.mcpp({"test"}); + expect(e2e::succeeded(d)); + expect(e2e::contains(s::text(d), "unit/a ... ok")); + expect(e2e::contains(s::text(d), "e2e/plain ... ok")); + p.write("mcpp.toml", p.read("mcpp.toml") + "\n[test]\nprograms = \"directory\"\n"); + expect(e2e::succeeded(p.mcpp({"test"}))); + // E: a shipping profile. + auto rel = s::project::scaffold("rel"); + rel.write("src/where.cppm", R"(export module rel.where; +import std; +export std::string library_file() { return std::source_location::current().file_name(); } +)"); + rel.write("tests/where.cpp", R"(import std; +import rel.where; +int main() { + const std::string here = std::source_location::current().file_name(); + std::println("test {}", here); + std::println("library {}", library_file()); + return std::filesystem::exists(std::filesystem::path(here).parent_path() / "test_smoke.cpp") ? 0 : 1; +} +)"); + expect(fatal(e2e::succeeded(rel.mcpp({"build", "--release"})))); + auto et = rel.mcpp({"test", "--profile", "release", "where"}); + expect(e2e::succeeded(et)) << "the release test (its file did not find its directory)"; + const auto rg = s::glob(rel.at("target"), "*/*/build.ninja"); + expect(fatal(eq(rg.size(), 1u))) << "build and test are several configurations"; + expect(e2e::contains(s::text(et), "library rel@0.1.0/src/where.cppm")) << "the code under test is not trimmed as built"; + const auto where_edges = std::ranges::count_if(s::ninja_log_outputs(rg.front().parent_path()), [](const std::string& o) { + return std::regex_search(o, std::regex(R"(obj/where\.m\.(o|obj)$)")); + }); + expect(eq(where_edges, 1)) << "the test compiled the library again"; + // F + auto grp = s::project::scaffold("grp"); + grp.write("tests/test_smoke.cpp", R"(import mcpplibs.mtest; +using namespace mcpplibs::mtest; +UTest g{"g", [] { + "one"_test = [] { expect(true); }; + "two"_test = [] { expect(true); }; +}}; +)"); + auto fc = grp.mcpp({"test", "--cases"}); + expect(fatal(e2e::succeeded(fc))); + expect(e2e::contains(s::text(fc), "test_smoke: 1 case")) << "the object is the one listed case"; + auto f1 = grp.mcpp({"test", "--case", "g/one"}); + expect(e2e::succeeded(f1)); + expect(s::has_line(s::text(f1), R"(test_smoke \.\.\. ok \(.*1 case\))")) << "a declared case is not selected alone"; + auto f2 = grp.mcpp({"test", "--case", "g/t*"}); + expect(e2e::succeeded(f2)); + expect(s::has_line(s::text(f2), R"(test_smoke \.\.\. ok \(.*3 cases\))")) << "a pattern below the listed case does not run it whole"; + }; +}}; + +} // namespace diff --git a/tests/e2e/test/archive.cpp b/tests/e2e/test/archive.cpp new file mode 100644 index 000000000..70efebaba --- /dev/null +++ b/tests/e2e/test/archive.cpp @@ -0,0 +1,202 @@ +// Tests built and not run, an archive of them run elsewhere, and the run mcpp keeps (formerly +// tests/e2e/745, 989, 1026 and 1050). +import std; +import mcpplibs.mtest; +import mcpp.test.support; + +using namespace mcpplibs::mtest; +namespace s = mcpp::test::support; +namespace fs = std::filesystem; + +namespace { + +std::string trim(std::string v) { + while (!v.empty() && (v.back() == '\n' || v.back() == '\r' || v.back() == ' ')) v.pop_back(); + return v; +} + +// Every line of a file that is a JSON object. +std::vector records(const fs::path& file) { return s::json_lines(s::read_file(file)); } + +E2E archive{"archive", [] { + "745 no run builds the tests and says so"_test[tag{"requires:gcc"}] = [] { + // `--no-run` builds the tests and reports that, distinguishably from a run that could not + // run them. The runner is a name that is not a program: tests built, nothing run, on every + // host, for the native target. The package's only sources are its tests. + s::needs({"gcc"}); + s::project p{{"mcpp.toml", R"([package] +name = "norun" +version = "0.1.0" + +# A name, not a path, and not a program: the runner lookup fails, which is the +# situation this test is about. +[target.HOST_TRIPLE] +runner = ["mcpp-no-such-runner-exists"] +)"}, + {"tests/alpha.cpp", "int main() { return 0; }\n"}, + {"tests/beta.cpp", "int main() { return 0; }\n"}}; + auto host = trim(p.mcpp({"--print-target"}).out); + if (host.empty()) { + // `--print-target` postdates some clients: the triple of a build directory. + p.mcpp({"build"}); + for (const auto& e : fs::directory_iterator(p.at("target"))) + if (!e.path().filename().string().starts_with(".")) { host = e.path().filename().string(); break; } + } + expect(fatal(!host.empty())) << "could not determine the host triple"; + p.write("mcpp.toml", std::regex_replace(p.read("mcpp.toml"), std::regex("HOST_TRIPLE"), host)); + + // A: it tried to run them and could not. + auto a = p.mcpp({"test", "--target", host}); + expect(e2e::exited(a, 2)) << "a runner that does not exist"; + expect(e2e::contains(s::text(a), "not run")); + // B: it was asked not to. + auto b = p.mcpp({"test", "--target", host, "--no-run"}); + expect(e2e::exited(b, 0)); + expect(e2e::contains(s::text(b), "2 built, not run")); + expect(s::lacks(s::text(b), "2 not run")) << "B reported the tests as not run as well as built"; + // C: --no-run does not make a broken test pass; it names it and still reports the others. + p.write("tests/gamma.cpp", "int main() { this_function_does_not_exist(); }\n"); + auto c = p.mcpp({"test", "--target", host, "--no-run"}); + expect(c.started && c.status != 0) << "a test that does not compile was reported as built"; + expect(e2e::contains(s::text(c), "gamma")); + expect(e2e::contains(s::text(c), "2 built, not run")); + // D: --no-run with --no-runner is refused, and says why. + auto d = p.mcpp({"test", "--target", host, "--no-run", "--no-runner"}); + expect(d.started && d.status != 0) << "--no-run with --no-runner was accepted"; + expect(e2e::contains(s::text(d), "cannot be combined")); + // E: a workspace's total says it too. + p.remove("tests/gamma.cpp"); + p.write("mcpp.toml", "[workspace]\nmembers = [\"members/one\", \"members/two\"]\n"); + for (auto m : {"one", "two"}) { + p.write(std::format("members/{}/mcpp.toml", m), + std::format("[package]\nname = \"{}\"\nversion = \"0.1.0\"\n\n[target.{}]\nrunner = [\"mcpp-no-such-runner-exists\"]\n", m, host)); + p.write(std::format("members/{}/tests/t.cpp", m), "int main() { return 0; }\n"); + } + auto e = p.mcpp({"test", "--workspace", "--target", host, "--no-run"}); + expect(e2e::exited(e, 0)); + expect(e2e::contains(s::text(e), "2 built, not run")) << "the workspace total did not report the built tests"; + }; + + "989 a native legs tests run from an archive"_test = [] { + // A leg of the host's own system, archived, runs from the archive in a checkout with + // nothing built (macOS once refused such a program: "invalid argument"). + const auto archive = temp_dir() / "t.tar.gz"; + s::project app(temp_dir() / "app", {{"mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n"}, + {"src/main.cpp", "import std;\nint main() { std::println(\"app\"); }\n"}, + {"tests/smoke.cpp", "import std;\nint main() { std::vector v{2, 1}; std::ranges::sort(v); return v.front() == 1 ? 0 : 1; }\n"}}); + // A + expect(fatal(e2e::succeeded(app.mcpp({"test", "--no-run", "--archive", archive.string()})))); + expect(fatal(fs::is_regular_file(archive) && fs::file_size(archive) > 0)) << "no archive written"; + // B: a checkout without target/. + s::project co(temp_dir() / "co", {}); + for (auto f : {"src", "tests", "mcpp.toml"}) + fs::copy(app.at(f), co.at(f), fs::copy_options::recursive); + auto b = co.mcpp({"test", "--archive", archive.string()}); + expect(e2e::succeeded(b)) << "the host leg's archived test did not pass"; + expect(e2e::contains(s::text(b), "smoke ... ok")); + // Nothing named smoke* outside the unpacked archive: the tests were not built again. + std::vector rebuilt; + if (fs::exists(co.at("target"))) + for (auto it = fs::recursive_directory_iterator(co.at("target")); it != fs::recursive_directory_iterator(); ++it) { + if (it->path().filename() == ".test-archive") { it.disable_recursion_pending(); continue; } + if (it->path().filename().string().starts_with("smoke")) rebuilt.push_back(it->path()); + } + expect(rebuilt.empty()) << "the archive's tests were built again: " << (rebuilt.empty() ? "" : rebuilt.front().string()); + }; + + "1026 an archived member test carries its members files"_test = [] { + // A member's archived test carries the files its `[runtime] deploy` names, from the member's + // own product directory, and finds them beside it in a checkout without target/. + const auto archive = temp_dir() / "a.tar.gz"; + s::project ws(temp_dir() / "ws", {{"mcpp.toml", "[workspace]\nmembers = [\"app\", \"tool\"]\n"}, + {"app/mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n"}, + {"app/src/main.cpp", "int main() { return 0; }\n"}, + {"tool/mcpp.toml", R"([package] +name = "tool" +version = "0.1.0" + +[runtime] +deploy = [ { from = "notices/LICENSE", to = "licenses/tool" } ] +)"}, + {"tool/notices/LICENSE", "tool license\n"}, + {"tool/src/main.cpp", "int main() { return 0; }\n"}, + {"tool/tests/notice.cpp", R"(import std; +int main(int, char** argv) { + auto here = std::filesystem::absolute(argv[0]).parent_path(); + std::ifstream in(here / "licenses" / "tool" / "LICENSE"); + std::string line; + std::getline(in, line); + return line == "tool license" ? 0 : 1; +} +)"}}); + // A + expect(fatal(e2e::succeeded(ws.mcpp({"test", "-p", "tool", "--no-run", "--archive", archive.string()})))); + const auto x = temp_dir() / "x"; + fs::create_directories(x); + expect(fatal(e2e::succeeded(e2e::run({.argv = {"tar", "-xzf", archive.string(), "-C", x.string()}})))); + const auto manifests = s::glob(x, "*/mcpp-tests.json"); + expect(fatal(!manifests.empty())) << "no mcpp-tests.json in the archive"; + const auto d = s::json_file(manifests.front()); + nlohmann::json test; + for (const auto& leg : d["legs"]) + for (const auto& t : leg["tests"]) + if (t["name"] == "notice") test = t; + expect(fatal(!test.is_null())) << d.dump(); + std::string file; + for (const auto& f : test["files"]) + if (f.get().ends_with("licenses/tool/LICENSE")) { file = f; break; } + expect(fatal(!file.empty())) << "the archive does not carry the member's file: " << test.dump(); + expect(eq(s::read_file(manifests.front().parent_path() / file), std::string("tool license\n"))); + // B + s::project co(temp_dir() / "co", {}); + for (auto f : {"mcpp.toml", "app", "tool"}) + fs::copy(ws.at(f), co.at(f), fs::copy_options::recursive); + for (auto t : {"target", "app/target", "tool/target"}) co.remove(t); + auto b = co.mcpp({"test", "-p", "tool", "--archive", archive.string()}); + expect(e2e::succeeded(b)); + expect(e2e::contains(s::text(b), "notice ... ok")) << "the archived test did not find its file"; + }; + + "1050 a test run is kept in the mtest store"_test[tag{"requires:gcc"}] = [] { + // target/mtest/runs// (run.json, programs.ndjson, cases.ndjson) and a line in + // target/mtest/index.ndjson per run; `[test] store` names another directory or "none". + s::needs({"gcc"}); + auto p = s::project::scaffold("prog"); + expect(fatal(e2e::succeeded(p.mcpp({"test"})))); + const auto store = p.at("target/mtest"); + expect(fatal(fs::is_regular_file(store / "index.ndjson"))) << "no index"; + const auto runs = records(store / "index.ndjson"); + expect(fatal(eq(runs.size(), 1u))); + const auto& run = runs.front(); + expect(run["schema"] == "mtest.run/1" && run["status"] == "pass") << run.dump(); + expect(run["counts"].value("pass", 0) >= 1) << run.dump(); + const auto dir = store / "runs" / run["id"].get(); + expect(s::json_file(dir / "run.json")["id"] == run["id"]); + const auto programs = records(dir / "programs.ndjson"); + expect(!programs.empty()); + for (const auto& pr : programs) + expect(pr["status"] == "pass" && pr["ms"] >= 0 && pr["started_ms"] >= 0 && pr["worker"] >= 0) << pr.dump(); + const auto cases = records(dir / "cases.ndjson"); + expect(!cases.empty()); + for (const auto& c : cases) + expect(c["status"] == "pass" && !c["program"].is_null() && c["program"] != "") << c.dump(); + + auto lines = [&] { return s::count_lines(s::read_file(store / "index.ndjson"), "{"); }; + expect(fatal(e2e::succeeded(p.mcpp({"test"})))); + expect(eq(lines(), 2u)) << "a second run is not a second record"; + const auto archive = temp_dir() / "tests.tar.gz"; + expect(fatal(e2e::succeeded(p.mcpp({"test", "--no-run", "--archive", archive.string()})))); + auto a2 = p.mcpp({"test", "--archive", archive.string()}); + expect(fatal(e2e::succeeded(a2))); + expect(s::has_line(s::text(a2), R"(test_smoke \.\.\. ok \(.*1 case\))")) << "an archived protocol program does not report its case"; + expect(eq(lines(), 3u)) << "the archive's run is not stored"; + p.write("mcpp.toml", p.read("mcpp.toml") + "\n[test]\nstore = \"none\"\n"); + expect(fatal(e2e::succeeded(p.mcpp({"test"})))); + expect(eq(lines(), 3u)) << "store = none kept a run"; + p.write("mcpp.toml", std::regex_replace(p.read("mcpp.toml"), std::regex("store = \"none\""), "store = \"out/test-runs\"")); + expect(fatal(e2e::succeeded(p.mcpp({"test"})))); + expect(p.exists("out/test-runs/index.ndjson")) << "store = did not write there"; + }; +}}; + +} // namespace diff --git a/tests/e2e/test/fast.cpp b/tests/e2e/test/fast.cpp new file mode 100644 index 000000000..006c65d06 --- /dev/null +++ b/tests/e2e/test/fast.cpp @@ -0,0 +1,155 @@ +// A test run beside other commands, and the tests of an unchanged project (formerly tests/e2e/1068 +// and 1069). +import std; +import mcpplibs.mtest; +import mcpp.test.support; + +using namespace mcpplibs::mtest; +namespace s = mcpp::test::support; +namespace fs = std::filesystem; +using namespace std::chrono_literals; + +namespace { + +void touch(const fs::path& f) { std::ofstream(f) << ""; } + +E2E fast{"fast", [] { + "1068 a running test holds no build directory"_test = [] { + // `mcpp test` releases the build directory's lock once its tests are built (mcpp#915): a + // build and a second test of the same configuration proceed while a test runs. The first + // test waits for a file only the commands started beside it create. + auto p = s::project::scaffold("app"); + for (const auto& f : s::glob(p.at("tests"), "*.cpp")) fs::remove(f); + const auto gate = temp_dir() / "gate"; + p.write("tests/waits.cpp", std::format(R"(import std; +int main() {{ + for (int i = 0; i < 600; ++i) {{ + if (std::filesystem::exists("{}")) return 0; + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + }} + std::println("the gate never opened"); + return 1; +}} +)", + gate.generic_string())); + p.write("tests/quick.cpp", "int main() { return 0; }\n"); + + // Warm: the tests are built. + { + auto warm = e2e::session::start(s::mcpp_command({"test"}, p.dir())); + expect(fatal(warm.started())); + std::this_thread::sleep_for(1s); + touch(gate); + auto r = warm.finish(10min); + expect(fatal(e2e::succeeded(r))) << "the tests did not build\n" << warm.transcript(); + } + fs::remove(gate); + + auto first = e2e::session::start(s::mcpp_command({"test", "waits"}, p.dir())); + expect(fatal(first.started())); + // The first test is running once its program has started. + first.expect("Running|waits", 10s); + std::this_thread::sleep_for(1s); + auto open_and_end = [&] { touch(gate); first.finish(2min); }; + + // Beside it: a build of the same configuration, and a second test. + auto build = p.mcpp({"build"}, {.timeout = 60s}); + if (!build) { + open_and_end(); + expect(fatal(e2e::succeeded(build))) << "mcpp build waited for a running test"; + } + auto second = p.mcpp({"test", "quick"}, {.timeout = 60s}); + if (!second) { + open_and_end(); + expect(fatal(e2e::succeeded(second))) << "a second mcpp test waited for a running one"; + } + expect(s::lacks(s::text(build), "Waiting for the build directory")); + expect(s::lacks(s::text(second), "Waiting for the build directory")); + + touch(gate); + auto r = first.finish(2min); + expect(e2e::succeeded(r)) << "the first test failed\n" << first.transcript(); + expect(e2e::contains(first.transcript(), "1 passed; 0 failed")) << "the first test's summary"; + expect(e2e::contains(s::text(second), "1 passed; 0 failed")) << "the second test's summary"; + // The protocol files were the invocations', and went with them. + expect(s::glob(p.at("target"), "*/*/test-protocol/*").empty()) << "an ended invocation left its protocol directory"; + + // The issue's own shape: a test that builds its own project with mcpp. The inner build has + // the test's environment; it finds the directory free, the configuration built and its + // graph current, so it neither waits nor plans. + const auto inner_log = temp_dir() / "inner.log"; + p.write("tests/inner.cpp", std::format(R"(#include +#ifdef _WIN32 +#define WHOLE(s) "\"" s "\"" // cmd /c drops the outer pair of quotes +#else +#define WHOLE(s) s +#endif +int main() {{ return std::system(WHOLE("\"{}\" build > \"{}\" 2>&1")) == 0 ? 0 : 1; }} +)", + fs::path(s::mcpp_path()).generic_string(), inner_log.generic_string())); + auto prime = p.mcpp({"test", "inner"}); + expect(fatal(e2e::succeeded(prime))) << "a test that builds its project failed\n" << s::read_file(inner_log); + auto again = p.mcpp({"test", "inner"}, {.timeout = 120s}); + expect(fatal(e2e::succeeded(again))) << "a test that builds its project failed\n" << s::read_file(inner_log); + expect(s::no_line(s::read_file(inner_log), "Waiting for the build directory|Resolving")) << "the inner build waited or planned"; + }; + + "1069 an unchanged project tests without planning"_test[resource{"machine"}] = [] { + // A full `mcpp test` records what it planned beside test.ninja; the next one runs the + // selected tests from it, resolving no dev-dependency. An edited test, a test added or + // removed and an edited source each take the full path again. Alone on the machine: the + // fast path declines when the home's environment description is newer than its graph. + auto p = s::project::scaffold("app"); + for (const auto& f : s::glob(p.at("tests"), "*.cpp")) fs::remove(f); + p.write("mcpp.toml", p.read("mcpp.toml") + "[dev-dependencies]\nmcpplibs.mtest = \"0.5.2\"\n"); + auto write_cases = [&](int two) { + p.write("tests/cases.cpp", std::format(R"(import std; +import mcpplibs.mtest; +using namespace mcpplibs::mtest; +UTest one{{"one", [] {{ expect(eq(1, 1)); }}}}; +UTest two{{"two", [] {{ expect(eq(2, {})); }}}}; +)", + two)); + }; + write_cases(2); + auto full = p.mcpp({"test"}); + expect(fatal(e2e::succeeded(full))); + expect(e2e::contains(s::text(full), "Resolved mcpplibs.")) << "the first run did not plan"; + auto fast = p.mcpp({"test"}); + expect(fatal(e2e::succeeded(fast))); + expect(s::lacks(s::text(fast), "Resolved mcpplibs.")) << "an unchanged project was planned again"; + expect(e2e::contains(s::text(fast), "cases: 2 passed, 0 failed")); + // A selection runs from the record. + auto one = p.mcpp({"test", "cases", "--case", "two"}); + expect(fatal(e2e::succeeded(one))); + expect(s::lacks(s::text(one), "Resolved mcpplibs.")) << "a selection was planned again"; + expect(e2e::contains(s::text(one), "cases: 1 passed, 0 failed")); + // An edited test is built, and its result is the edit's. + std::this_thread::sleep_for(1s); + write_cases(3); + auto edited = p.mcpp({"test"}); + expect(edited.started && edited.status != 0) << "the edited test passed"; + expect(e2e::contains(s::text(edited), "cases: 1 passed, 1 failed")); + auto again = p.mcpp({"test"}); + expect(again.started && again.status != 0) << "the failing test passed from the record"; + expect(s::lacks(s::text(again), "Resolved mcpplibs.")) << "an unchanged failing project was planned again"; + expect(e2e::contains(s::text(again), "cases: 1 passed, 1 failed")) << "the record's run lost the failure"; + // A test added is planned and run; one removed is no longer run. + std::this_thread::sleep_for(1s); + write_cases(2); + p.write("tests/extra.cpp", "int main() { return 0; }\n"); + auto added = p.mcpp({"test"}); + expect(fatal(e2e::succeeded(added))); + expect(e2e::contains(s::text(added), "extra ... ok")); + p.remove("tests/extra.cpp"); + auto removed = p.mcpp({"test"}); + expect(fatal(e2e::succeeded(removed))); + expect(s::lacks(s::text(removed), "extra")) << "a removed test ran"; + // A build after the tests keeps its own graph. + auto build = p.mcpp({"build"}); + expect(fatal(e2e::succeeded(build))); + expect(s::lacks(s::text(build), "Compiling")) << "mcpp build after mcpp test compiled again"; + }; +}}; + +} // namespace diff --git a/tests/e2e/test/frameworks.cpp b/tests/e2e/test/frameworks.cpp new file mode 100644 index 000000000..57c03b1a8 --- /dev/null +++ b/tests/e2e/test/frameworks.cpp @@ -0,0 +1,86 @@ +// A test framework over the package under test, and the C++ library a test links on macOS +// (formerly tests/e2e/1052 and 94). +import std; +import mcpplibs.mtest; +import mcpp.test.support; + +using namespace mcpplibs::mtest; +namespace s = mcpp::test::support; +namespace fs = std::filesystem; + +namespace { + +E2E frameworks{"frameworks", [] { + "1052 a framework that uses the package under test is built against it"_test[tag{"requires:gcc"}] = [] { + // mtest uses mcpplibs.os, which is tested with mtest: the package being built stands for + // itself in its own graph, whatever version the framework's edge asked for. + s::needs({"gcc"}); + s::project t(temp_dir() / "t", {{"core/mcpp.toml", R"([package] +namespace = "acme" +name = "core" +version = "0.2.0" + +[targets.core] +kind = "lib" + +[dev-dependencies] +acme.tf = { path = "../tf" } +)"}, + {"core/src/core.cppm", "export module acme.core;\nexport int answer() { return 42; }\n"}, + {"core/tests/uses_tf.cpp", + "import acme.core;\nimport acme.tf;\nint main() { return tf::check(answer() == 42) ? 0 : 1; }\n"}, + {"tf/mcpp.toml", R"([package] +namespace = "acme" +name = "tf" +version = "0.1.0" + +[targets.tf] +kind = "lib" + +[dependencies] +acme.core = { path = "../core", version = "0.1.0" } +)"}, + {"tf/src/tf.cppm", R"(export module acme.tf; +import acme.core; +export namespace tf { bool check(bool held) { return held && answer() == 42; } } +)"}}); + auto r = t.mcpp_in("core", {"test"}); + expect(fatal(e2e::succeeded(r))) << "the test of the package under its own framework failed"; + expect(e2e::contains(s::text(r), "uses_tf ... ok")); + expect(e2e::contains(s::text(r), "it is the package being built, 0.2.0, which stands for itself")) + << "the version the edge did not ask for is not said"; + std::size_t n = 0; + for (const auto& f : s::find_files(t.at("core/target"), R"(core\.m\.o|core\.o)")) + if (f.generic_string().contains("acme_core") || f.generic_string().contains("/core")) ++n; + expect(n <= 1u) << "the root's module was compiled more than once (" << n << ")"; + }; + + "94 macos test stdlib"_test[tag{"requires:macos"}] = [] { + // A test program links the toolchain's libc++, not the system's (the header/dylib split of + // the old -lc++ broke on libc++ 22's out-of-line __hash_memory). + s::needs({"macos"}); + auto p = s::project::scaffold("hashapp"); + p.write("tests/hash_test.cpp", R"(// Forces the string-hash path (__hash_memory-class symbols on libc++ >= 22). +import std; +int main() { + std::unordered_map m; + for (int i = 0; i < 100; ++i) m["key" + std::to_string(i)] = i; + return (m.size() == 100 && m.at("key42") == 42) ? 0 : 1; +} +)"); + auto r = p.mcpp({"test"}); + expect(fatal(e2e::succeeded(r))); + const auto log = s::text(r); + expect(s::has_line(log, "[0-9]+ passed; 0 failed")); // the template ships its own test_smoke too + std::optional bin; + for (const auto& f : s::find_files(p.at("target"), "hash_test")) + if (f.parent_path().filename() == "bin") { bin = f; break; } + expect(fatal(bin.has_value())) << "no bin/hash_test"; + auto otool = e2e::run({.argv = {"otool", "-L", bin->string()}}); + // The link assertion applies to an LLVM toolchain; another resolution was covered above. + if (e2e::contains(log, "Resolved llvm@") || e2e::contains(otool.out, "libc++")) + expect(s::lacks(otool.out, "/usr/lib/libc++")) << "the test program links the system libc++"; + }; +}}; + +} // namespace diff --git a/tests/e2e/test/graph.cpp b/tests/e2e/test/graph.cpp new file mode 100644 index 000000000..3a0d89e2f --- /dev/null +++ b/tests/e2e/test/graph.cpp @@ -0,0 +1,223 @@ +// What `mcpp test` builds: dev-dependencies, the profile, link order against a framework's main, +// per-glob flags, the goal set and the test graph beside the build's (formerly tests/e2e/18, 72, +// 78, 157, 164 and 213). +import std; +import mcpplibs.mtest; +import mcpp.test.support; + +using namespace mcpplibs::mtest; +namespace s = mcpp::test::support; +namespace fs = std::filesystem; + +namespace { + +// The graph `mcpp test` writes (test.ninja, beside build.ninja since 2026.11.10.1), outside the +// build programs' directories. +std::optional test_graph(const s::project& p) { + for (const auto& f : s::find_files(p.at("target"), "test\\.ninja")) + if (!f.generic_string().contains("/.build-mcpp/")) + return f; + return std::nullopt; +} + +E2E graph{"graph", [] { + "18 devdeps isolation"_test = [] { + // dev-dependencies are not resolved by `mcpp build` and are by `mcpp test`. + auto p = s::project::scaffold("myapp"); + p.write("mcpp.toml", R"([package] +name = "myapp" +version = "0.1.0" + +[language] +standard = "c++23" +modules = true +import_std = true + +[modules] +sources = ["src/**/*.cppm", "src/**/*.cpp"] + +[targets.myapp] +kind = "bin" +main = "src/main.cpp" + +[dev-dependencies."ghost-test-framework"] +path = "/nonexistent/ghost-package" +)"); + auto b = p.mcpp({"build"}); + expect(fatal(e2e::succeeded(b))); + expect(e2e::contains(s::text(b), "Finished")); + expect(s::lacks(s::text(b), "ghost-test-framework")) << "the build path resolved the dev-dependency"; + auto t = p.mcpp({"test"}); + expect(t.started && t.status != 0) << "the test should fail on the bogus dev-dependency"; + expect(e2e::contains(s::text(t), "ghost-test-framework")) << "the test path did not reach the dev-dependency"; + }; + + "72 test profile features"_test[tag{"requires:gcc"}] = [] { + // `mcpp test --profile` selects the mode of the whole test build: it reaches the code + // under test (a shared src/ unit), not only the test's own file (#131 scenario B). + s::needs({"gcc"}); + s::project p{{"mcpp.toml", R"([package] +name = "proj" +version = "0.1.0" + +[build] +sources = ["src/**/*.cpp"] + +[profile.observe] +cxxflags = ["-DOBSERVE_MODE=1"] +)"}, + {"src/probe.cpp", R"(extern "C" int probe_mode(void) { +#ifdef OBSERVE_MODE + return 1; +#else + return 0; +#endif +} +)"}, + {"tests/test_mode.cpp", + "extern \"C\" int probe_mode(void);\nint main() { return probe_mode() == 1 ? 0 : 1; }\n"}}; + auto plain = p.mcpp({"test"}); + expect(plain.started && plain.status != 0) << "the default `mcpp test` passed: the macro reached the code"; + expect(e2e::succeeded(p.mcpp({"test", "--profile", "observe"}))); + }; + + "78 test main combinations"_test = [] { + // Every combination of {own main, the framework's main} x {uses gtest, does not} with gtest + // a dev-dependency: a library dependency is linked as an archive after the test's objects, + // so gtest_main.o is pulled only while main is undefined. + auto p = s::project::scaffold("combo"); + // The scaffold's tests use mtest; these do not. + auto manifest = p.read("mcpp.toml"); + manifest = std::regex_replace(manifest, std::regex("\n\\[dev-dependencies\\]\nmcpplibs\\.mtest = \"[^\"]*\"\n"), ""); + p.write("mcpp.toml", manifest + "\n[dev-dependencies.compat]\ngtest = \"1.15.2\"\n"); + p.remove("tests/test_smoke.cpp"); + p.write("tests/t_own_main_gtest.cpp", R"(#include +import std; +TEST(A, ok) { EXPECT_EQ(1 + 1, 2); } +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} +)"); + p.write("tests/t_framework_main.cpp", "#include \nTEST(B, ok) { EXPECT_TRUE(true); }\n"); + p.write("tests/t_own_main_no_gtest.cpp", "import std;\nint main() { std::println(\"ok\"); return 0; }\n"); + auto r = p.mcpp({"test"}); + expect(fatal(e2e::succeeded(r))); + const auto out = s::text(r); + for (auto t : {"t_own_main_gtest", "t_framework_main", "t_own_main_no_gtest"}) + expect(e2e::contains(out, std::format("{} ... ok", t))); + expect(e2e::contains(out, "3 passed; 0 failed")); + // The link line `build bin[.exe] : cxx_link ...` of each. + auto nj = test_graph(p); + expect(fatal(nj.has_value())) << "no test.ninja"; + const auto graph = s::read_file(*nj); + std::string own, fw; + for (const auto& l : s::lines_with(graph, " : cxx_link")) { + if (std::regex_search(l, std::regex(R"(t_own_main_gtest(\.exe)? : cxx_link)"))) own += l; + if (std::regex_search(l, std::regex(R"(t_framework_main(\.exe)? : cxx_link)"))) fw += l; + } + expect(fatal(!own.empty())) << "no link line for t_own_main_gtest"; + expect(fatal(!fw.empty())) << "no link line for t_framework_main"; + expect(s::lacks(own, "gtest_main")) << "the own-main test links gtest_main (a duplicate main)"; + expect(e2e::contains(fw, "gtest_main")) << "the framework-main test does not link gtest_main"; + }; + + "157 test glob flags"_test = [] { + // [build].flags globs cover tests/ units too, and a glob that matches real files (not + // scanned sources) does not warn that it matched nothing; a dead glob still does. + s::project p{{"mcpp.toml", R"([package] +name = "globtest" +version = "0.1.0" +standard = "c++23" + +[build] +flags = [ + { glob = "tests/tagged*.cpp", cxxflags = ["-DTAG=7"] }, + { glob = "tests/never/**", cxxflags = ["-DNOPE"] }, +] +)"}, + {"tests/tagged.cpp", R"(#ifndef TAG +#error "per-glob cxxflags did not reach the test TU" +#endif +int main() { return TAG == 7 ? 0 : 1; } +)"}, + {"tests/plain.cpp", R"(#ifdef TAG +#error "per-glob cxxflags leaked into an unmatched test TU" +#endif +int main() { return 0; } +)"}}; + auto r = p.mcpp({"test"}); + expect(fatal(e2e::succeeded(r))); + const auto out = s::text(r); + expect(e2e::contains(out, "tagged ... ok")); + expect(e2e::contains(out, "plain ... ok")); + expect(s::lacks(out, "'tests/tagged*.cpp' matched no source file")) << "a spurious dead-glob warning"; + expect(e2e::contains(out, "'tests/never/**' matched no source file")) << "a dead glob must still warn"; + }; + + "164 ninja goal command length"_test = [] { + // The explicit goal set of `mcpp test` reaches ninja through the manifest (a phony edge), + // never on the command line (FFmpeg's 50,781-character argv against cmd.exe's 8191). + s::project p{{"mcpp.toml", "[package]\nname = \"goalset\"\nversion = \"0.1.0\"\n"}, + {"src/main.cpp", "int main() { return 0; }\n"}, + {"tests/sanity.cpp", "extern int unit_1();\nint main() { return unit_1() == 1 ? 0 : 1; }\n"}}; + for (int i = 1; i <= 30; ++i) + p.write(std::format("src/unit_{}.cpp", i), std::format("int unit_{}() {{ return {}; }}\n", i, i)); + auto r = p.mcpp({"test"}); + expect(fatal(e2e::succeeded(r))); + expect(e2e::contains(s::text(r), "sanity ... ok")); + auto nj = test_graph(p); + expect(fatal(nj.has_value())) << "no test.ninja"; + expect(s::has_line(s::read_file(*nj), "^build mcpp-requested-goals : phony ")) + << "the explicit goals are not aggregated into a phony edge"; + // mcpp records the ninja program it ran in target/.build_cache; the goals never belong there. + if (p.exists("target/.build_cache")) + expect(s::no_line(p.read("target/.build_cache"), "obj/.*obj/.*obj/")) + << "goal paths leaked into the recorded ninja invocation"; + }; + + "213 build after test is not the test graph"_test[tag{"requires:gcc"}] = [] { + // mcpp#407: a plain build after `mcpp test` must not replay the test's graph, and a broken + // test file must not fail a plain build. Nothing is touched or deleted between the + // commands: the defect exists only while the fast path is eligible. + s::needs({"gcc"}); + s::project p{{"mcpp.toml", "[package]\nname = \"tst\"\nversion = \"0.1.0\"\n"}, + {"src/main.cpp", "int main() { return 0; }\n"}, + {"tests/smoke.cpp", "int main() { return 0; }\n"}}; + auto target_bin = [&] { return s::glob(p.at("target"), "*/*/bin/tst"); }; + auto build_graph = [&] { return s::find_file(p.at("target"), "build.ninja"); }; + + // 1. A plain build declares its graph. + expect(fatal(e2e::succeeded(p.mcpp({"build"})))); + auto n = build_graph(); + expect(fatal(n.has_value())) << "no build.ninja after the first build"; + expect(!target_bin().empty()) << "the plain build produced no bin/tst"; + expect(fatal(s::has_line(s::read_file(*n), "^# mcpp:graph=normal"))); + + // 2. `mcpp test` writes test.ninja beside it and leaves build.ninja the build's. + expect(fatal(e2e::succeeded(p.mcpp({"test"})))); + const auto t = n->parent_path() / "test.ninja"; + expect(fatal(fs::is_regular_file(t))) << "mcpp test wrote no test.ninja beside build.ninja"; + expect(s::has_line(s::read_file(t), "^# mcpp:graph=test")); + expect(s::has_line(s::read_file(*n), "^# mcpp:graph=normal")) << "mcpp test rewrote build.ninja"; + + // 3. A plain build after the test does not replay that graph. + expect(fatal(e2e::succeeded(p.mcpp({"build"})))) << "the plain build after mcpp test failed"; + auto n3 = build_graph(); + expect(fatal(n3.has_value())); + const auto g3 = s::read_file(*n3); + expect(s::has_line(g3, "^# mcpp:graph=normal")) << "the plain build replayed the test graph (mcpp#407)"; + const auto defaults = s::lines_with(g3, "default "); + std::string default_line; + for (const auto& l : defaults) + if (l.starts_with("default ")) { default_line = l; break; } + expect(e2e::contains(default_line, "bin/tst")) << "the default line names the test graph"; + expect(!target_bin().empty()) << "bin/tst is gone after a plain build"; + + // 4. A broken test file does not fail a plain build. + p.write("tests/smoke.cpp", "this is not valid C++ at all\n"); + expect(e2e::succeeded(p.mcpp({"build"}))) << "a broken tests/*.cpp failed a plain mcpp build"; + }; +}}; + +} // namespace diff --git a/tests/e2e/test/launch.cpp b/tests/e2e/test/launch.cpp new file mode 100644 index 000000000..8b8c65a74 --- /dev/null +++ b/tests/e2e/test/launch.cpp @@ -0,0 +1,146 @@ +// `mcpp test --print-launch`: one test, built and described as `mcpp test` would start it +// (formerly tests/e2e/993). +import std; +import mcpplibs.mtest; +import mcpp.test.support; + +using namespace mcpplibs::mtest; +namespace s = mcpp::test::support; +namespace fs = std::filesystem; +using nlohmann::json; + +namespace { + +constexpr std::string_view math_cpp = R"(import std; +int main(int argc, char** argv) { + std::ofstream("started-math") << argc; + std::println("math ran with {} argument(s), last [{}]", argc - 1, argc > 1 ? argv[argc - 1] : ""); + return 0; +} +)"; +constexpr std::string_view math2_cpp = "import std;\nint main() { std::ofstream(\"started-math2\") << 1; return 0; }\n"; + +bool same_path(const std::string& a, const std::string& b) { + return fs::path(a).lexically_normal() == fs::path(b).lexically_normal(); +} + +// Runs what an envelope's `data` describes: its argv, in its cwd, with its env. +e2e::outcome run_described(const json& data) { + e2e::command c; + for (const auto& a : data["argv"]) c.argv.push_back(a); + c.cwd = data["cwd"].get(); + if (data.contains("env") && data["env"].is_object()) + for (auto it = data["env"].begin(); it != data["env"].end(); ++it) + c.env.emplace_back(it.key(), it.value().get()); + return e2e::run(std::move(c)); +} + +s::project app_with_tests() { + auto p = s::project::scaffold("app"); + p.write("tests/unit/math.cpp", math_cpp); + p.write("tests/unit/math2.cpp", math2_cpp); + return p; +} + +E2E launch{"launch", [] { + "993 a test describes its launch"_test = [] { + auto p = app_with_tests(); + // A: the envelope names the test, its program, env and cwd, ends its argv with the arguments + // after `--`, and the test is not started. + auto a = p.mcpp({"test", "unit/math", "--print-launch", "--", "--flag", "x"}); + expect(fatal(e2e::succeeded(a))); + expect(!p.exists("started-math") && !p.exists("target/started-math")) << "the test was started"; + const auto d = s::json(a.out); + expect(fatal(d.contains("kind") && d["kind"] == "mcpp.launch")) << a.out; + const auto& data = d["data"]; + expect(data["test"] == "unit/math") << data.dump(); + expect(fs::is_regular_file(data["program"].get())) << data.dump(); + const auto& argv = data["argv"]; + expect(std::ranges::any_of(argv, [&](const json& x) { return same_path(x.get(), data["program"].get()); })) << data.dump(); + expect(argv.size() >= 2 && argv[argv.size() - 2] == "--flag" && argv.back() == "x") << argv.dump(); + expect(data.contains("env") && data.contains("cwd")); + const auto last = s::json_file(p.at("target/.last-result.json"))["data"]; + const auto recorded = s::json_file(last["result"].get())["data"]["configuration"]; + for (auto key : {"triple", "profile", "platform"}) + expect(data["configuration"][key] == recorded[key]) << key << ": " << data.dump(); + expect(data["configuration"]["runners"].is_array()) << data.dump(); + // B: what the envelope describes runs. + auto b = run_described(data); + expect(e2e::succeeded(b)) << "what the envelope describes did not run"; + expect(e2e::contains(b.out, "math ran with 2 argument(s), last [x]")); + // C: `unit/math` is contained in `unit/math2` too; the exact name is chosen. + expect(e2e::contains(a.out, R"("unit/math")")); + // D: the refusals, exit 2. + auto d1 = p.mcpp({"test", "--print-launch"}); + expect(e2e::exited(d1, 2)); + expect(e2e::contains(d1.err, "names no test")); + auto d2 = p.mcpp({"test", "unit/", "--print-launch"}); + expect(e2e::exited(d2, 2)); + expect(e2e::contains(d2.err, "describes one test")); + auto d3 = p.mcpp({"test", "unit/math", "--print-launch", "--message-format", "json"}); + expect(e2e::exited(d3, 2)); + expect(e2e::contains(d3.err, "message-format json")); + auto d4 = p.mcpp({"test", "unit/math", "--print-launch", "--no-run"}); + expect(e2e::exited(d4, 2)); + expect(e2e::contains(d4.err, "--no-run")); + // E + auto e = p.mcpp({"--protocol-version"}); + expect(e2e::succeeded(e)); + expect(e2e::contains(s::text(e), R"("test-launch")")); + }; + + "993 a test describes its launch (a named runner)"_test[tag{"requires:python3"}] = [] { + // A named runner is the same transformation for a test and a run. The runner is a Python + // program, a real host runner whose generated argv can be executed. + s::needs({"python3"}); + auto p = app_with_tests(); + const auto python = s::on_path("python3")->generic_string(); + p.write("runner.py", + "import json,subprocess,sys\nwith open(\"named-invocations.txt\",\"a\") as f: f.write(json.dumps(sys.argv[1:])+\"\\n\")\n" + "print(\"NAMED RUNNER\", flush=True)\nsys.exit(subprocess.call(sys.argv[1:]))\n"); + const auto os_name = s::on_linux() ? "linux" : s::on_macos() ? "macos" : "windows"; + p.write("mcpp.toml", p.read("mcpp.toml") + + std::format("\n[target.'cfg(os = \"{}\")'.runners]\ndebuggee = {}\n", os_name, + json::array({python, p.at("runner.py").generic_string(), "{}"}).dump())); + auto named = p.mcpp({"test", "unit/math", "--runner", "debuggee", "--print-launch", "--", "with spaces"}); + expect(fatal(e2e::succeeded(named))) << "named test launch"; + const auto d = s::json(named.out)["data"]; + expect(d["via"] == "runner") << d.dump(); + expect(d["configuration"]["runners"] == json::array({"debuggee"})) << d.dump(); + expect(d["argv"].back() == "with spaces") << d.dump(); + expect(d["argv"][1].get().ends_with("runner.py")) << d.dump(); + auto run = run_described(d); + expect(e2e::succeeded(run)) << "named launch execution"; + expect(e2e::contains(run.out, "NAMED RUNNER")) << "the named runner did not run"; + expect(e2e::contains(run.out, "last [with spaces]")) << "the named runner changed the arguments"; + p.remove("named-invocations.txt"); + + auto invocations = [&] { + std::vector rows; + for (const auto& l : s::lines_with(p.read("named-invocations.txt"), "[")) + rows.push_back(json::parse(l)); + return rows; + }; + auto one_call = [&](std::string_view last) { + const auto rows = invocations(); + return rows.size() == 1 && rows[0][0].get().contains("math2") && rows[0].back() == last; + }; + auto direct = p.mcpp({"test", "unit/math2", "--runner", "debuggee", "--", "direct"}); + expect(e2e::succeeded(direct)) << "named runner direct test"; + expect(one_call("direct")) << "a direct test skipped the named runner: " << p.read("named-invocations.txt"); + expect(fatal(e2e::succeeded(p.mcpp({"test", "unit/math2", "--no-run", "--archive", "named-tests.tar.gz"})))); + p.remove("named-invocations.txt"); + auto archived = p.mcpp({"test", "--archive", "named-tests.tar.gz", "--runner", "debuggee", "--", "archived"}); + expect(e2e::succeeded(archived)) << "named archive runner"; + expect(one_call("archived")) << "the archive skipped the named runner: " << p.read("named-invocations.txt"); + + auto missing = p.mcpp({"test", "unit/math", "--runner", "absent", "--print-launch"}); + expect(e2e::exited(missing, 2)); + expect(e2e::contains(missing.err, "no runner named 'absent'")); + auto conflict = p.mcpp({"test", "unit/math", "--runner", "debuggee", "--no-runner"}); + expect(e2e::exited(conflict, 2)); + expect(e2e::contains(conflict.err, "--runner and --no-runner")); + }; +}}; + +} // namespace diff --git a/tests/e2e/test/protocol.cpp b/tests/e2e/test/protocol.cpp new file mode 100644 index 000000000..029a606bd --- /dev/null +++ b/tests/e2e/test/protocol.cpp @@ -0,0 +1,359 @@ +// Test programs that speak mtest's test protocol: their cases, live case records, the spec's +// launch, capabilities, checks and output (formerly tests/e2e/1035, 1065 and 1070). +import std; +import mcpplibs.mtest; +import mcpp.test.support; + +using namespace mcpplibs::mtest; +namespace s = mcpp::test::support; +namespace fs = std::filesystem; +using nlohmann::json; + +namespace { + +// The record of test `name` in a stream (the first line naming it). +json record(std::string_view stream, std::string_view name) { + for (auto& r : s::json_lines(stream)) + if (r.contains("test") && r["test"] == name) + return r; + return json{}; +} + +std::vector ids(const json& rec) { + std::vector out; + if (rec.contains("cases")) + for (const auto& c : rec["cases"]) out.push_back(c["id"]); + return out; +} + +std::string generic(const fs::path& p) { return p.generic_string(); } + +E2E protocol{"protocol", [] { + "1035 a test program reports its cases"_test[tag{"requires:gcc"}] = [] { + // mcpp runs the runner's side of the mtest protocol, version 1; every other program is one + // case judged by its exit status and is told none of the variables. + s::needs({"gcc"}); + s::project ws{{"mcpp.toml", "[workspace]\nmembers = [\"fw\", \"app\", \"plain\"]\n"}, + {"fw/mcpp.toml", R"([package] +name = "fw" +version = "0.1.0" + +[targets.main] +kind = "entry" +main = "src/main.cpp" +test-protocol = 1 +)"}, + {"fw/src/main.cpp", R"CPP(import std; +struct test_case { std::string id; int line; int (*body)(); }; +std::vector cases(); +int main() { + const char* mode = std::getenv("MTEST_MODE"); + const char* path = std::getenv("MTEST_REPORT"); + std::ofstream report; + if (path) report.open(path, std::ios::app); + auto event = [&](const std::string& body) { if (report) { report << "{" << body << "}\n"; report.flush(); } }; + event(R"("e":"program","protocol":1,"level":1,"framework":"fw","version":"0.1.0")"); + std::set selected; + bool selecting = false; + if (const char* s = std::getenv("MTEST_SELECT")) { + selecting = true; + std::ifstream in(s); + for (std::string line; std::getline(in, line);) if (!line.empty()) selected.insert(line); + } + int failed = 0; + for (const auto& c : cases()) { + if (mode && std::string(mode) == "list") { + event(std::format(R"("e":"case","id":"{}","name":"{}","file":"t.cpp","line":{})", c.id, c.id, c.line)); + continue; + } + if (selecting && !selected.contains(c.id)) continue; + event(std::format(R"("e":"begin","id":"{}","t":0)", c.id)); + const int r = c.body(); + if (r != 0) { + ++failed; + event(std::format(R"("e":"failure","id":"{}","file":"t.cpp","line":{},"message":"{} != 0")", c.id, c.line, r)); + } + event(std::format(R"("e":"end","id":"{}","status":"{}","ms":1)", c.id, r ? "fail" : "pass")); + } + event(std::format(R"("e":"done","status":"{}","counts":{{}})", failed ? "fail" : "pass")); + return failed ? 1 : 0; +} +)CPP"}, + {"app/mcpp.toml", R"([package] +name = "app" +version = "0.1.0" + +[dev-dependencies] +fw = { path = "../fw" } + +[test] +main = { from = "fw/main" } +)"}, + {"app/src/app.cppm", "export module app;\nexport int answer() { return 42; }\n"}, + {"app/tests/math.cpp", R"(import std; +import app; +struct test_case { std::string id; int line; int (*body)(); }; +std::vector cases() { + return { + {"math/adds", 4, [] { return answer() == 42 ? 0 : 1; }}, + {"math/fails", 5, [] { return 7; }}, + {"math/crashes", 6, [] { std::exit(3); return 0; }}, + }; +} +)"}, + {"plain/mcpp.toml", "[package]\nname = \"plain\"\nversion = \"0.1.0\"\n"}, + {"plain/tests/bare.cpp", R"(import std; +int main() { + for (const char* v : {"MTEST_PROTOCOL", "MTEST_MODE", "MTEST_REPORT"}) + if (std::getenv(v)) { std::println("{} reached a program that implements no protocol", v); return 3; } + return 0; +} +)"}, + {"plain/tests/marked.cpp", R"CPP(import std; +constinit const char marker[] = "mtest-protocol=1"; +int main() { + [[maybe_unused]] volatile char keep = marker[0]; + const char* path = std::getenv("MTEST_REPORT"); + if (!path) return 4; + std::ofstream report(path, std::ios::app); + const std::string mode = std::getenv("MTEST_MODE") ? std::getenv("MTEST_MODE") : "run"; + report << R"({"e":"program","protocol":1,"level":1,"framework":"marked","version":"1"})" << "\n"; + if (mode == "list") report << R"({"e":"case","id":"only","name":"only","file":"marked.cpp","line":1})" << "\n"; + else report << R"({"e":"begin","id":"only","t":0})" << "\n" << R"({"e":"end","id":"only","status":"pass","ms":0})" << "\n"; + report << R"({"e":"done","status":"pass","counts":{}})" << "\n"; + return 0; +} +)CPP"}}; + // A + auto a = ws.mcpp({"test", "-p", "app", "--message-format", "json"}); + expect(a.started && a.status != 0) << "a failing case passed the run"; + auto rec = record(a.out, "math"); + expect(fatal(!rec.is_null())) << "no record of the test\n" << s::text(a); + expect(rec["protocol"] == 1) << rec.dump(); + std::vector results; + for (const auto& c : rec["cases"]) results.push_back(c["id"].get() + ":" + c["status"].get()); + expect(eq(results, std::vector{"math/adds:pass", "math/fails:fail", "math/crashes:fail"})) << rec.dump(); + expect(rec["cases"][1]["failures"][0]["line"] == 5) << "the failure's place"; + expect(e2e::contains(rec["cases"][2].value("reason", ""), "status 3 before the case ended")) << "a crashed case's reason (P5.4)"; + auto ah = ws.mcpp({"test", "-p", "app"}); + expect(ah.started && ah.status != 0) << "the human run passed"; + expect(s::has_line(s::text(ah), R"(^ math/fails t\.cpp:5$)")) << "the failures' index does not name the failed case at its place"; + expect(e2e::contains(s::text(ah), "cases: 1 passed, 2 failed, 0 skipped")); + // B + auto b = ws.mcpp({"test", "--case", "math/adds", "--message-format", "json"}); + expect(fatal(e2e::succeeded(b))); + expect(eq(ids(record(b.out, "math")), std::vector{"math/adds"})); + expect(record(b.out, "bare").is_null()) << "a program with no protocol ran under --case"; + // C + auto c = ws.mcpp({"test", "--cases"}); + expect(fatal(e2e::succeeded(c))); + expect(e2e::contains(s::text(c), "math: 3 cases")); + expect(e2e::contains(s::text(c), "math/fails t.cpp:5")); + expect(e2e::contains(s::text(c), "bare: one case")); + // D, E: mcpp's own environment has the protocol's variables. + auto d = ws.mcpp({"test", "-p", "plain", "--message-format", "json"}, + {.env = {{"MTEST_PROTOCOL", "1"}, {"MTEST_MODE", "list"}, {"MTEST_REPORT", (temp_dir() / "outer.ndjson").string()}}}); + expect(fatal(e2e::succeeded(d))); + expect(record(d.out, "bare")["status"] == "pass") << "the protocol's variables reached a program with none (P1.2)"; + expect(record(d.out, "marked")["protocol"] == 1) << "the marker"; + expect(eq(ids(record(d.out, "marked")), std::vector{"only"})); + // F + auto f = ws.mcpp({"test", "-p", "app", "--no-run", "--message-format", "json"}); + expect(fatal(e2e::succeeded(f))); + rec = record(f.out, "math"); + expect(rec["status"] == "built"); + expect(e2e::contains(rec.value("program", ""), "math")); + bool test_src = false, lib_src = false; + for (const auto& src : rec["sources"]) { + const auto v = src.get(); + test_src = test_src || v.ends_with("app/tests/math.cpp"); + lib_src = lib_src || v.ends_with("app/src/app.cppm"); + } + expect(test_src && lib_src) << "the sources: " << rec["sources"].dump(); + }; + + "1065 test records carry parts live cases and metrics"_test[tag{"requires:gcc"}] = [] { + s::needs({"gcc"}); + auto p = s::project::scaffold("app"); + p.write("tests/unit/a.cpp", R"(import mcpplibs.mtest; +import mcpplibs.mtest.bench; +using namespace mcpplibs::mtest; +Bench timed{"timed", at_most{"spin", 10.0}, [] { + bench::measure("spin", [] { int x = 0; for (int i = 0; i < 100; ++i) bench::keep(x += i); }, + {.samples = 3, .warmup = 0}); +}}; +)"); + p.write("tests/unit/b.cpp", R"(import mcpplibs.mtest; +using namespace mcpplibs::mtest; +UTest plain{"plain", tag{"quick"}, [] { expect(eq(1 + 1, 2)); }}; +)"); + p.write("mcpp.toml", p.read("mcpp.toml") + "\n[test]\nprograms = \"directory\"\n"); + // A: the listing names the parts. + auto a = p.mcpp({"test", "--cases", "--message-format", "json"}); + expect(fatal(e2e::succeeded(a))); + const auto unit = record(a.out, "unit"); + expect(fatal(unit.contains("parts"))) << a.out; + std::vector names; + for (const auto& part : unit["parts"]) { + names.push_back(part["name"]); + const auto file = part["file"].get(); + expect(file.ends_with("a.cpp") || file.ends_with("b.cpp")) << file; + } + std::ranges::sort(names); + expect(eq(names, std::vector{"unit/a", "unit/b"})); + // B: case records as they happen, before their program's record. + auto b = p.mcpp({"test", "--message-format", "json"}); + expect(fatal(e2e::succeeded(b))); + const auto recs = s::json_lines(b.out); + for (auto part : {"unit/a", "unit/b"}) { + bool begin = false, end = false, reached = false; + for (const auto& r : recs) { + if (r.contains("test") && r["test"] == part) { reached = true; break; } + if (r.contains("case") && r["case"]["test"] == part) { + begin = begin || r["case"]["event"] == "begin"; + end = end || r["case"]["event"] == "end"; + } + } + expect(reached && begin && end) << part << ": no begin and end before its record"; + } + json end; + for (const auto& r : recs) + if (r.contains("case") && r["case"]["event"] == "end" && r["case"]["id"] == "timed") { end = r["case"]; break; } + expect(fatal(!end.is_null())); + expect(end["status"] == "pass") << end.dump(); + expect(end["metrics"][0]["name"] == "spin" && end["metrics"][0]["unit"] == "s") << end.dump(); + expect(end["bounds"] == json::parse(R"([{"metric": "spin", "max": 10.0}])")) << end.dump(); + const auto ra = record(b.out, "unit/a"); + expect(ra["part_of"] == "unit") << ra.dump(); + json timed; + for (const auto& c : ra["cases"]) if (c["id"] == "timed") timed = c; + expect(timed["kind"] == "bench" && !timed["metrics"].empty() && !timed["bounds"].empty()) << timed.dump(); + json plain; + for (const auto& c : record(b.out, "unit/b")["cases"]) if (c["id"] == "plain") plain = c; + expect(plain.value("tags", json::array()) == json::parse(R"(["quick"])")) << plain.dump(); + // C: the kept run. + const auto runs = s::json_lines(p.read("target/mtest/index.ndjson")); + expect(fatal(!runs.empty())); + json kept; + for (const auto& c : s::json_lines(s::read_file(p.at("target/mtest/runs") / runs.back()["id"].get() / "cases.ndjson"))) + if (c["id"] == "timed") kept = c; + expect(kept["kind"] == "bench" && kept["started_ms"] >= 0) << kept.dump(); + expect(kept["metrics"][0]["name"] == "spin" && kept["bounds"][0]["metric"] == "spin") << kept.dump(); + // D + auto d = p.mcpp({"self", "env", "--format", "json"}); + expect(fatal(e2e::succeeded(d))); + for (auto f : {"test-parts", "test-case-stream", "test-metrics"}) + expect(e2e::contains(s::text(d), std::format("\"{}\"", f))); + }; + + "1070 the test spec launches frameworks checks and output"_test[tag{"requires:gcc"}] = [] { + // The mcpp test spec's B5, B6, O3 and O8 (docs/specs/test.md). + s::needs({"gcc"}); + s::project t(temp_dir() / "t", {{"fw/mcpp.toml", R"([package] +name = "fw" +version = "0.1.0" + +[targets.main] +kind = "entry" +main = "src/main.cpp" +test-protocol = 1 +test-main = true +test-capabilities = ["check-details", "output"] +test-session = ["fw-serve", "--stdio"] +)"}, + {"fw/src/main.cpp", R"CPP(import std; +extern bool cases(std::ofstream& report, bool list, const std::set& selected); +int main() { + const char* path = std::getenv("MTEST_REPORT"); + if (!path) return 4; + std::ofstream report(path, std::ios::app); + const std::string mode = std::getenv("MTEST_MODE") ? std::getenv("MTEST_MODE") : "run"; + std::set selected; + if (const char* s = std::getenv("MTEST_SELECT")) { + std::ifstream in(s); + for (std::string line; std::getline(in, line);) selected.insert(line); + } + report << R"({"e":"program","protocol":1,"level":1,"framework":"fw","version":"0.1.0"})" << "\n"; + const bool failed = cases(report, mode == "list", selected); + report << (failed ? R"({"e":"done","status":"fail","counts":{}})" : R"({"e":"done","status":"pass","counts":{}})") << "\n"; + report.flush(); + return failed ? 1 : 0; +} +)CPP"}, + {"app/tests/t.cpp", R"CPP(import std; +bool cases(std::ofstream& r, bool list, const std::set& selected) { + auto on = [&](std::string_view id) { return selected.empty() || selected.contains(std::string(id)); }; + if (list) { + r << R"({"e":"case","id":"stated","name":"stated","file":"t.cpp","line":3})" << "\n"; + r << R"({"e":"case","id":"plain","name":"plain","file":"t.cpp","line":4})" << "\n"; + return false; + } + if (on("stated")) { + r << R"({"e":"begin","id":"stated","t":0})" << "\n"; + r << R"({"e":"output","id":"stated","stream":"out","text":"said by the case\n"})" << "\n"; + r << R"({"e":"failure","id":"stated","file":"t.cpp","line":3,"message":"a == b","expected":"2","actual":"3","source":"expect(eq(a, b));","fatal":true})" << "\n"; + r << R"({"e":"end","id":"stated","status":"fail","ms":0,"checks":{"held":4,"failed":1}})" << "\n"; + } + if (on("plain")) { + std::println("written by plain"); + r << R"({"e":"begin","id":"plain","t":0})" << "\n"; + r << R"({"e":"end","id":"plain","status":"pass","ms":0})" << "\n"; + } + return on("stated"); +} +)CPP"}}); + t.write("app/mcpp.toml", std::format("[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dev-dependencies]\nfw = {{ path = \"{}\" }}\n", + generic(t.at("fw")))); + auto app = [&](std::vector args) { return t.mcpp_in("app", std::move(args)); }; + // B6 + auto g = app({"test", "--guide", "--message-format", "json"}); + expect(fatal(e2e::succeeded(g))); + const auto guide = s::json_lines(g.out); + expect(fatal(!guide.empty() && guide.front().contains("guide"))) << g.out; + json fw; + for (const auto& x : guide.front()["guide"]["frameworks"]) if (x["entry"] == "fw/main") fw = x; + expect(fw["capabilities"] == json::parse(R"(["check-details", "output"])")) << fw.dump(); + expect(fw["session"] == json::parse(R"(["fw-serve", "--stdio"])")) << fw.dump(); + auto gh = app({"test", "--guide"}); + expect(e2e::succeeded(gh)); + expect(e2e::contains(s::text(gh), "implements check-details, output")); + expect(e2e::contains(s::text(gh), "session fw-serve --stdio")); + // B5 + auto nr = app({"test", "--no-run", "--message-format", "json", "--", "--flag"}); + expect(fatal(e2e::succeeded(nr))); + auto rec = record(nr.out, "t"); + expect(rec["status"] == "built") << rec.dump(); + expect(fatal(rec.contains("launch"))) << rec.dump(); + expect(rec["launch"]["argv"].back() == "--flag") << "the launch omits the arguments after --"; + auto argv0 = rec["launch"]["argv"][0].get(); + std::string argv0_slashed = argv0; + std::ranges::replace(argv0_slashed, '\\', '/'); + expect(argv0 == rec["program"].get() || argv0_slashed == rec["program"].get()) << "the launch does not start the program"; + expect(!rec["launch"]["env"].contains("MTEST_PROTOCOL")) << "the launch carries the protocol's variables"; + expect(!rec["launch"].value("cwd", "").empty()) << "the launch names no directory"; + // O3, O8: both cases. + auto all = app({"test", "--message-format", "json"}); + expect(all.started && all.status != 0) << "the failing case passed"; + rec = record(all.out, "t"); + expect(fatal(rec.contains("cases"))) << all.out; + expect(rec["cases"][0]["failures"][0]["source"] == "expect(eq(a, b));") << "the failure's source"; + expect(rec["cases"][0]["failures"][0]["fatal"] == true) << "the failure's fatal"; + expect(rec["cases"][0]["checks"]["held"] == 4 && rec["cases"][0]["checks"]["failed"] == 1) << "the case's checks"; + // The script compared python's print of the string, which ends no line: the stated text. + expect(rec["cases"][0].value("output", "") == "said by the case\n" || rec["cases"][0].value("output", "") == "said by the case") + << "the case's stated output: " << rec["cases"][0].dump(); + expect(!rec["cases"][1].contains("output")) << "the output of a program of two cases was divided"; + bool kept = false; + for (const auto& f : s::glob(t.at("app/target/mtest/runs"), "*/cases.ndjson")) { + const auto c = s::read_file(f); + kept = kept || c.contains(R"("checks":{"failed":1,"held":4})") || c.contains(R"("checks":{"held":4,"failed":1})"); + } + expect(kept) << "the kept run has no checks"; + // O8: one case run alone. + auto one = app({"test", "--case", "plain", "--message-format", "json"}); + expect(fatal(e2e::succeeded(one))); + expect(e2e::contains(record(one.out, "t")["cases"][0].value("output", ""), "written by plain")); + }; +}}; + +} // namespace diff --git a/tests/e2e/test/report.cpp b/tests/e2e/test/report.cpp new file mode 100644 index 000000000..55b3d42e8 --- /dev/null +++ b/tests/e2e/test/report.cpp @@ -0,0 +1,137 @@ +// `mcpp test` runs each test and reports it: verdicts, failures, the JSON stream, signals and +// timeouts (formerly tests/e2e/15, 16, 17, 153, 155, 158 and 160). +import std; +import mcpplibs.mtest; +import mcpp.test.support; + +using namespace mcpplibs::mtest; +namespace s = mcpp::test::support; + +namespace { + +std::string manifest(std::string_view name) { + return std::format("[package]\nname = \"{}\"\nversion = \"0.1.0\"\nstandard = \"c++23\"\n", name); +} + +E2E report{"report", [] { + "15 test passing"_test = [] { + // `mcpp test` discovers tests/**/*.cpp and runs each as a separate binary; all passing: + // exit 0 and the summary. + auto p = s::project::scaffold("myapp"); + p.write("tests/test_other.cpp", "import std;\nint main() { std::println(\"test_other: ok\"); return 0; }\n"); + auto r = p.mcpp({"test"}); + expect(fatal(e2e::succeeded(r))); + const auto out = s::text(r); + expect(e2e::contains(out, "test_smoke ... ok")); + expect(e2e::contains(out, "test_other ... ok")); + expect(e2e::contains(out, "2 passed; 0 failed")); + }; + + "16 test failing"_test[tag{"requires:scan-deps"}] = [] { + // One ok and one failing test: exit non-zero, and the summary lists the failures. + s::needs({"scan-deps"}); + auto p = s::project::scaffold("myapp"); + p.write("tests/test_fail.cpp", "int main() { return 1; }\n"); + auto r = p.mcpp({"test"}); + expect(r.started && r.status != 0) << "expected a non-zero exit\n" << r.line; + const auto out = s::text(r); + expect(e2e::contains(out, "test_fail ... FAIL")); + expect(e2e::contains(out, "test_smoke ... ok")); + expect(e2e::contains(out, "failures:")); + expect(e2e::contains(out, "1 passed; 1 failed")); + }; + + "17 test no tests"_test = [] { + // A project with no tests/: `mcpp test` says "no tests found" (and exits 0). + auto p = s::project::scaffold("myapp"); + p.remove("tests"); + auto r = p.mcpp({"test"}); + expect(e2e::succeeded(r)); + expect(e2e::contains(s::text(r), "no tests found")); + }; + + "153 test isolation"_test = [] { + // A test that does not compile fails alone; the others build and run. + s::project p{{"mcpp.toml", manifest("isolation")}, + {"tests/ok.cpp", "int main() { return 0; }\n"}, + {"tests/runfail.cpp", "int main() { return 1; }\n"}, + {"tests/nocompile.cpp", "int main() { D2X_YOUR_ANSWER x = 1; return x; }\n"}}; + auto r = p.mcpp({"test"}); + expect(e2e::exited(r, 1)); + const auto out = s::text(r); + expect(e2e::contains(out, "ok ... ok")); + // A test's line carries its own wall time: the prefix is matched. + expect(e2e::contains(out, "runfail ... FAIL (exit 1,")); + expect(e2e::contains(out, "nocompile ... FAIL (compile,")); + expect(e2e::contains(out, "1 passed")); + expect(e2e::contains(out, "2 failed")); + + // A package that does not compile stays a hard error: 'build failed', not all red. + p.write("src/isolation.cppm", "export module isolation;\nthis is not C++;\n"); + auto r2 = p.mcpp({"test"}); + expect(r2.started && r2.status != 0) << "a package error must fail\n" << r2.line; + expect(s::lacks(s::text(r2), "nocompile ... FAIL (compile,")) << "the package error was given to the tests"; + }; + + "155 test json"_test = [] { + // --message-format json: a record per test, the package error's record, stdout pure. + s::project p{{"mcpp.toml", manifest("jsonout")}, + {"tests/ok.cpp", "#include \nint main() { std::puts(\"hello from ok\"); return 0; }\n"}, + {"tests/runfail.cpp", "int main() { return 1; }\n"}, + {"tests/nocompile.cpp", "int main() { D2X_YOUR_ANSWER x = 1; return x; }\n"}}; + auto r = p.mcpp({"test", "--message-format", "json"}); + expect(e2e::exited(r, 1)); + for (const auto& line : s::lines_with(r.out, "")) + if (!line.empty()) + expect(line.starts_with("{")) << "a line of stdout is not JSON: " << line; + expect(e2e::contains(r.out, R"("test":"ok","status":"pass")")); + expect(e2e::contains(r.out, R"("test":"runfail","status":"run_fail")")); + expect(e2e::contains(r.out, R"("exit_code":1)")); + expect(e2e::contains(r.out, R"("test":"nocompile","status":"compile_fail")")); + expect(e2e::contains(r.out, "D2X_YOUR_ANSWER")) << "the compile output carries no diagnostic"; + expect(e2e::contains(r.out, "hello from ok")) << "the run output is not captured"; + // The summary leads with its member and carries the build/run split: the counts are matched. + expect(e2e::contains(r.out, R"("passed":1,"failed":2)")); + expect(e2e::matches(r.out, R"("build_ms":[0-9]+,"run_ms":[0-9]+)")); + // An invalid format is a usage error. + expect(e2e::exited(p.mcpp({"test", "--message-format", "yaml"}), 2)); + }; + + "158 test signal and duration"_test[tag{"requires:unix-shell"}] = [] { + // A signaled test reports the shell's 128+signal with the signal in JSON, and every + // test record carries duration_ms. + s::needs({"unix-shell"}); + s::project p{{"mcpp.toml", manifest("sigdur")}, + {"tests/crash.cpp", "int main() { int* p = nullptr; return *p; }\n"}, + {"tests/ok.cpp", "int main() { return 0; }\n"}}; + auto r = p.mcpp({"test", "--message-format", "json"}); + expect(e2e::exited(r, 1)); + expect(e2e::contains(r.out, R"("test":"crash","status":"run_fail","exit_code":139,"signal":11)")); + expect(e2e::contains(r.out, R"("test":"ok","status":"pass")")); + expect(eq(s::count_lines(r.out, R"("duration_ms":)"), 2u)); + // The human report uses the shell's code too; its line carries the wall time. + expect(e2e::contains(s::text(p.mcpp({"test", "crash"})), "crash ... FAIL (exit 139,")); + }; + + "160 test timeout"_test[tag{"requires:unix-shell"}] = [] { + // `mcpp test --timeout `: a hung test is killed and reported; the rest still runs. + s::needs({"unix-shell"}); + s::project p{{"mcpp.toml", manifest("hang")}, + {"tests/sleepy.cpp", + "import std;\nint main() { std::this_thread::sleep_for(std::chrono::seconds(30)); return 0; }\n"}, + {"tests/quick.cpp", "int main() { return 0; }\n"}}; + const auto start = std::chrono::steady_clock::now(); + auto r = p.mcpp({"test", "--timeout", "2"}); + const auto elapsed = std::chrono::steady_clock::now() - start; + expect(e2e::exited(r, 1)); + expect(elapsed < std::chrono::seconds{25}) << "the timeout did not take effect"; + const auto out = s::text(r); + expect(e2e::contains(out, "sleepy ... FAIL (timeout")); + expect(e2e::contains(out, "quick ... ok")); + auto j = p.mcpp({"test", "--timeout", "2", "--message-format", "json"}); + expect(e2e::contains(j.out, R"("test":"sleepy","status":"run_fail")")); + expect(e2e::contains(j.out, R"("timed_out":true)")); + }; +}}; + +} // namespace diff --git a/tests/e2e/test/selection.cpp b/tests/e2e/test/selection.cpp new file mode 100644 index 000000000..4aa514c59 --- /dev/null +++ b/tests/e2e/test/selection.cpp @@ -0,0 +1,123 @@ +// Which tests `mcpp test` finds and runs: names from paths, a pattern, the listing, `[test] +// discover` (formerly tests/e2e/152, 154, 159 and 673). +import std; +import mcpplibs.mtest; +import mcpp.test.support; + +using namespace mcpplibs::mtest; +namespace s = mcpp::test::support; + +namespace { + +std::string manifest(std::string_view name) { + return std::format("[package]\nname = \"{}\"\nversion = \"0.1.0\"\nstandard = \"c++23\"\n", name); +} + +E2E selection{"selection", [] { + "152 test subdir names"_test = [] { + // The same stem in two directories of tests/ coexists; a test is named by its path. + s::project p{{"mcpp.toml", manifest("subdirs")}, + {"tests/00-a/0.cpp", "int main() { return 0; }\n"}, + {"tests/01-b/0.cpp", "int main() { return 0; }\n"}}; + auto r = p.mcpp({"test"}); + expect(fatal(e2e::succeeded(r))); + const auto out = s::text(r); + expect(e2e::contains(out, "00-a/0 ... ok")); + expect(e2e::contains(out, "01-b/0 ... ok")); + expect(e2e::contains(out, "2 passed")); + }; + + "154 test filter"_test = [] { + // `mcpp test `: a substring of the path-based names. + s::project p{{"mcpp.toml", manifest("filter")}, + {"tests/00-a/0.cpp", "int main() { return 0; }\n"}, + {"tests/01-b/0.cpp", "int main() { return 0; }\n"}}; + auto r = p.mcpp({"test", "00-a"}); + expect(fatal(e2e::succeeded(r))); + const auto out = s::text(r); + expect(e2e::contains(out, "00-a/0 ... ok")); + expect(s::lacks(out, "01-b/0")) << "the filter let another test through"; + expect(e2e::contains(out, "1 passed")); + auto none = p.mcpp({"test", "does-not-exist"}); + expect(e2e::exited(none, 2)); + expect(e2e::contains(s::text(none), "no tests match")); + }; + + "159 test list"_test = [] { + // `mcpp test --list` names the (selected) tests and builds nothing; a file that does not + // parse is listed too. + s::project p{{"mcpp.toml", manifest("listing")}, + {"tests/00-a/0.cpp", "int main() { return 0; }\n"}, + {"tests/01-b/0.cpp", "this does not even parse\n"}}; + auto r = p.mcpp({"test", "--list"}); + expect(fatal(e2e::succeeded(r))); + const auto out = s::text(r); + expect(e2e::contains(out, "00-a/0")); + expect(e2e::contains(out, "01-b/0")); + expect(!p.exists("target")) << "--list built (target/ was created)"; + auto j = p.mcpp({"test", "--list", "00-a", "--message-format", "json"}); + expect(s::has_line(j.out, R"x("test":"00-a/0","main":".*tests/00-a/0\.cpp")x")); + // The script's `grep -qv` held for any line without the record; what it meant is that the + // filter keeps 01-b/0 out of the listing. + expect(s::lacks(j.out, R"("test":"01-b/0")")) << "the filter leaked into the list"; + expect(e2e::contains(j.out, R"("summary":{"total":1})")); + }; + + "673 test discover names where tests are"_test = [] { + // `[test] discover` takes globs in the vocabulary of `[build] sources`; a test is named by + // its path below the fixed directory of the glob that found it. + auto p = s::project::scaffold("disc"); + for (const auto& f : s::find_files(p.at("tests"), R"(.*\.cpp)")) + std::filesystem::remove(f); + p.write("tests/a_ok.cpp", "int main() { return 0; }\n"); + p.write("tests/unit/b_ok.cpp", "int main() { return 0; }\n"); + p.write("tests/fails.cpp", "int main() { return 1; }\n"); + p.write("checks/c_ok.cpp", "int main() { return 0; }\n"); + p.write("checks/deep/skipped.cpp", "int main() { return 1; }\n"); + const auto base = p.read("mcpp.toml"); + + // 1. Without the key, the names are those of every earlier release. + auto l1 = p.mcpp({"test", "--list"}); + expect(fatal(e2e::succeeded(l1))); + expect(s::has_line(s::text(l1), "^a_ok$")) << "tests/a_ok.cpp is not named 'a_ok'"; + expect(s::has_line(s::text(l1), "^unit/b_ok$")) << "tests/unit/b_ok.cpp is not named 'unit/b_ok'"; + expect(s::no_line(s::text(l1), "^c_ok$")) << "the default discovered a file outside tests/"; + + // 2 and 3. Elsewhere, with an exclusion. + p.write("mcpp.toml", base + "\n[test]\ndiscover = [\"checks/**/*.cpp\", \"!checks/deep/**\"]\n"); + auto l2 = p.mcpp({"test", "--list"}); + expect(fatal(e2e::succeeded(l2))); + expect(s::has_line(s::text(l2), "^c_ok$")); + expect(s::no_line(s::text(l2), "fails|a_ok")) << "discover still discovered tests/"; + expect(s::lacks(s::text(l2), "skipped")) << "the ! glob did not exclude checks/deep/skipped.cpp"; + auto t2 = p.mcpp({"test"}); + expect(e2e::succeeded(t2)) << "a file outside the set ran?"; + expect(e2e::contains(s::text(t2), "c_ok ... ok")); + expect(s::lacks(s::text(t2), "fails")) << "tests/fails.cpp ran"; + + // 4. An empty list discovers nothing, and says where it looked. + p.write("mcpp.toml", base + "\n[test]\ndiscover = []\n"); + auto t4 = p.mcpp({"test"}); + expect(e2e::succeeded(t4)); + expect(e2e::contains(s::text(t4), "no tests found ([test] discover = [])")); + + // 5. Two files that map to one name are refused, naming both. + p.write("mcpp.toml", base + "\n[test]\ndiscover = [\"tests/*.cpp\", \"checks/*.cpp\"]\n"); + p.write("checks/a_ok.cpp", "int main() { return 0; }\n"); + auto t5 = p.mcpp({"test", "--list"}); + expect(t5.started && t5.status != 0) << "two files mapping to 'a_ok' were accepted"; + expect(e2e::contains(s::text(t5), "duplicate test name 'a_ok'")); + expect(e2e::contains(s::text(t5), "tests/a_ok.cpp")); + expect(e2e::contains(s::text(t5), "checks/a_ok.cpp")); + p.remove("checks/a_ok.cpp"); + + // 6. A value that is not an array of strings is refused, naming the key (`mcpp test`: the + // listing is best effort over a manifest that does not load). + p.write("mcpp.toml", base + "\n[test]\ndiscover = \"tests/**/*.cpp\"\n"); + auto t6 = p.mcpp({"test"}); + expect(t6.started && t6.status != 0) << "a string discover was accepted"; + expect(e2e::contains(s::text(t6), "[test] discover must be an array")); + }; +}}; + +} // namespace diff --git a/tests/e2e/test/skeleton.cpp b/tests/e2e/test/skeleton.cpp new file mode 100644 index 000000000..d429880e3 --- /dev/null +++ b/tests/e2e/test/skeleton.cpp @@ -0,0 +1,199 @@ +// `mcpp new`'s test skeleton, and where a test that defines no `main` begins (formerly +// tests/e2e/1037). +import std; +import mcpplibs.mtest; +import mcpp.test.support; + +using namespace mcpplibs::mtest; +namespace s = mcpp::test::support; +namespace fs = std::filesystem; +using nlohmann::json; + +namespace { + +// The lines that follow (up to `after`) a line containing `anchor` (`grep -A`). +std::string following(std::string_view text, std::string_view anchor, std::size_t after) { + std::vector lines = s::lines_with(text, ""); + std::string out; + for (std::size_t i = 0; i < lines.size(); ++i) + if (lines[i].contains(anchor)) + for (std::size_t j = i; j < lines.size() && j <= i + after; ++j) out += lines[j] + "\n"; + return out; +} + +E2E skeleton{"skeleton", [] { + + "1037 a skeleton tests with mtest and a test needs no main"_test[tag{"requires:gcc"}] = [] { + // `mcpp new` scaffolds a program or a library whose test is mtest's, and a test file with no + // `main` begins at the entry a dependency offers. + s::needs({"gcc"}); + s::project t(temp_dir() / "t", {}); + auto in = [&](std::string_view sub, std::vector args) { return t.mcpp_in(sub, std::move(args)); }; + + // A + auto a = t.mcpp({"new", "acme.my-lib", "--template", "lib"}); + expect(fatal(e2e::succeeded(a))); + for (auto f : {"mcpp.toml", "src/my-lib.cppm", "src/hello.cppm", "src/hello.cpp", "tests/hello.cpp"}) + expect(fatal(t.exists(std::format("acme.my-lib/{}", f)))) << "no " << f; + expect(s::has_line(t.read("acme.my-lib/src/my-lib.cppm"), "^export module acme.my_lib;")); + expect(s::has_line(t.read("acme.my-lib/src/hello.cppm"), "^export module acme.my_lib:hello;")); + expect(e2e::contains(t.read("acme.my-lib/src/hello.cpp"), "namespace acme::my_lib")); + expect(s::lacks(t.read("acme.my-lib/tests/hello.cpp"), "int main")) << "the test defines main"; + expect(e2e::succeeded(in("acme.my-lib", {"build"}))) << "the library does not build"; + auto at = in("acme.my-lib", {"test"}); + expect(e2e::succeeded(at)); + expect(s::has_line(s::text(at), R"(hello \.\.\. ok \(.*1 case\))")) << "the test is not one protocol case"; + // B + expect(fatal(e2e::succeeded(t.mcpp({"new", "prog"})))); + auto bt = in("prog", {"test"}); + expect(e2e::succeeded(bt)); + expect(s::has_line(s::text(bt), R"(test_smoke \.\.\. ok \(.*1 case\))")); + // C: a framework whose entry offers to begin tests. + t.write("fw/mcpp.toml", R"([package] +name = "fw" +version = "0.1.0" + +[targets.main] +kind = "entry" +main = "src/entry.cpp" +test-main = true +)"); + t.write("fw/src/entry.cpp", "import std;\nextern \"C++\" int run_cases();\nint main() { std::println(\"fw main\"); return run_cases(); }\n"); + t.write("fw/tests/own.cpp", "extern \"C++\" int run_cases() { return 0; }\n"); + t.write("app/mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dev-dependencies]\nfw = { path = \"../fw\" }\n"); + t.write("app/tests/begun.cpp", "extern \"C++\" int run_cases() { return 0; }\n"); + t.write("app/tests/own.cpp", "import std;\nint main() { std::println(\"own main\"); return 0; }\n"); + auto c = in("app", {"test"}); + expect(e2e::succeeded(c)) << "the dependent's tests"; + expect(e2e::contains(s::text(c), "begun ... ok")); + expect(e2e::contains(s::text(c), "own ... ok")); + expect(e2e::succeeded(in("fw", {"test"}))) << "the framework's own test"; + const auto app_c = t.read("app/mcpp.toml"); + t.write("app/mcpp.toml", app_c + "\n[dependencies]\nfw = { path = \"../fw\" }\n"); + expect(e2e::succeeded(in("app", {"test"}))) << "a framework named in two tables offered twice"; + t.write("app/mcpp.toml", app_c); + // D: a second framework offers too. + fs::copy(t.at("fw"), t.at("fw2"), fs::copy_options::recursive); + t.write("fw2/mcpp.toml", std::regex_replace(t.read("fw2/mcpp.toml"), std::regex("name = \"fw\""), "name = \"fw2\"")); + t.write("app/mcpp.toml", t.read("app/mcpp.toml") + "fw2 = { path = \"../fw2\" }\n"); + auto d = in("app", {"test"}); + expect(d.started && d.status != 0) << "two offers were taken"; + expect(e2e::contains(s::text(d), "fw/main")); + expect(e2e::contains(s::text(d), "fw2/main")); + t.remove("app/tests/own.cpp"); // under [test] main every test begins at the entry + t.write("app/mcpp.toml", t.read("app/mcpp.toml") + "\n[test]\nmain = { from = \"fw2/main\" }\n"); + expect(e2e::succeeded(in("app", {"test"}))) << "[test] main did not choose"; + // E + auto e = t.mcpp({"new", "3d", "--template", "lib"}); + expect(e.started && e.status != 0) << "a name beginning with a digit was taken"; + expect(e2e::contains(s::text(e), "does not begin with a letter")); + // F + t.write("fwm/mcpp.toml", R"([package] +name = "fwm" +version = "0.1.0" + +[targets.main] +kind = "entry" +main = "src/entry.cpp" +test-main = true +)"); + t.write("fwm/src/fwm.cppm", "export module fwm;\nexport int fwm_cases() { return 0; }\n"); + t.write("fwm/src/entry.cpp", "import std;\nimport fwm;\nint main() { std::println(\"began at fwm\"); return fwm_cases(); }\n"); + t.write("hdr/mcpp.toml", "[package]\nname = \"hdr\"\nversion = \"0.1.0\"\n"); + t.write("hdr/src/hdr_main.cpp", "import std;\nint main() { std::println(\"began at hdr\"); return 0; }\n"); + t.write("hdr/src/hdr.cpp", "int hdr_marker() { return 0; }\n"); + t.write("mixed/mcpp.toml", "[package]\nname = \"mixed\"\nversion = \"0.1.0\"\n\n[dev-dependencies]\nfwm = { path = \"../fwm\" }\nhdr = { path = \"../hdr\" }\n"); + t.write("mixed/tests/modular.cpp", "import fwm;\nint uses_fwm() { return fwm_cases(); }\n"); + t.write("mixed/tests/headers.cpp", "int uses_headers() { return 0; }\n"); + auto f = in("mixed", {"test"}); + expect(fatal(e2e::succeeded(f))); + expect(e2e::contains(following(s::text(f), "modular ... ok", 2), "began at fwm")); + expect(e2e::contains(following(s::text(f), "headers ... ok", 2), "began at hdr")); + // G + t.write("fwm/mcpp.toml", t.read("fwm/mcpp.toml") + "test-guide = \"GUIDE.md\"\n"); + t.write("fwm/GUIDE.md", "# Writing a test with fwm\n"); + auto g = in("mixed", {"test", "--guide"}); + expect(e2e::succeeded(g)); + expect(e2e::contains(s::text(g), "fwm 0.1.0")); + expect(e2e::contains(s::text(g), "fwm/GUIDE.md")); + auto gj = in("mixed", {"test", "--guide", "--message-format", "json"}); + expect(e2e::succeeded(gj)); + expect(e2e::matches(gj.out, R"("guide":"[^"]*fwm/GUIDE.md")")); + t.write("mixed/tests/modular.cpp", "import fwm;\nint uses_fwm() { return fwm_cases(); }\nint broken = missing_name;\n"); + auto gf = in("mixed", {"test"}); + expect(gf.started && gf.status != 0) << "a test that does not compile passed"; + expect(e2e::contains(s::text(gf), "how these tests are written: mcpp test --guide")); + // H + t.write("fwh/mcpp.toml", R"([package] +name = "fwh" +version = "0.1.0" + +[targets.main] +kind = "entry" +main = "src/entry.cpp" +test-main = true + +[dependencies] +hdr = { path = "../hdr" } +)"); + t.write("fwh/src/entry.cpp", "import std;\nint main() { std::println(\"began at fwh\"); return 0; }\n"); + t.write("fwh/src/fwh.cpp", "int fwh_marker() { return 0; }\n"); + t.write("deep/mcpp.toml", "[package]\nname = \"deep\"\nversion = \"0.1.0\"\n\n[dev-dependencies]\nfwh = { path = \"../fwh\" }\n"); + t.write("deep/tests/one.cpp", "int uses_fwh() { return 0; }\n"); + auto h = in("deep", {"test"}); + expect(e2e::succeeded(h)) << "a main beneath the dev-dependency was linked"; + expect(e2e::contains(s::text(h), "began at fwh")); + // I + t.write("shlib/mcpp.toml", "[package]\nname = \"shlib\"\nversion = \"0.1.0\"\n\n[targets.shlib]\nkind = \"shared\"\n"); + t.write("shlib/src/shlib.cpp", "extern \"C\" int shlib_answer() { return 42; }\n"); + t.write("shapp/mcpp.toml", "[package]\nname = \"shapp\"\nversion = \"0.1.0\"\n\n[dev-dependencies]\nshlib = { path = \"../shlib\" }\n"); + t.write("shapp/tests/links.cpp", "extern \"C\" int shlib_answer();\nint main() { return shlib_answer() == 42 ? 0 : 1; }\n"); + expect(e2e::succeeded(in("shapp", {"test"}))) << "a shared dev-dependency is not linked"; + // J + t.write("wsj/mcpp.toml", "[workspace]\nmembers = [\"one\", \"two\"]\n"); + t.write("wsj/one/mcpp.toml", "[package]\nname = \"one\"\nversion = \"0.1.0\"\n\n[dev-dependencies]\nhdr = { path = \"../../hdr\" }\n"); + t.write("wsj/one/tests/plain.cpp", "int uses_headers() { return 0; }\n"); + t.write("wsj/two/mcpp.toml", "[package]\nname = \"two\"\nversion = \"0.1.0\"\n\n[dev-dependencies]\nfw = { path = \"../../fw\" }\n"); + t.write("wsj/two/tests/begun.cpp", "extern \"C++\" int run_cases() { return 0; }\n"); + auto j = in("wsj", {"test", "--workspace"}); + expect(e2e::succeeded(j)) << "a member's test took another member's beginning"; + expect(e2e::contains(s::text(j), "began at hdr")); + expect(e2e::contains(s::text(j), "fw main")); + // K + expect(fatal(e2e::succeeded(t.mcpp({"new", "kapp"})))); + for (auto cmd : {"build", "test", "build", "test"}) + expect(fatal(e2e::succeeded(in("kapp", {cmd})))); + const auto graphs = s::glob(t.at("kapp/target"), "*/*/build.ninja"); + expect(fatal(eq(graphs.size(), 1u))) << "build and test are several configurations"; + const auto dir = graphs.front().parent_path(); + const auto main_edges = std::ranges::count_if(s::ninja_log_outputs(dir), [](const std::string& o) { + return std::regex_search(o, std::regex(R"(obj/main\.(o|obj)$)")); + }); + expect(eq(main_edges, 1)) << "the program's source was compiled again"; + const auto cdb_text = s::read_file(dir / "compile_commands.json"); + expect(e2e::contains(cdb_text, "test_smoke")) << "the compile database has no test"; + for (const auto& entry : s::json(cdb_text)) { + auto file = entry.value("file", ""); + std::ranges::replace(file, '\\', '/'); + if (!file.ends_with("kapp/src/main.cpp") || !entry.contains("arguments")) continue; + for (const auto& arg : entry["arguments"]) + expect(!e2e::contains(arg.get(), "__OPENKAL__") && !e2e::contains(arg.get(), "openkal")) + << "the program's source sees the dev-dependency: " << arg.get(); + } + // L + t.write("lib-tools/mcpp.toml", "[package]\nname = \"lib-tools\"\nversion = \"0.1.0\"\n\n[build]\nsources = [\"src/*.c\"]\n"); + t.write("lib-tools/src/table.c", R"(int table_value(void) { return 7; } +#ifdef MAKE_TABLE +#include +int main(void) { printf("%d\n", table_value()); return 0; } +#endif +)"); + t.write("lapp/mcpp.toml", "[package]\nname = \"lapp\"\nversion = \"0.1.0\"\n\n[dev-dependencies]\nlib-tools = { path = \"../lib-tools\" }\n"); + t.write("lapp/tests/own.cpp", "extern \"C\" int table_value(void);\nint main() { return table_value() == 7 ? 0 : 1; }\n"); + auto l = in("lapp", {"test"}); + expect(e2e::succeeded(l)) << "a source whose main no command compiles was left out of the link"; + expect(e2e::contains(s::text(l), "own ... ok")); + }; +}}; + +} // namespace diff --git a/tests/e2e/test/streams.cpp b/tests/e2e/test/streams.cpp new file mode 100644 index 000000000..46041251a --- /dev/null +++ b/tests/e2e/test/streams.cpp @@ -0,0 +1,137 @@ +// What `mcpp test` writes where and when: steps on standard error, results on standard output, +// streamed, timed and bounded (formerly tests/e2e/865 and 178). +import std; +import mcpplibs.mtest; +import mcpp.test.support; + +using namespace mcpplibs::mtest; +namespace s = mcpp::test::support; + +namespace { + +constexpr std::string_view hang_cpp = + "#include \n#include \nint main() { std::this_thread::sleep_for(std::chrono::seconds(30)); return 0; }\n"; + +E2E streams{"streams", [] { + "865 mcpp test reports its steps on stderr and its results on stdout"_test[tag{"requires:unix-shell"}] = [] { + s::needs({"unix-shell"}); + s::project p{{"mcpp.toml", R"([package] +name = "p" +version = "0.1.0" + +[targets.p] +kind = "bin" +main = "src/main.cpp" +)"}, + {"src/main.cpp", "int main() { return 0; }\n"}, + {"tests/t_ok.cpp", "#include \nint main() { std::puts(\"output of the passing test\"); return 0; }\n"}}; + // A: a run whose tests pass. + auto a = p.mcpp({"test"}); + expect(fatal(e2e::succeeded(a))); + expect(e2e::contains(a.out, "t_ok ... ok")) << "the verdict is not on standard output"; + expect(s::has_line(a.out, R"(test result ok\.)")); + expect(s::no_line(a.out, "Compiling|Resolving|Resolved")) << "a step is on standard output"; + expect(e2e::contains(a.err, "Compiling")) << "the steps are not on standard error"; + expect(s::lacks(a.err, "test result")) << "the test result line is on standard error"; + expect(s::lacks(a.err, "... ok")) << "a verdict is on standard error"; + // B: a failing test exits 1. + p.write("tests/t_bad.cpp", "#include \nint main() { std::puts(\"output of the failing test\"); return 1; }\n"); + auto b = p.mcpp({"test"}); + expect(e2e::exited(b, 1)); + expect(e2e::contains(b.out, "t_bad ... FAIL")); + expect(e2e::contains(b.out, "output of the failing test")); + expect(e2e::contains(b.out, "failures:")); + expect(e2e::contains(b.err, "test result: FAILED")) << "the failed summary is not on standard error"; + expect(s::no_line(b.out, "Compiling|Resolving")) << "a step is on standard output"; + p.remove("tests/t_bad.cpp"); + // C: --list names on standard output alone. + auto c = p.mcpp({"test", "--list"}); + expect(fatal(e2e::succeeded(c))); + expect(s::has_line(c.out, "^t_ok$")); + expect(s::lacks(c.err, "t_ok")) << "a name is on standard error"; + // D: a workspace. + s::project ws(temp_dir() / "ws", {{"mcpp.toml", "[workspace]\nmembers = [\"a\", \"b\"]\n"}}); + for (auto m : {"a", "b"}) { + ws.write(std::format("{}/mcpp.toml", m), + std::format("[package]\nname = \"{}\"\nversion = \"0.1.0\"\n\n[targets.{}]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n", m, m)); + ws.write(std::format("{}/src/main.cpp", m), "int main() { return 0; }\n"); + ws.write(std::format("{}/tests/t_{}.cpp", m, m), "int main() { return 0; }\n"); + } + auto d = ws.mcpp({"test", "--workspace"}); + expect(fatal(e2e::succeeded(d))); + expect(s::has_line(d.out, R"(workspace result ok\.)")); + expect(s::has_line(d.out, R"(test result ok\.)")); + expect(s::no_line(d.out, "Compiling|Resolving|Resolved|Workspace")) << "a step is on standard output"; + expect(e2e::contains(d.err, "Compiling")); + expect(s::lacks(d.err, "workspace result")) << "the workspace result line is on standard error"; + }; + + "178 test observability"_test[tag{"requires:unix-shell"}] = [] { + // `mcpp test` is streamed, timed and bounded. POSIX only: the deadline runner has no + // kill-by-handle path on Windows, so --timeout and --build-timeout are documented POSIX-only. + s::needs({"unix-shell"}); + s::project ws{{"mcpp.toml", "[workspace]\nmembers = [\"fast\", \"slow\"]\n"}, + {"fast/mcpp.toml", "[package]\nname = \"fast\"\nversion = \"0.1.0\"\n"}, + {"slow/mcpp.toml", "[package]\nname = \"slow\"\nversion = \"0.1.0\"\n"}, + {"fast/tests/quick.cpp", "int main() { return 0; }\n"}, + {"slow/tests/hang.cpp", hang_cpp}}; + + // 1. Streaming: the fast member's verdict reaches a pipe while the slow one still runs. + { + auto session = e2e::session::start(s::mcpp_command({"test", "--workspace", "--timeout", "25"}, ws.dir())); + expect(fatal(session.started())); + expect(session.saw(R"(quick \.\.\. ok)", std::chrono::seconds{25})) + << "no output reached the pipe before the process ended (stdout not flushed)"; + } // the session ends mcpp + + // 2. Timings: a test's duration and the build/run split. + auto t2 = ws.mcpp({"test", "-p", "fast"}); + expect(fatal(e2e::succeeded(t2))); + expect(s::has_line(s::text(t2), R"(quick \.\.\. ok \([0-9]+\.[0-9]+s\))")) << "the test's line carries no duration"; + expect(s::has_line(s::text(t2), R"(finished in [0-9]+\.[0-9]+s \(build [0-9]+\.[0-9]+s \+ run [0-9]+\.[0-9]+s\))")) + << "the summary carries no build/run split"; + + // 3. --timeout bounds a hung run, and the fan-out continues. + auto t3 = ws.mcpp({"test", "--workspace", "--timeout", "2"}); + expect(e2e::contains(s::text(t3), "hang ... FAIL (timeout after 2s)")); + expect(s::has_line(s::text(t3), R"(member 'fast' \(1/2\) ok)")) << "no per-member progress line"; + expect(e2e::contains(s::text(t3), "failed members: slow")); + expect(t3.started && t3.status != 0) << "the workspace test returned 0 despite a failed member"; + // `--timeout 0` means no limit. + expect(e2e::succeeded(ws.mcpp({"test", "-p", "fast", "--timeout", "0"}))) << "--timeout 0 was refused"; + + // 4. --build-timeout bounds a slow compile and names itself: 20k distinct class templates. + std::string heavy; + for (int i = 0; i < 20000; ++i) { + heavy += std::format("template struct S{} {{ static constexpr long v = A*{}+1; }};\n", i, i); + heavy += std::format("long f{}(long x) {{ return x + S{}<{}>::v; }}\n", i, i, i % 97); + } + heavy += "int main() { return 0; }\n"; + ws.write("heavy/mcpp.toml", "[package]\nname = \"heavy\"\nversion = \"0.1.0\"\n"); + ws.write("heavy/tests/slowbuild.cpp", heavy); + expect(e2e::contains(s::text(ws.mcpp_in("heavy", {"test", "--build-timeout", "1"})), "build timeout after 1s")) + << "--build-timeout did not stop the slow compile"; + // The deadlines share their argument's validation. + expect(e2e::contains(s::text(ws.mcpp({"test", "-p", "fast", "--build-timeout", "abc"})), "invalid --build-timeout")); + + // 5. --workspace-timeout stops the fan-out and names what did not run (the slow member first). + ws.write("wst/mcpp.toml", "[workspace]\nmembers = [\"one\", \"two\"]\n"); + ws.write("wst/one/mcpp.toml", "[package]\nname = \"wst_one\"\nversion = \"0.1.0\"\n"); + ws.write("wst/two/mcpp.toml", "[package]\nname = \"wst_two\"\nversion = \"0.1.0\"\n"); + ws.write("wst/one/tests/hang.cpp", hang_cpp); + ws.write("wst/two/tests/quick.cpp", "int main() { return 0; }\n"); + auto t5 = ws.mcpp_in("wst", {"test", "--workspace", "--timeout", "3", "--workspace-timeout", "1"}); + expect(s::lacks(s::text(t5), "member 'two'")) << "--workspace-timeout did not stop the fan-out"; + expect(e2e::contains(s::text(t5), "not run (--workspace-timeout 1s reached)")); + expect(t5.started && t5.status != 0) << "the workspace timeout returned 0"; + + // 6. The JSON stream: no stray line, member-qualified, a workspace summary. + auto t6 = ws.mcpp({"test", "--workspace", "--timeout", "2", "--message-format", "json"}); + expect(t6.out.starts_with("{")) << "the first line is not JSON (stdout polluted)"; + expect(e2e::contains(t6.out, R"("member":"fast")")); + expect(e2e::contains(t6.out, R"("workspace_summary")")); + expect(e2e::contains(t6.out, R"("failed_members":["slow"])")); + }; +}}; + +} // namespace diff --git a/tests/e2e/test/workspace.cpp b/tests/e2e/test/workspace.cpp new file mode 100644 index 000000000..13f8ab130 --- /dev/null +++ b/tests/e2e/test/workspace.cpp @@ -0,0 +1,383 @@ +// `mcpp test` over a workspace's members: selection, one plan per configuration group, the report +// of a shared build, a failing member, a member's programs (formerly tests/e2e/90, 854, 855, 856 +// and 1034). +import std; +import mcpplibs.mtest; +import mcpp.test.support; + +using namespace mcpplibs::mtest; +namespace s = mcpp::test::support; +namespace fs = std::filesystem; + +namespace { + +// The directories holding a graph (build.ninja, test.ninja or both), outside the build programs'. +std::vector graph_dirs(const s::project& p) { + std::set dirs; + for (const auto& f : s::find_files(p.at("target"), "(build|test)\\.ninja")) + if (!f.generic_string().contains("/.build-mcpp/")) + dirs.insert(f.parent_path()); + return {dirs.begin(), dirs.end()}; +} + +// The value of `"key":` in a record line. +std::optional number(std::string_view line, std::string_view key) { + std::match_results m; + const std::regex re{std::format("\"{}\":([0-9]+)", key)}; + if (!std::regex_search(line.begin(), line.end(), m, re)) + return std::nullopt; + return std::stol(m[1].str()); +} + +// A workspace of libraries, each with one test `test_` importing its module `sel_`. +s::project libraries(const fs::path& dir, std::string_view tag, std::initializer_list members) { + std::string list; + for (auto m : members) list += std::format("{}\"{}\"", list.empty() ? "" : ", ", m); + s::project ws(dir, {}); + ws.write("mcpp.toml", std::format("[workspace]\nmembers = [{}]\n", list)); + for (auto m : members) { + ws.write(std::format("{}/mcpp.toml", m), + std::format("[package]\nname = \"{}\"\nversion = \"0.1.0\"\n\n[targets.{}]\nkind = \"lib\"\n", m, m)); + ws.write(std::format("{}/src/{}.cppm", m, m), + std::format("export module sel{}_{};\nexport int {}_v() {{ return 1; }}\n", tag, m, m)); + ws.write(std::format("{}/tests/test_{}.cpp", m, m), + std::format("import sel{}_{};\nint main() {{ return {}_v() == 1 ? 0 : 1; }}\n", tag, m, m)); + } + return ws; +} + +E2E workspace{"workspace", [] { + "90 workspace test"_test = [] { + // `-p `, `--workspace` and a bare `mcpp test` at a virtual root; discovery is scoped + // to a member, so two members may each have tests/main.cpp. (The script's `requires: gcc` + // stood on line 8, which run_all.sh does not read: it ran on every host, and so does this.) + s::project ws{{"mcpp.toml", "[workspace]\nmembers = [\"liba\", \"libb\"]\n"}, + {"liba/tests/main.cpp", "int main() { return (1 + 1 == 2) ? 0 : 1; }\n"}, + {"libb/tests/main.cpp", "int main() { return (2 * 2 == 4) ? 0 : 1; }\n"}}; + for (auto l : {"liba", "libb"}) { + ws.write(std::format("{}/mcpp.toml", l), + std::format("[package]\nname = \"{}\"\nversion = \"0.1.0\"\n\n[targets.{}]\nkind = \"lib\"\n", l, l)); + ws.write(std::format("{}/src/{}.cppm", l, l), std::format("export module {};\n", l)); + } + // 1. -p selects one member. + auto r1 = ws.mcpp({"test", "-p", "liba"}); + expect(fatal(e2e::succeeded(r1))); + expect(e2e::contains(s::text(r1), "1 passed")); + expect(!s::contains_nocase(s::text(r1), "testing member 'libb'")) << "-p liba also ran libb"; + // 2. --workspace runs both, with no duplicate-stem error. + auto r2 = ws.mcpp({"test", "--workspace"}); + expect(fatal(e2e::succeeded(r2))); + expect(!s::contains_nocase(s::text(r2), "duplicate test name")) << "the duplicate-stem bug"; + expect(s::contains_nocase(s::text(r2), "testing member 'liba'")); + expect(s::contains_nocase(s::text(r2), "testing member 'libb'")); + // 3. A bare `mcpp test` at the virtual root fans out the same way. + auto r3 = ws.mcpp({"test"}); + expect(fatal(e2e::succeeded(r3))); + expect(!s::contains_nocase(s::text(r3), "duplicate test name")); + expect(s::contains_nocase(s::text(r3), "testing member 'libb'")); + // 4. A failing member fails the run, and the other still runs; the summary carries the + // counts and, on a line of its own, the names. + ws.write("libb/tests/main.cpp", "int main(){return 1;}\n"); + auto r4 = ws.mcpp({"test", "--workspace"}); + expect(r4.started && r4.status != 0) << "the failing member did not fail the run"; + expect(s::contains_nocase(s::text(r4), "workspace test: 1/2 member(s) failed")); + expect(s::contains_nocase(s::text(r4), "failed members: libb")); + expect(s::contains_nocase(s::text(r4), "testing member 'liba'")); + }; + + "854 a test over several members plans and builds once"_test[tag{"requires:unix-shell"}] = [] { + // `test --workspace` plans once per configuration group: a member two members use runs its + // build program once, with the union of the features asked of it. + s::needs({"unix-shell"}); + s::project ws{{"mcpp.toml", "[workspace]\nmembers = [\"core\", \"a\", \"b\"]\n"}, + {"core/mcpp.toml", R"([package] +name = "core" +version = "0.1.0" + +[features] +extra = [] + +[targets.core] +kind = "lib" +)"}, + {"core/src/core.cppm", R"(export module sel854_core; +export int core_v() { return 1; } +export int core_extra() { +#ifdef MCPP_FEATURE_EXTRA + return 1; +#else + return 0; +#endif +} +)"}, + {"core/build.mcpp", R"(import std; +import mcpp; +int main() { + std::ofstream(std::string(mcpp::out_dir()) + "/runs.log", std::ios::app) << "ran\n"; + return 0; +} +)"}, + {"a/mcpp.toml", R"([package] +name = "a" +version = "0.1.0" + +[dependencies] +core = { path = "../core", features = ["extra"] } + +[targets.a] +kind = "lib" +)"}, + {"b/mcpp.toml", R"([package] +name = "b" +version = "0.1.0" + +[dependencies] +core = { path = "../core" } + +[targets.b] +kind = "lib" +)"}}; + for (auto m : {"a", "b"}) { + ws.write(std::format("{}/src/{}.cppm", m, m), + std::format("export module sel854_{};\nimport sel854_core;\nexport int {}_v() {{ return core_v(); }}\n", m, m)); + // Both members name their test `main`: discovery is scoped to each member. + ws.write(std::format("{}/tests/main.cpp", m), + std::format("import sel854_{};\nint main() {{ return {}_v() == 1 ? 0 : 1; }}\n", m, m)); + } + auto runs = [&] { + std::size_t n = 0; + for (const auto& f : s::find_files(ws.dir(), "runs\\.log")) n += s::count_lines(s::read_file(f), "ran"); + return n; + }; + auto core_edges = [&](const fs::path& dir) { + return std::ranges::count_if(s::ninja_log_outputs(dir), [](const std::string& o) { + return std::regex_search(o, std::regex(R"((^|/)obj/core/src/core\.m\.o$)")); + }); + }; + auto reset = [&] { for (auto d : {"target", "core/target", "a/target", "b/target"}) ws.remove(d); }; + + // F + reset(); + auto f = ws.mcpp({"test", "--workspace"}); + expect(fatal(e2e::succeeded(f))); + const auto fl = s::text(f); + expect(eq(runs(), 1u)) << "core's build program ran more than once"; + expect(eq(s::count_lines(fl, "Workspace building"), 1u)) << "the members were planned more than once"; + expect(e2e::contains(fl, "Workspace building 2 members: a, b")); + expect(e2e::contains(fl, "member 'a' (2/3) ok — 1 passed")); + expect(e2e::contains(fl, "member 'b' (3/3) ok — 1 passed")); + expect(e2e::contains(fl, "workspace result ok. 3 member(s); 2 passed")); + auto dirs = graph_dirs(ws); + expect(fatal(eq(dirs.size(), 1u))) << "the members were built in more than one directory"; + expect(eq(core_edges(dirs.front()), 1)) << "core was compiled more than once"; + + // G + reset(); + expect(fatal(e2e::succeeded(ws.mcpp({"build", "--workspace"})))); + dirs = graph_dirs(ws); + expect(fatal(!dirs.empty())); + const auto dir = dirs.front(); + const auto before = core_edges(dir); + expect(before == 1 && runs() == 1) << "the build did not compile and run core once"; + auto g = ws.mcpp({"test", "--workspace"}); + expect(fatal(e2e::succeeded(g))); + expect(eq(graph_dirs(ws).front(), dir)) << "the test build used another directory"; + expect(eq(core_edges(dir), before)) << "test --workspace compiled core again"; + expect(eq(runs(), 1u)) << "test --workspace ran core's build program again"; + expect(e2e::contains(s::text(g), "workspace result ok. 3 member(s); 2 passed")); + }; + + "855 a shared test build is reported once per group"_test[tag{"requires:unix-shell"}] = [] { + // A build several members' tests share is reported once, by addition: a group_build + // record, a build_group on each member's summary, the members' build_ms its group's. + s::needs({"unix-shell"}); + auto ws = libraries(temp_dir() / "ws", "855", {"a", "b", "c"}); + + // H: the records, from stdout alone. + auto h = ws.mcpp({"test", "-p", "a", "-p", "b", "--message-format", "json"}); + expect(fatal(e2e::succeeded(h))); + std::vector lines; + for (const auto& l : s::lines_with(h.out, "")) + if (!l.empty()) lines.push_back(l); + expect(fatal(!lines.empty())); + for (const auto& l : lines) + expect(l.starts_with("{") && l.ends_with("}")) << "a line of the stream is not one JSON object: " << l; + expect(eq(s::count_lines(h.out, R"("group_build")"), 1u)); + const auto& first = lines.front(); + expect(first.starts_with(R"({"group_build":)")) << "group_build is not before the first test record"; + expect(e2e::contains(first, R"("group":0)")); + expect(e2e::contains(first, R"("members":["a","b"])")); + const auto gms = number(first, "build_ms"); + expect(fatal(gms.has_value())) << "group_build has no build_ms"; + for (auto m : {"a", "b"}) { + std::string summary; + for (const auto& l : lines) + if (l.contains(R"("summary")") && l.contains(std::format(R"("member":"{}")", m))) { summary = l; break; } + expect(fatal(!summary.empty())) << "no summary for " << m; + expect(e2e::contains(summary, R"("build_group":0)")); + expect(number(summary, "build_ms") == gms) << m << "'s build_ms is not its group's"; + expect(e2e::contains(summary, R"("elapsed_ms":)")); + expect(e2e::contains(summary, R"("run_ms":)")); + expect(e2e::contains(h.out, std::format(R"("member":"{}","test":"test_{}","status":"pass")", m, m))); + std::string rec; + for (const auto& l : lines) + if (l.contains(std::format(R"("member":"{}","test":)", m))) { rec = l; break; } + expect(number(rec, "duration_ms").has_value()) << "no duration_ms for " << m << "'s test"; + expect(number(rec, "build_ms").has_value()) << "no build_ms for " << m << "'s test"; + } + const auto& last = lines.back(); + expect(last.starts_with(R"({"workspace_summary":)")) << "the stream does not end with workspace_summary"; + expect(e2e::contains(last, R"("members":2,"passed":2,"failed":0)")); + + // J: a member no -p names is in no record, and is not built. + expect(s::lacks(h.out, R"("member":"c")")); + bool built_c = false; + for (const auto& f : s::find_files(ws.at("target"), ".*")) + if (f.generic_string().contains("/obj/c")) built_c = true; + expect(!built_c) << "c was built"; + + // I: the human report states the group once, and each member's run. + auto i = ws.mcpp({"test", "-p", "a", "-p", "b"}); + expect(fatal(e2e::succeeded(i))); + const auto il = s::text(i); + std::size_t group_lines = 0; + for (const auto& l : s::lines_with(il, "built members")) + if (std::regex_search(l, std::regex(R"(Workspace +built members a, b in [0-9]+\.[0-9]+s)"))) ++group_lines; + expect(eq(group_lines, 1u)) << "the report does not have one line for the group"; + for (auto m : {"a", "b"}) + expect(s::has_line(il, std::format(R"(member '{}' \([12]/2\) ok — 1 passed, run [0-9]+\.[0-9]+s)", m))); + }; + + "856 a test over several members continues past a failing one"_test[tag{"requires:unix-shell"}] = [] { + // `mcpp test -p a -p b` tests both (mcpp#750), and a member that fails -- its test, its + // package's build or its plan -- fails alone. + s::needs({"unix-shell"}); + auto obj_c = [](const s::project& ws) { + for (const auto& f : s::find_files(ws.at("target"), ".*")) + if (f.generic_string().contains("/obj/c")) return true; + return false; + }; + // I + { + auto ws = libraries(temp_dir() / "w1", "856", {"a", "b", "c"}); + auto i1 = ws.mcpp({"test", "-p", "a", "-p", "b"}); + expect(fatal(e2e::succeeded(i1))); + const auto l = s::text(i1); + expect(e2e::contains(l, "test_a ... ok")); + expect(e2e::contains(l, "test_b ... ok")) << "the repeated -p kept one value"; + expect(s::lacks(l, "test_c")) << "c's test ran although no -p named it"; + expect(e2e::contains(l, "workspace result ok. 2 member(s); 2 passed; 0 failed")); + auto i2 = ws.mcpp({"test", "-p", "b", "-p", "a"}); + expect(e2e::succeeded(i2)); + expect(e2e::contains(s::text(i2), "workspace result ok. 2 member(s); 2 passed; 0 failed")); + auto i3 = ws.mcpp({"test", "-p", "a", "-p", "b", "--message-format", "json"}); + expect(e2e::succeeded(i3)); + expect(e2e::contains(i3.out, R"("workspace_summary":{"members":2,"passed":2,"failed":0)")); + expect(!obj_c(ws)) << "c was built"; + } + // K + { + auto ws = libraries(temp_dir() / "w2", "856", {"a", "b", "c"}); + ws.write("c/tests/test_c.cpp", "int main() { return 1; }\n"); + auto k = ws.mcpp({"test", "--workspace"}); + expect(e2e::exited(k, 1)); + const auto l = s::text(k); + expect(e2e::contains(l, "member 'a' (1/3) ok")); + expect(e2e::contains(l, "member 'b' (2/3) ok")); + expect(e2e::contains(l, "member 'c' (3/3) FAILED")); + expect(e2e::contains(l, "failed members: c")); + expect(eq(s::count_lines(l, "Workspace building"), 1u)) << "the members were planned more than once"; + } + // L + { + auto ws = libraries(temp_dir() / "w3", "856", {"a", "b", "c"}); + ws.write("c/src/c.cppm", "export module sel856_c;\nexport int c_v() { return \"not an int\"; }\n"); + auto r = ws.mcpp({"test", "--workspace"}); + expect(r.started && r.status != 0) << "a member whose package does not compile exited 0"; + const auto l = s::text(r); + expect(e2e::contains(l, "test_a ... ok")); + expect(e2e::contains(l, "test_b ... ok")); + expect(e2e::contains(l, "member 'c' (3/3) FAILED")); + expect(e2e::contains(l, "member 'c': its package did not build")); + expect(e2e::contains(l, "failed members: c")); + expect(s::lacks(l, "test_c ... ok")); + } + // M + { + auto ws = libraries(temp_dir() / "w4", "856", {"a", "b", "d", "c"}); + ws.write("d/mcpp.toml", R"([package] +name = "d" +version = "0.1.0" + +[dependencies] +absent = { path = "../no-such-directory" } + +[targets.d] +kind = "lib" +)"); + auto r = ws.mcpp({"test", "--workspace"}); + expect(r.started && r.status != 0) << "a member that cannot be planned exited 0"; + const auto l = s::text(r); + for (auto m : {"a", "b", "c"}) + expect(e2e::contains(l, std::format("test_{} ... ok", m))); + expect(e2e::contains(l, "member 'd' (3/4) FAILED")); + expect(e2e::contains(l, "failed members: d")); + expect(e2e::contains(l, "workspace test: 1/4 member(s) failed; 3 passed")); + } + // N (Linux: LD_LIBRARY_PATH is the runtime search variable) + if (s::on_linux()) { + s::project ws(temp_dir() / "w5", {{"mcpp.toml", "[workspace]\nmembers = [\"x\", \"y\"]\n"}}); + for (auto [m, other] : {std::pair{"x", "y"}, std::pair{"y", "x"}}) { + ws.write(std::format("{}/rt856{}/keep", m, m), ""); + ws.write(std::format("{}/mcpp.toml", m), + std::format("[package]\nname = \"{}\"\nversion = \"0.1.0\"\n\n[runtime]\nlibrary_dirs = " + "[\"rt856{}\"]\n\n[targets.{}]\nkind = \"lib\"\n", + m, m, m)); + ws.write(std::format("{}/src/{}.cppm", m, m), + std::format("export module sel856_{};\nexport int {}_v() {{ return 1; }}\n", m, m)); + ws.write(std::format("{}/tests/env.cpp", m), std::format(R"(#include +#include +int main() {{ + const char* p = std::getenv("LD_LIBRARY_PATH"); + if (!p) return 2; + if (!std::strstr(p, "rt856{}")) return 3; // its own runtime directory + if (std::strstr(p, "rt856{}")) return 4; // another member's + return 0; +}} +)", + m, other)); + } + auto n = ws.mcpp({"test", "--workspace"}); + expect(e2e::succeeded(n)) << "a member's test saw the wrong runtime directories (3: its own missing, 4: another's present)"; + expect(eq(s::count_lines(s::text(n), "Workspace building"), 1u)); + expect(e2e::contains(s::text(n), "workspace result ok. 2 member(s); 2 passed")); + } + }; + + "1034 a workspace members test finds its programs"_test[tag{"requires:gcc"}] = [] { + // A member's test finds the member's programs (MCPP_BIN_) under --workspace as + // under -p; each finds its own program of one shared name. + s::needs({"gcc"}); + s::project ws{{"mcpp.toml", "[workspace]\nmembers = [\"a\", \"b\"]\n"}}; + for (auto m : {"a", "b"}) { + ws.write(std::format("{}/mcpp.toml", m), + std::format("[package]\nname = \"{}\"\nversion = \"0.1.0\"\n\n[targets.tool]\nkind = \"bin\"\nmain = \"src/tool.cpp\"\n", m)); + ws.write(std::format("{}/src/tool.cpp", m), std::format("import std;\nint main() {{ std::println(\"{}\"); return 0; }}\n", m)); + ws.write(std::format("{}/tests/finds.cpp", m), std::format(R"(import std; +int main() {{ + const char* tool = std::getenv("MCPP_BIN_TOOL"); + if (!tool || !std::filesystem::is_regular_file(tool)) {{ std::println("no MCPP_BIN_TOOL"); return 1; }} + const std::string path = tool; + return path.find("{}") != std::string::npos || std::filesystem::path(path).parent_path().string().find("{}") != std::string::npos ? 0 : 2; +}} +)", + m, m)); + } + auto w = ws.mcpp({"test", "--workspace"}); + expect(e2e::succeeded(w)) << "under --workspace a member's test did not find its program"; + expect(e2e::succeeded(ws.mcpp({"test", "-p", "a"}))); + expect(e2e::contains(s::text(w), "finds ... ok")); + }; +}}; + +} // namespace diff --git a/tests/support/src/support.cpp b/tests/support/src/support.cpp index b2eff0fb3..2dbffdcbe 100644 --- a/tests/support/src/support.cpp +++ b/tests/support/src/support.cpp @@ -38,6 +38,133 @@ std::optional env(std::string_view name) { return std::nullopt; } +std::optional on_path(std::string_view name) { + const auto path = env("PATH"); + if (!path) + return std::nullopt; + const char sep = on_windows() ? ';' : ':'; + for (auto part : std::views::split(std::string_view(*path), sep)) { + const fs::path dir(std::string(part.begin(), part.end())); + if (dir.empty()) + continue; + std::error_code ec; + for (const auto& candidate : {dir / name, dir / (std::string(name) + ".exe")}) + if (fs::is_regular_file(candidate, ec)) + return candidate; + } + return std::nullopt; +} + +// ---- capabilities (tests/e2e/run_all.sh, "Build the set of capabilities") ---------------------- + +namespace { + +fs::path home_dir() { + if (auto h = env("MCPP_HOME"); h && !h->empty()) + return *h; + return fs::path(env(on_windows() ? "USERPROFILE" : "HOME").value_or("")) / ".mcpp"; +} + +fs::path user_dir() { return env(on_windows() ? "USERPROFILE" : "HOME").value_or(""); } + +// A payload under the registry of the home or of the user's xlings: `/`. +bool payload(std::string_view pattern, bool xlings_too = true) { + if (!glob(home_dir() / "registry/data/xpkgs", pattern).empty()) + return true; + return xlings_too && !glob(user_dir() / ".xlings/data/xpkgs", pattern).empty(); +} + +bool probe(std::string_view c) { + const bool unix_like = on_linux() || on_macos(); + if (c == "x86_64-host") { +#if defined(__x86_64__) || defined(_M_X64) + return true; +#else + return false; +#endif + } + if (c == "elf") return on_linux(); + if (c == "unix-shell" || c == "fresh-sandbox" || c == "symlink") return unix_like; + if (c == "macos") return on_macos(); + if (c == "windows") return on_windows(); + if (c == "gcc") return on_linux() && on_path("g++"); + if (c == "patchelf" || c == "pack") return on_linux() && on_path("patchelf"); + if (c == "musl") + return on_linux() && (on_path("x86_64-linux-musl-g++") || payload("xim-x-musl-gcc/*/bin/x86_64-linux-musl-g++")); + if (c == "mingw-cross") return on_linux() && payload("xim-x-mingw-cross-gcc/*/bin/x86_64-w64-mingw32-g++"); + if (c == "llvm") return on_linux() && payload("xim-x-llvm/*/bin/clang++", false); + if (c == "mingw-host-headers") { + if (!on_linux()) return false; + if (fs::is_directory("/usr/x86_64-w64-mingw32/include")) return true; + auto root = env("MCPP_E2E_HOST_MINGW_ROOT"); + return root && !root->empty() && fs::is_regular_file(fs::path(*root) / "x86_64-w64-mingw32/include/io.h"); + } + if (c == "wine") return on_linux() && on_path("wine"); + if (c == "qemu-riscv") return on_linux() && on_path("qemu-system-riscv64"); + if (c == "qemu-arm") return on_linux() && (on_path("qemu-system-arm") || payload("xim-x-qemu-arm/*/bin/qemu-system-arm")); + if (c == "android-ndk") return on_linux() && payload("xim-x-android-ndk/*/toolchains/llvm/prebuilt/*/bin/clang++"); + if (c == "android-device") { + // An arm64 device (not an emulator) answering the platform-tools' adb. + if (!on_linux()) return false; + std::vector adbs = glob(user_dir() / ".xlings/data/xpkgs", "xim-x-android-platform-tools/*/adb"); + for (auto& a : glob(home_dir() / "registry/data/xpkgs", "xim-x-android-platform-tools/*/adb")) adbs.push_back(a); + for (const auto& adb : adbs) { + auto r = e2e::run({.argv = {adb.string(), "devices"}}); + for (auto line : std::views::split(std::string_view(r.out), '\n')) { + std::string_view l(line.begin(), line.end()); + const auto tab = l.find('\t'); + if (tab == std::string_view::npos || l.starts_with("emulator-") || !l.substr(tab + 1).starts_with("device")) + continue; + auto abi = e2e::run({.argv = {adb.string(), "shell", "getprop", "ro.product.cpu.abi"}, + .env = {{"ANDROID_SERIAL", std::string(l.substr(0, tab))}}}); + if (abi.out.starts_with("arm64-v8a")) return true; + } + } + return false; + } + if (c == "msvc" || c == "no-msvc") { + if (!on_windows()) return false; + const fs::path vswhere = "C:/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe"; + bool msvc = false; + if (fs::is_regular_file(vswhere)) { + auto r = e2e::run({.argv = {vswhere.string(), "-latest", "-products", "*", "-requires", + "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", "-property", + "installationPath"}}); + msvc = r.out.find_first_not_of(" \r\n") != std::string::npos; + } + return c == "msvc" ? msvc : !msvc; + } + if (c == "mingw") return on_windows() && payload("xim-x-mingw-gcc/*/bin/g++.exe"); + if (c == "python3") return on_path("python3").has_value(); + if (c == "jq") return on_path("jq").has_value(); + if (c == "nasm") return on_path("nasm") || payload("xim-x-nasm/*/nasm", false) || payload("xim-x-nasm/*/bin/nasm", false); + if (c == "scan-deps") + return on_path("clang-scan-deps") || payload("xim-x-llvm/*/bin/clang-scan-deps", false) || + payload("xim-x-llvm/*/bin/clang-scan-deps.exe", false); + if (c == "import-std-libcxx") return payload("xim-x-llvm/*/share/libc++/v1/std.cppm", false); + if (c == "xlings-msvc") return env("MCPP_E2E_XLINGS_MSVC").value_or("") == "1"; + mcpplibs::mtest::fail(std::format("unknown capability '{}' (run_all.sh's KNOWN_CAPS)", c)); +} + +} // namespace + +bool has(std::string_view capability) { + static std::mutex m; + static std::map> known; + std::scoped_lock lock(m); + if (auto it = known.find(capability); it != known.end()) + return it->second; + const bool held = probe(capability); + known.emplace(std::string(capability), held); + return held; +} + +void needs(std::initializer_list capabilities) { + for (auto c : capabilities) + if (!has(c)) + mcpplibs::mtest::skip(std::format("missing capability: {}", c)); +} + // ---- mcpp ---------------------------------------------------------------------------------------- std::string mcpp_path() { @@ -49,6 +176,10 @@ std::string mcpp_path() { } e2e::outcome mcpp(std::vector args, const fs::path& cwd, const options& o) { + return e2e::run(mcpp_command(std::move(args), cwd, o)); +} + +e2e::command mcpp_command(std::vector args, const fs::path& cwd, const options& o) { e2e::command c; c.argv.push_back(mcpp_path()); for (auto& a : args) c.argv.push_back(std::move(a)); @@ -64,7 +195,7 @@ e2e::outcome mcpp(std::vector args, const fs::path& cwd, const opti for (const auto& kv : o.env) c.env.push_back(kv); c.input = o.input; c.timeout = o.timeout; - return e2e::run(std::move(c)); + return c; } std::string text(const e2e::outcome& r) { return r.out + r.err; } @@ -107,6 +238,17 @@ std::optional project::artifact(std::string_view name) const { return find_file(dir_ / "target", name); } +project project::scaffold(std::string_view name, std::vector args) { + const auto parent = mcpplibs::mtest::temp_dir() / "new"; + fs::create_directories(parent); + std::vector argv{"new", std::string(name)}; + argv.insert(argv.end(), args.begin(), args.end()); + auto r = support::mcpp(std::move(argv), parent); + if (!r) + mcpplibs::mtest::fail(std::format("mcpp new {} failed: {}\n{}", name, r.line, text(r))); + return project(parent / fs::path(std::u8string(name.begin(), name.end())), {}); +} + std::optional find_file(const fs::path& root, std::string_view name) { std::error_code ec; if (!fs::is_directory(root, ec)) @@ -136,6 +278,49 @@ std::vector find_files(const fs::path& root, std::string_view pattern) return found; } +namespace { +// A shell's `*` within one name; no leading dot unless the pattern has one. +bool wildcard(std::string_view pattern, std::string_view name) { + if (name.starts_with('.') && !pattern.starts_with('.')) + return false; + std::size_t p = 0, n = 0, star = std::string_view::npos, mark = 0; + while (n < name.size()) { + if (p < pattern.size() && pattern[p] == '*') { star = p++; mark = n; } + else if (p < pattern.size() && pattern[p] == name[n]) { ++p; ++n; } + else if (star != std::string_view::npos) { p = star + 1; n = ++mark; } + else return false; + } + while (p < pattern.size() && pattern[p] == '*') ++p; + return p == pattern.size(); +} +} // namespace + +std::vector glob(const fs::path& root, std::string_view pattern) { + std::vector level{root}; + for (auto part : std::views::split(pattern, '/')) { + const std::string_view component(part.begin(), part.end()); + if (component.empty()) + continue; + std::vector next; + std::error_code ec; + for (const auto& dir : level) { + if (!component.contains('*')) { + if (fs::exists(dir / component, ec)) + next.push_back(dir / component); + continue; + } + if (!fs::is_directory(dir, ec)) + continue; + for (const auto& e : fs::directory_iterator(dir, fs::directory_options::skip_permission_denied, ec)) + if (wildcard(component, e.path().filename().string())) + next.push_back(e.path()); + } + level = std::move(next); + } + std::ranges::sort(level); + return level; +} + // ---- readings -------------------------------------------------------------------------------------- std::string read_file(const fs::path& file) { @@ -185,6 +370,64 @@ std::vector json_lines(std::string_view text) { return out; } +namespace { +std::vector split_lines(std::string_view text) { + std::vector out; + for (auto line : std::views::split(text, '\n')) { + std::string_view l(line.begin(), line.end()); + if (l.ends_with('\r')) l.remove_suffix(1); + out.push_back(l); + } + return out; +} +std::string tail_of(std::string_view text) { + constexpr std::size_t keep = 3000; + return std::string(text.size() > keep ? text.substr(text.size() - keep) : text); +} +std::string lower(std::string_view s) { + std::string r(s); + for (auto& ch : r) ch = static_cast(std::tolower(static_cast(ch))); + return r; +} +bool any_line(std::string_view text, std::string_view pattern) { + const std::regex re{std::string(pattern)}; + for (auto l : split_lines(text)) + if (std::regex_search(l.begin(), l.end(), re)) + return true; + return false; +} +} // namespace + +mcpplibs::mtest::comparison has_line(std::string_view text, std::string_view pattern) { + return {any_line(text, pattern), "has_line", std::format("a line matching /{}/", pattern), tail_of(text)}; +} +mcpplibs::mtest::comparison no_line(std::string_view text, std::string_view pattern) { + return {!any_line(text, pattern), "no_line", std::format("no line matching /{}/", pattern), tail_of(text)}; +} +mcpplibs::mtest::comparison lacks(std::string_view text, std::string_view needle) { + return {!text.contains(needle), "lacks", std::format("text without \"{}\"", needle), tail_of(text)}; +} +mcpplibs::mtest::comparison contains_nocase(std::string_view text, std::string_view needle) { + return {lower(text).contains(lower(needle)), "contains_nocase", + std::format("text containing \"{}\" (any case)", needle), tail_of(text)}; +} +std::size_t count_lines(std::string_view text, std::string_view needle) { + return static_cast(std::ranges::count_if(split_lines(text), [&](auto l) { return l.contains(needle); })); +} + +std::vector ninja_log_outputs(const fs::path& build_dir) { + std::vector out; + for (auto l : split_lines(read_file(build_dir / ".ninja_log"))) { + if (l.starts_with('#')) + continue; + std::vector cols; + for (auto c : std::views::split(l, '\t')) cols.emplace_back(c.begin(), c.end()); + if (cols.size() >= 4) + out.emplace_back(cols[3]); + } + return out; +} + // ---- executables ------------------------------------------------------------------------------------- namespace { diff --git a/tests/support/src/support.cppm b/tests/support/src/support.cppm index f82d19407..6756b9628 100644 --- a/tests/support/src/support.cppm +++ b/tests/support/src/support.cppm @@ -33,6 +33,15 @@ bool on_linux() noexcept; std::string exe(std::string_view name); // The value of the environment variable `name`, if set. std::optional env(std::string_view name); +// The executable `name` as PATH finds it (`name.exe` too on Windows). +std::optional on_path(std::string_view name); + +// A capability word of a script's `# requires:` line, as tests/e2e/run_all.sh detects it on this +// machine (the same probes: a program on PATH, a payload in the home, the system). An unknown word +// fails the case, so that a misspelt word cannot turn into a skip on every machine. +bool has(std::string_view capability); +// Ends the case as skipped ("missing capability: ") unless this machine has every word. +void needs(std::initializer_list capabilities); // ---- mcpp ---------------------------------------------------------------------------------------- @@ -51,6 +60,8 @@ struct options { // mcpp with `args`, in `cwd`. e2e::outcome mcpp(std::vector args, const fs::path& cwd, const options& o = {}); +// The same command, not run: for an `e2e::session` (a mcpp that runs while the case does more). +e2e::command mcpp_command(std::vector args, const fs::path& cwd, const options& o = {}); // Standard output, then standard error: what a shell's `2>&1` would have collected, for a // message whose stream is not what the case asserts. @@ -82,6 +93,10 @@ public: // `exe(name)`), or nothing. std::optional artifact(std::string_view name) const; + // `mcpp new ` in a directory of the case: the project it made (the case fails + // when mcpp does). + static project scaffold(std::string_view name, std::vector args = {}); + private: fs::path dir_; }; @@ -90,6 +105,10 @@ private: std::optional find_file(const fs::path& root, std::string_view name); // Every regular file under `root` whose name matches `pattern` (ECMAScript, the whole name). std::vector find_files(const fs::path& root, std::string_view pattern); +// The paths under `root` that a shell glob of `/`-separated components names, where `*` in a +// component matches any run of characters and, as in a shell, no leading dot: +// glob(p.at("target"), "*/*/build.ninja"). +std::vector glob(const fs::path& root, std::string_view pattern); // ---- readings -------------------------------------------------------------------------------------- @@ -103,6 +122,21 @@ nlohmann::json json_file(const fs::path& file); // Every line of `text` that is a JSON object (a stream of records). std::vector json_lines(std::string_view text); +// What a script's `grep` asked, as comparisons whose failure shows the text: +// some line of `text` matches `pattern` (ECMAScript; `^` and `$` are the line's ends: `grep -E`); +mcpplibs::mtest::comparison has_line(std::string_view text, std::string_view pattern); +// no line of `text` matches `pattern` (`! grep -E`); +mcpplibs::mtest::comparison no_line(std::string_view text, std::string_view pattern); +// `text` does not contain `needle`; +mcpplibs::mtest::comparison lacks(std::string_view text, std::string_view needle); +// `text` contains `needle`, ignoring ASCII case (`grep -i`). +mcpplibs::mtest::comparison contains_nocase(std::string_view text, std::string_view needle); +// The number of lines of `text` that contain `needle` (`grep -c`). +std::size_t count_lines(std::string_view text, std::string_view needle); + +// The outputs `.ninja_log` records, one per edge run (its fourth column), in order. +std::vector ninja_log_outputs(const fs::path& build_dir); + // ---- executables ------------------------------------------------------------------------------------- enum class format { unknown, elf, pe, macho, macho_fat, archive, wasm }; From 1bf5e8e963d33c037eb22a75fec6287c158d54c6 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 13 Nov 2026 18:04:55 +0900 Subject: [PATCH 005/115] e2e: the build-program subsystem as C++ cases (66 scripts) The 66 scripts MIGRATION.tsv assigns to `build-program` (build.mcpp: its directives and protocol, its environment and cache, dependencies' programs, host tools and host modules, build plugins and the build programs' graph, accelerator requests and the probe channel, payload overrides) are 66 cases of the program e2e/build-program (tests/e2e/build-program/*.cpp, 6 files: accel, cross, deps, graph, hosttools, protocol). Each case is named by its script's sentence and keeps its number; its `# requires:` words are its tags. Scripts: 58 89 92 110 111 112 125 139 143 144 145 168 181 186 187 189 193 194 309 310 311 312 334 600 605 612 617 618 622 624 625 626 632 633 651 665 683 693 698 712 720 775 779 800 802 804 823 824 826 831 859 873 874 877 902 903 904 905 906 908 910 958 997 1011 1036 1051. 168 and 804 keep their scripts: ci-aarch64-fresh-install.yml runs them by path. The other 64 are removed. Old run (the scripts, against this branch's mcpp, Linux x86_64): 65 passed, 1 failed (779). New run (`mcpp test e2e/build-program`): 65 passed, 1 failed (779), 0 skipped. 779 fails on this host in both runs for one reason outside mcpp: it builds its plugin with the `gcc` PATH names, here an xlings subos gcc whose shared objects record NEEDED libc.so (the linker script), which the loader cannot load; /usr/bin/gcc records none. Under the script the plugin built and `mcpp run` failed loading it; under `mcpp test` PATH names another subos gcc that does not find crti.o. tests/support gains: home kinds `linked` (the case's home with the shared registry linked, as `ln -s ~/.mcpp/registry`), `inherited` (_inherit_toolchain.sh) and `payloads` (the same without config and subos), made under the system's temporary directory (glibc refuses an install directory over 255 bytes, which a case's own directory exceeds); case_home, shared_home, newest_payload, payload_root, family_version (and llvm_version, gcc_version, musl_gcc_version, mingw_cross_version: _toolchain_env.sh), repository, listing, newest_file, write_executable, run, has_line_nocase, last_line, last_starting, count_matching. Every assertion is kept. Where a reading changed: - `mcpp run 2>&1 | tail -1` is read as the last line of the program's standard output (the shell's interleaving put the program's line last). - 168: `file`'s "statically linked" and readelf's interpreter are read from the ELF header: no PT_INTERP and no DT_NEEDED. - 802: readelf's NEEDED is read from the header; 804: the .comment strings are read from the file's bytes (`clang version `, `GCC: (...) 16.1.0`), the llvm version being the one _toolchain_env.sh chooses (23.1.3 here, the literal the script grepped). - 310: `strings -a` and `nm -C` become the file's bytes and `nm -C` when PATH has it. - 720, 877, 903: python3 read JSON for the scripts; the cases read it with nlohmann::json and no longer require python3. 1011 required python3 and did not use it; the case does not require it. - 600, 605, 612, 626, 1036 and the other `mcpp new` fixtures keep `mcpp new`; fixtures the scripts wrote whole are written whole. - 698's Windows long-path reading runs powershell where the script did. --- ...tor_requests_are_complete_and_canonical.sh | 68 - ...ext_states_the_engine_values_in_process.sh | 50 - ...chain_section_is_not_the_build_programs.sh | 46 - tests/e2e/110_build_mcpp_env_contract.sh | 113 -- tests/e2e/111_dep_build_mcpp.sh | 88 -- tests/e2e/112_build_mcpp_cross.sh | 93 -- tests/e2e/125_build_mcpp_dep_dir.sh | 97 -- tests/e2e/139_build_program_advisory.sh | 138 -- tests/e2e/143_build_mcpp_source_directive.sh | 90 -- tests/e2e/144_build_mcpp_include_dir.sh | 91 -- tests/e2e/145_root_build_mcpp_dep_dirs.sh | 80 -- tests/e2e/181_build_mcpp_import_std.sh | 110 -- .../e2e/186_build_mcpp_protocol_and_bound.sh | 198 --- tests/e2e/187_dep_host_tool.sh | 274 ---- tests/e2e/189_host_module_rules.sh | 154 --- tests/e2e/193_provision_reexport.sh | 236 ---- tests/e2e/194_build_mcpp_glob_input.sh | 125 -- tests/e2e/309_host_module_identity.sh | 235 ---- tests/e2e/310_build_dependencies.sh | 163 --- tests/e2e/311_rule_build_dependencies.sh | 180 --- tests/e2e/312_build_rules_example.sh | 97 -- .../334_dependency_declared_tool_is_found.sh | 109 -- .../e2e/58_preinstall_mcpp_deps_for_hooks.sh | 233 ---- tests/e2e/600_accel_variant_selection.sh | 114 -- ...annel_and_accel_reach_the_build_program.sh | 124 -- .../612_build_program_file_input_fast_path.sh | 96 -- .../617_build_program_knows_the_cxx_stdlib.sh | 92 -- ...y_feature_tools_reach_its_build_program.sh | 121 -- ...oad_reaches_the_consumers_build_program.sh | 127 -- ...624_a_missing_host_module_names_the_key.sh | 88 -- ...payload_can_be_gated_on_the_accelerator.sh | 71 -- ..._a_subset_of_backends_is_not_a_mismatch.sh | 93 -- ..._build_program_is_told_its_package_name.sh | 82 -- ...modules_are_ordered_by_what_they_import.sh | 186 --- ...build_program_deploys_what_it_generated.sh | 185 --- .../e2e/665_a_host_tool_follows_its_source.sh | 137 -- ...ogram_reads_the_payload_pkg_config_view.sh | 47 - ...ild_program_reads_dependency_link_forms.sh | 169 --- ...er_its_dependency_has_a_bounded_address.sh | 225 ---- ...age_contributes_nothing_to_its_consumer.sh | 200 --- ..._build_program_reads_the_resolved_graph.sh | 176 --- ...ol_merges_its_conditional_sections_once.sh | 88 -- ...d_program_declares_a_runtime_search_dir.sh | 218 ---- .../800_a_feature_provides_its_host_tools.sh | 131 -- .../802_a_host_build_applies_its_host_row.sh | 89 -- ...3_mcpp_core_and_mcpp_name_one_interface.sh | 36 - ...ld_program_reads_the_resolved_toolchain.sh | 81 -- ...6_plugin_diagnostics_features_and_names.sh | 91 -- ...path_dependency_host_module_outside_src.sh | 86 -- ..._entry_is_reused_while_its_inputs_agree.sh | 128 -- tests/e2e/873_payload_overrides.sh | 125 -- tests/e2e/874_payload_on_request.sh | 95 -- tests/e2e/877_why_sources.sh | 46 - tests/e2e/89_build_mcpp.sh | 103 -- ...ency_library_is_compiled_as_its_package.sh | 81 -- ...ackage_in_both_tables_has_two_instances.sh | 138 -- ...endency_feature_stays_out_of_the_target.sh | 44 - ...build_program_links_its_libraries_whole.sh | 35 - ...gram_is_compiled_when_its_graph_changes.sh | 44 - .../908_what_a_build_dependency_asks_for.sh | 53 - ...lugin_is_a_package_of_the_build_program.sh | 114 -- tests/e2e/92_build_mcpp_import.sh | 59 - ...gram_runs_again_when_its_output_is_gone.sh | 53 - ...ev_dependencies_stay_out_of_a_host_tool.sh | 124 -- tests/e2e/MIGRATION.tsv | 132 +- tests/e2e/build-program/accel.cpp | 213 ++++ tests/e2e/build-program/cross.cpp | 122 ++ tests/e2e/build-program/deps.cpp | 942 ++++++++++++++ tests/e2e/build-program/graph.cpp | 265 ++++ tests/e2e/build-program/hosttools.cpp | 1126 +++++++++++++++++ tests/e2e/build-program/protocol.cpp | 898 +++++++++++++ tests/support/src/support.cpp | 236 +++- tests/support/src/support.cppm | 46 +- 73 files changed, 3909 insertions(+), 7534 deletions(-) delete mode 100755 tests/e2e/1011_accelerator_requests_are_complete_and_canonical.sh delete mode 100755 tests/e2e/1036_a_context_states_the_engine_values_in_process.sh delete mode 100755 tests/e2e/1051_a_cross_targets_toolchain_section_is_not_the_build_programs.sh delete mode 100755 tests/e2e/110_build_mcpp_env_contract.sh delete mode 100755 tests/e2e/111_dep_build_mcpp.sh delete mode 100755 tests/e2e/112_build_mcpp_cross.sh delete mode 100755 tests/e2e/125_build_mcpp_dep_dir.sh delete mode 100755 tests/e2e/139_build_program_advisory.sh delete mode 100755 tests/e2e/143_build_mcpp_source_directive.sh delete mode 100755 tests/e2e/144_build_mcpp_include_dir.sh delete mode 100755 tests/e2e/145_root_build_mcpp_dep_dirs.sh delete mode 100755 tests/e2e/181_build_mcpp_import_std.sh delete mode 100755 tests/e2e/186_build_mcpp_protocol_and_bound.sh delete mode 100755 tests/e2e/187_dep_host_tool.sh delete mode 100755 tests/e2e/189_host_module_rules.sh delete mode 100755 tests/e2e/193_provision_reexport.sh delete mode 100755 tests/e2e/194_build_mcpp_glob_input.sh delete mode 100755 tests/e2e/309_host_module_identity.sh delete mode 100755 tests/e2e/310_build_dependencies.sh delete mode 100755 tests/e2e/311_rule_build_dependencies.sh delete mode 100755 tests/e2e/312_build_rules_example.sh delete mode 100755 tests/e2e/334_dependency_declared_tool_is_found.sh delete mode 100644 tests/e2e/58_preinstall_mcpp_deps_for_hooks.sh delete mode 100755 tests/e2e/600_accel_variant_selection.sh delete mode 100755 tests/e2e/605_probe_channel_and_accel_reach_the_build_program.sh delete mode 100755 tests/e2e/612_build_program_file_input_fast_path.sh delete mode 100755 tests/e2e/617_build_program_knows_the_cxx_stdlib.sh delete mode 100755 tests/e2e/618_dependency_feature_tools_reach_its_build_program.sh delete mode 100755 tests/e2e/622_rule_declared_payload_reaches_the_consumers_build_program.sh delete mode 100755 tests/e2e/624_a_missing_host_module_names_the_key.sh delete mode 100755 tests/e2e/625_a_payload_can_be_gated_on_the_accelerator.sh delete mode 100755 tests/e2e/626_naming_a_subset_of_backends_is_not_a_mismatch.sh delete mode 100755 tests/e2e/632_a_build_program_is_told_its_package_name.sh delete mode 100755 tests/e2e/633_host_modules_are_ordered_by_what_they_import.sh delete mode 100755 tests/e2e/651_a_build_program_deploys_what_it_generated.sh delete mode 100755 tests/e2e/665_a_host_tool_follows_its_source.sh delete mode 100755 tests/e2e/683_a_build_program_reads_the_payload_pkg_config_view.sh delete mode 100644 tests/e2e/693_the_root_build_program_reads_dependency_link_forms.sh delete mode 100755 tests/e2e/698_a_tool_source_under_its_dependency_has_a_bounded_address.sh delete mode 100755 tests/e2e/712_a_tool_package_contributes_nothing_to_its_consumer.sh delete mode 100755 tests/e2e/720_the_root_build_program_reads_the_resolved_graph.sh delete mode 100755 tests/e2e/775_a_host_tool_merges_its_conditional_sections_once.sh delete mode 100755 tests/e2e/779_a_build_program_declares_a_runtime_search_dir.sh delete mode 100755 tests/e2e/800_a_feature_provides_its_host_tools.sh delete mode 100755 tests/e2e/802_a_host_build_applies_its_host_row.sh delete mode 100755 tests/e2e/823_mcpp_core_and_mcpp_name_one_interface.sh delete mode 100755 tests/e2e/824_a_build_program_reads_the_resolved_toolchain.sh delete mode 100755 tests/e2e/826_plugin_diagnostics_features_and_names.sh delete mode 100755 tests/e2e/831_a_path_dependency_host_module_outside_src.sh delete mode 100755 tests/e2e/859_a_host_module_entry_is_reused_while_its_inputs_agree.sh delete mode 100755 tests/e2e/873_payload_overrides.sh delete mode 100755 tests/e2e/874_payload_on_request.sh delete mode 100755 tests/e2e/877_why_sources.sh delete mode 100755 tests/e2e/89_build_mcpp.sh delete mode 100755 tests/e2e/902_a_build_dependency_library_is_compiled_as_its_package.sh delete mode 100755 tests/e2e/903_one_package_in_both_tables_has_two_instances.sh delete mode 100755 tests/e2e/904_a_build_dependency_feature_stays_out_of_the_target.sh delete mode 100755 tests/e2e/905_a_build_program_links_its_libraries_whole.sh delete mode 100755 tests/e2e/906_a_build_program_is_compiled_when_its_graph_changes.sh delete mode 100755 tests/e2e/908_what_a_build_dependency_asks_for.sh delete mode 100755 tests/e2e/910_a_build_plugin_is_a_package_of_the_build_program.sh delete mode 100755 tests/e2e/92_build_mcpp_import.sh delete mode 100755 tests/e2e/958_a_build_program_runs_again_when_its_output_is_gone.sh delete mode 100755 tests/e2e/997_a_members_dev_dependencies_stay_out_of_a_host_tool.sh create mode 100644 tests/e2e/build-program/accel.cpp create mode 100644 tests/e2e/build-program/cross.cpp create mode 100644 tests/e2e/build-program/deps.cpp create mode 100644 tests/e2e/build-program/graph.cpp create mode 100644 tests/e2e/build-program/hosttools.cpp create mode 100644 tests/e2e/build-program/protocol.cpp diff --git a/tests/e2e/1011_accelerator_requests_are_complete_and_canonical.sh b/tests/e2e/1011_accelerator_requests_are_complete_and_canonical.sh deleted file mode 100755 index 6fc9de4f3..000000000 --- a/tests/e2e/1011_accelerator_requests_are_complete_and_canonical.sh +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc unix-shell python3 -# Invalid accelerator syntax must not become a successful CPU build, and an -# implementation cannot occur twice with order-dependent constraint coverage. -set -e -MCPP="${MCPP:-mcpp}" -work=$(mktemp -d) -trap 'rm -rf "$work"' EXIT -export MCPP_HOME="$work/home" -source "$(dirname "$0")/_inherit_toolchain.sh" -mkdir -p "$work/app/src" -cd "$work/app" -fail() { echo "FAIL: $*"; cat out.log 2>/dev/null; exit 1; } -manifest() { - cat > mcpp.toml <<'TOML' -[package] -name = "accel-request" -version = "0.1.0" -[targets.accel-request] -kind = "bin" -main = "src/main.cpp" -TOML -} -manifest -echo 'int main(){return 0;}' > src/main.cpp - -for request in 'sycl+{sm_89' 'sycl+{sm_89}garbage' 'sycl+{sm_89,,sm_90}' \ - 'sycl+{sm_89},sycl+{sm_90}' 'sycl+{sm_90},sycl+{sm_89}'; do - if "$MCPP" build --accel "$request" > out.log 2>&1; then - fail "accepted malformed or repeated request: $request" - fi - grep -q 'invalid accelerator request at byte' out.log || fail "missing syntax location" - grep -q -- '--accel' out.log || fail "missing request source" -done - -cat >> mcpp.toml <<'TOML' -[build] -accel = "sycl+{sm_89" -TOML -if "$MCPP" build > out.log 2>&1; then fail 'accepted malformed manifest request'; fi -grep -q '\[build\] accel' out.log || fail 'missing manifest field' - -manifest -cat >> mcpp.toml <<'TOML' -[build] -sources = [{ glob = "src/main.cpp", accel = "sycl+{sm_90" }] -TOML -if "$MCPP" build --no-accel > out.log 2>&1; then fail 'ignored malformed source constraint'; fi -grep -q "sources entry 'src/main.cpp'" out.log || fail 'missing source constraint' -grep -q 'invalid accelerator request' out.log || fail 'missing constraint syntax error' - -manifest -cat >> mcpp.toml <<'TOML' -[build] -sources = [{ glob = "src/main.cpp", accel = "sycl+{sm_90}" }] -TOML -"$MCPP" build --accel 'sycl+{sm_90,sm_89},newmodel2+{opaque}' > out.log 2>&1 \ - || fail 'valid open-vocabulary request did not build' -before=$(find target -maxdepth 3 -name build.ninja | sort) -[ -n "$before" ] || fail 'no build graph was generated' -"$MCPP" build --accel 'newmodel2+{opaque},sycl+{sm_89,sm_90,sm_89}' > out.log 2>&1 \ - || fail 'equivalent request did not build' -after=$(find target -maxdepth 3 -name build.ninja | sort) -[ "$before" = "$after" ] || fail 'equivalent sets allocated different variants' -grep -q 'fingerprint changed' out.log && fail 'equivalent sets changed the fingerprint' -"$MCPP" run --accel 'sycl+{sm_89,sm_90},newmodel2+{opaque}' > out.log 2>&1 \ - || fail 'the canonical variant did not run' -echo 'PASS: accelerator requests are complete, unique and canonical' diff --git a/tests/e2e/1036_a_context_states_the_engine_values_in_process.sh b/tests/e2e/1036_a_context_states_the_engine_values_in_process.sh deleted file mode 100755 index f0403aebf..000000000 --- a/tests/e2e/1036_a_context_states_the_engine_values_in_process.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 1036 -- a `mcpp::context` states the engine's values to the interface's accessors in-process -# (docs/30 "Testing a program's logic"). -# -# Every accessor of the build-program interface reads the engine's values through one function: -# within a context, the values it states, the innermost context first, and a name it does not -# state from the context around it or the environment; outside every context, the environment. -# -# Criteria: a build program that states contexts reads their values, nested and restored, and -# `has_feature` and `dep_dir` answer within them; the build succeeds and its program prints what the -# build program read. -set -e -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } - -cd "$TMP" -"$MCPP" new ctx > /dev/null -cd ctx -cat > build.mcpp <<'EOF' -import std; -import mcpp; -int main() { - const std::string os = mcpp::target_os(); - std::string seen; - { - mcpp::context outer({{"MCPP_TARGET_OS", "plan9"}, {"MCPP_FEATURE_FAST", "1"}}); - seen += mcpp::target_os(); - seen += mcpp::has_feature("fast") ? " fast" : " slow"; - { - mcpp::context inner({{"MCPP_PROFILE", "inner"}, {"MCPP_DEP_ZLIB_DIR", "/z"}}); - seen += std::format(" {} {} {}", mcpp::target_os(), mcpp::profile(), mcpp::dep_dir("zlib")); - } - seen += std::format(" {}", std::string(mcpp::profile()) == "inner" ? "leaked" : "restored"); - } - seen += std::string(mcpp::target_os()) == os ? " environment" : " leaked"; - std::ofstream("src/seen.cpp") << "extern \"C\" const char* seen() { return \"" << seen << "\"; }\n"; - mcpp::generated("src/seen.cpp"); - return 0; -} -EOF -cat > src/main.cpp <<'EOF' -import std; -extern "C" const char* seen(); -int main() { std::println("{}", seen()); return 0; } -EOF -"$MCPP" run > run.log 2>&1 || fail "the build or the program failed" run.log -grep -q "plan9 fast plan9 inner /z restored environment" run.log || fail "the values in scope" run.log -echo "PASS: 1036 a context states the engine's values in-process" diff --git a/tests/e2e/1051_a_cross_targets_toolchain_section_is_not_the_build_programs.sh b/tests/e2e/1051_a_cross_targets_toolchain_section_is_not_the_build_programs.sh deleted file mode 100755 index 1e7b747be..000000000 --- a/tests/e2e/1051_a_cross_targets_toolchain_section_is_not_the_build_programs.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash -# requires: elf llvm gcc musl -source "$(dirname "${BASH_SOURCE[0]}")/_toolchain_env.sh" -# 1051 -- a cross target's `[target.] toolchain` names the target's compiler, not the one the -# build program is compiled with. -# -# A build program is compiled and run on this machine. `[target.x86_64-linux-musl] toolchain = -# "gcc@16.1.0"` replaces the project's `[toolchain]` for the target; the build program takes the -# spec it replaced, as it takes the one a row's pin replaced (657). Measured on mcpp's own tree, -# built on Windows for x86_64-linux-musl: the build program was compiled by the musl cross gcc and -# the host could not start it (error 8, executable format error). -# -# Criteria: under `--target x86_64-linux-musl` the build program's compiler is the `[toolchain]` -# one (llvm), the target's is the section's (gcc), and the build program runs. -set -e -t=$(mktemp -d); trap 'rm -rf "$t"' EXIT -fail() { echo "FAIL: $1"; [ -n "${2:-}" ] && printf '%s\n' "$2" | tail -20; exit 1; } -[ "$(uname -m)" = x86_64 ] || { echo "SKIP: an x86_64 host"; exit 77; } -mkdir -p "$t/src" && cd "$t" -cat > mcpp.toml < build.mcpp <<'EOF' -import std; -import mcpp; -int main() { - std::ofstream(std::string(mcpp::out_dir()) + "/ran.txt") << "ran\n"; -} -EOF -echo 'int main() { return 0; }' > src/main.cpp -out=$("$MCPP" build --target x86_64-linux-musl 2>&1) || fail "the cross build failed" "$out" -grep -q 'host toolchain for build.mcpp: llvm' <<<"$out" \ - || grep -Eq 'Resolved host toolchain for build.mcpp: clang' <<<"$out" \ - || fail "the build program is not compiled by the [toolchain] compiler" "$out" -grep -q 'x86_64-linux-musl-g++' <<<"$out" || fail "the target is not compiled by the section's gcc" "$out" -find target -name ran.txt | grep -q . || fail "the build program did not run" "$(find target -maxdepth 4 | head)" -echo " ok the build program takes the [toolchain] compiler; the target takes its section's" -echo "PASS: 1051 a cross target's toolchain section is not the build program's" diff --git a/tests/e2e/110_build_mcpp_env_contract.sh b/tests/e2e/110_build_mcpp_env_contract.sh deleted file mode 100755 index b31a60eae..000000000 --- a/tests/e2e/110_build_mcpp_env_contract.sh +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# G3: build.mcpp environment contract — MCPP_TARGET/HOST/PROFILE/OUT_DIR/ -# MANIFEST_DIR/FEATURES injected into the child; contract values are part of -# the cache key (a feature change re-runs the program; an identical build -# hits the cache). -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT - -cd "$TMP" -"$MCPP" new envbp > /dev/null -cd envbp - -cat > build.mcpp <<'EOF' -#include -#include -#include -#include -static const char* env_or(const char* n) { const char* v = std::getenv(n); return v ? v : ""; } -int main() { - std::ofstream f("src/contract_gen.cpp"); - f << "extern \"C\" const char* bp_target() { return \"" << env_or("MCPP_TARGET") << "\"; }\n"; - f << "extern \"C\" const char* bp_host() { return \"" << env_or("MCPP_HOST") << "\"; }\n"; - f << "extern \"C\" const char* bp_profile() { return \"" << env_or("MCPP_PROFILE") << "\"; }\n"; - f << "extern \"C\" const char* bp_tos() { return \"" << env_or("MCPP_TARGET_OS") << "\"; }\n"; - f << "extern \"C\" const char* bp_tarch() { return \"" << env_or("MCPP_TARGET_ARCH") << "\"; }\n"; - // ENV may legitimately be empty (macOS) — assert set-ness, not content. - f << "extern \"C\" int bp_tenv_set() { return " - << (std::getenv("MCPP_TARGET_ENV") ? 1 : 0) << "; }\n"; - f << "extern \"C\" const char* bp_features() { return \"" << env_or("MCPP_FEATURES") << "\"; }\n"; - f << "extern \"C\" int bp_has_extra() { return " - << (std::getenv("MCPP_FEATURE_EXTRA") ? 1 : 0) << "; }\n"; - if (!f) return 1; - // OUT_DIR must exist and be writable. - const char* out = std::getenv("MCPP_OUT_DIR"); - if (!out) { std::fprintf(stderr, "MCPP_OUT_DIR unset\n"); return 1; } - std::ofstream probe(std::string(out) + "/probe.txt"); - if (!probe) { std::fprintf(stderr, "MCPP_OUT_DIR not writable\n"); return 1; } - std::printf("mcpp:generated=src/contract_gen.cpp\n"); - return 0; -} -EOF - -cat > src/main.cpp <<'EOF' -import std; -extern "C" const char* bp_target(); -extern "C" const char* bp_host(); -extern "C" const char* bp_profile(); -extern "C" const char* bp_features(); -extern "C" int bp_has_extra(); -extern "C" const char* bp_tos(); -extern "C" const char* bp_tarch(); -extern "C" int bp_tenv_set(); -int main() { - std::println("target={} host={} profile={} features=[{}] extra={} tos={} tarch={} tenvset={}", - bp_target(), bp_host(), bp_profile(), bp_features(), bp_has_extra(), - bp_tos(), bp_tarch(), bp_tenv_set()); - return 0; -} -EOF - -cat > mcpp.toml <<'EOF' -[package] -name = "envbp" -version = "0.1.0" - -[features] -default = [] -extra = [] -EOF - -"$MCPP" build > build1.log 2>&1 || { cat build1.log; echo "build 1 failed"; exit 1; } -out="$("$MCPP" run 2>&1 | tail -1)" -host_triple_re='[a-z0-9_]+-(linux|macos|windows)(-[a-z]+)?' -[[ "$out" =~ target=$host_triple_re\ host=$host_triple_re\ profile=dev\ features=\[\]\ extra=0 ]] || { - echo "unexpected contract values: $out"; exit 1; } -# MCPP_TARGET_OS/ARCH/ENV splits: OS is one of the canonical spellings, ARCH is -# non-empty, ENV is always SET (may be "" — no env segment on macOS) and must -# agree with the full triple's segments. -[[ "$out" =~ tos=(linux|macos|windows)\ tarch=([a-z0-9_]+)\ tenvset=1 ]] || { - echo "target split vars missing/odd: $out"; exit 1; } -tos="${BASH_REMATCH[1]}"; tarch="${BASH_REMATCH[2]}" -[[ "$out" == *"target=${tarch}-${tos}"* ]] || { - echo "target split disagrees with MCPP_TARGET: $out"; exit 1; } - -# Identical build → no re-run (either the whole-build fast path short-circuits -# before build.mcpp, or the build.mcpp cache hits — both are fine; a "running" -# line is not). -"$MCPP" build > build2.log 2>&1 -grep -qE "^ *build\.mcpp .* ran [0-9]" build2.log && { - cat build2.log; echo "identical build re-ran build.mcpp"; exit 1; } || true - -# Feature change → contract hash changes → re-run, and the program observes -# the feature. -"$MCPP" build --features extra > build3.log 2>&1 || { cat build3.log; echo "build 3 failed"; exit 1; } -grep -qE "^ *build\.mcpp .* ran [0-9]" build3.log || { - cat build3.log; echo "feature change did not re-run build.mcpp"; exit 1; } -bin="$(ls -t $(find target -name 'envbp' -type f) | head -1)" -out="$("./$bin" | tail -1)" -[[ "$out" == *"features=[extra] extra=1"* ]] || { - echo "feature not visible to build.mcpp: $out"; exit 1; } - -# Profile change also re-runs. -"$MCPP" build --release > build4.log 2>&1 || { cat build4.log; echo "build 4 failed"; exit 1; } -grep -qE "^ *build\.mcpp .* ran [0-9]" build4.log || { - cat build4.log; echo "profile change did not re-run build.mcpp"; exit 1; } -bin="$(ls -t $(find target -name 'envbp' -type f) | head -1)" -out="$("./$bin" | tail -1)" -[[ "$out" == *"profile=release"* ]] || { echo "profile not visible: $out"; exit 1; } - -echo "OK" diff --git a/tests/e2e/111_dep_build_mcpp.sh b/tests/e2e/111_dep_build_mcpp.sh deleted file mode 100755 index 01f247432..000000000 --- a/tests/e2e/111_dep_build_mcpp.sh +++ /dev/null @@ -1,88 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# G2: a dependency's build.mcpp runs (Cargo build.rs model) with Cargo scope: -# its cfg define reaches ONLY the dep's own TUs; its generated source (written -# to MCPP_OUT_DIR) compiles into the dep; artifacts live in the CONSUMING -# project's tree — the dep root itself is never written to. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT - -mkdir -p "$TMP/deplib/src" "$TMP/app/src" - -cat > "$TMP/deplib/mcpp.toml" <<'EOF' -[package] -name = "deplib" -version = "0.1.0" - -[targets.deplib] -kind = "lib" -EOF - -cat > "$TMP/deplib/build.mcpp" <<'EOF' -#include -#include -#include -#include -int main() { - const char* out = std::getenv("MCPP_OUT_DIR"); - if (!out) { std::fprintf(stderr, "MCPP_OUT_DIR unset\n"); return 1; } - std::ofstream f(std::string(out) + "/gen_dep.cpp"); - f << "extern \"C\" int dep_gen_answer() { return 77; }\n"; - if (!f) return 1; - std::printf("mcpp:generated=gen_dep.cpp\n"); - std::printf("mcpp:cfg=DEP_HAS_BP\n"); - return 0; -} -EOF - -cat > "$TMP/deplib/src/dep.cpp" <<'EOF' -#ifndef DEP_HAS_BP -#error "dep build.mcpp define did not reach the dep's own TU" -#endif -extern "C" int dep_gen_answer(); -extern "C" int dep_answer() { return dep_gen_answer(); } -EOF - -cat > "$TMP/app/mcpp.toml" <<'EOF' -[package] -name = "app" -version = "0.1.0" - -[dependencies.deplib] -path = "../deplib" -EOF - -cat > "$TMP/app/src/main.cpp" <<'EOF' -#ifdef DEP_HAS_BP -#error "dep build.mcpp define leaked into the consumer TU" -#endif -import std; -extern "C" int dep_answer(); -int main() { - std::println("dep = {}", dep_answer()); - return 0; -} -EOF - -# Snapshot the dep root so we can prove it is never written to. -before="$(find "$TMP/deplib" | sort)" - -cd "$TMP/app" -"$MCPP" build > build.log 2>&1 || { cat build.log; echo "build failed"; exit 1; } -out="$("$MCPP" run 2>&1 | tail -1)" -[[ "$out" == "dep = 77" ]] || { echo "unexpected output: $out"; exit 1; } - -# Dep root untouched (no target/, no generated files, nothing). -after="$(find "$TMP/deplib" | sort)" -[[ "$before" == "$after" ]] || { - diff <(echo "$before") <(echo "$after") || true - echo "dependency root was written to"; exit 1; } - -# Artifacts live in the consuming project: the program in .build-mcpp/deps, -# its output in this configuration's directory (mcpp 2026.10.10.1+). -find target/.build-mcpp/out -path '*deplib/gen_dep.cpp' | grep -q . || { - echo "dep OUT_DIR generated file not in project tree"; exit 1; } - -echo "OK" diff --git a/tests/e2e/112_build_mcpp_cross.sh b/tests/e2e/112_build_mcpp_cross.sh deleted file mode 100755 index c99ec03ef..000000000 --- a/tests/e2e/112_build_mcpp_cross.sh +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc mingw-cross -# G3 (cross): build.mcpp under `--target x86_64-windows-gnu` is no longer -# skipped — it compiles AND runs on the host (host-resolved toolchain) and -# sees MCPP_TARGET = the cross triple while MCPP_HOST stays the host. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT - -cd "$TMP" -"$MCPP" new crossbp > /dev/null -cd crossbp - -cat > build.mcpp <<'EOF' -#include -#include -#include -static const char* env_or(const char* n) { const char* v = std::getenv(n); return v ? v : ""; } -int main() { - std::ofstream f("src/cross_gen.cpp"); - f << "extern \"C\" const char* bp_target() { return \"" << env_or("MCPP_TARGET") << "\"; }\n"; - if (!f) return 1; - std::printf("mcpp:generated=src/cross_gen.cpp\n"); - return 0; -} -EOF - -cat > src/main.cpp <<'EOF' -import std; -extern "C" const char* bp_target(); -int main() { - std::println("bp target = {}", bp_target()); - return 0; -} -EOF - -cat > mcpp.toml <<'EOF' -[package] -name = "crossbp" -version = "0.1.0" -EOF - -"$MCPP" build --target x86_64-windows-gnu > build.log 2>&1 || { - cat build.log; echo "cross build failed"; exit 1; } -# Not skipped anymore. -grep -qi "skipped under a cross" build.log && { - echo "build.mcpp still skipped under cross"; exit 1; } || true -# The generated source baked in the CROSS triple (contract value), and the -# produced artifact is a PE binary. -grep -q 'x86_64-windows-gnu' src/cross_gen.cpp || { - cat src/cross_gen.cpp; echo "MCPP_TARGET was not the cross triple"; exit 1; } -exe="$(find target -name 'crossbp.exe' | head -1)" -[[ -n "$exe" ]] || { echo "no PE artifact produced"; exit 1; } - -# Wine run (when available) proves the full loop. -if command -v wine &>/dev/null; then - out="$(wine "$exe" 2>/dev/null | tr -d '\r' | tail -1)" - [[ "$out" == "bp target = x86_64-windows-gnu" ]] || { - echo "unexpected wine output: $out"; exit 1; } -fi - -# ── host≠target for `import std;` in build.mcpp ──────────────────────────── -# The std module staged for a build.mcpp must be the HOST one. Feeding it the -# target's would produce a helper that cannot execute here, and the failure is -# silent until exec time — the same class of mistake the mingw-cross work had -# to fix in four separate places. A cross build is the only configuration -# where host and target BMIs differ, so this is the one place it can be -# caught. -cat > build.mcpp <<'EOF' -import std; -int main() { - std::ofstream f("src/cross_gen.cpp"); - f << "extern \"C\" const char* bp_target() { return \"" - << (std::getenv("MCPP_TARGET") ? std::getenv("MCPP_TARGET") : "") - << "\"; }\n"; - if (!f) return 1; - std::println("mcpp:generated=src/cross_gen.cpp"); - return 0; -} -EOF - -rm -f src/cross_gen.cpp -"$MCPP" build --target x86_64-windows-gnu > build-std.log 2>&1 || { - cat build-std.log; echo "cross build with import std in build.mcpp failed"; exit 1; } -# The helper actually RAN on the host — proven by the file it was asked to -# write, not by the compiler's exit code. -[[ -f src/cross_gen.cpp ]] || { - cat build-std.log; echo "import-std build.mcpp did not run on the host"; exit 1; } -grep -q 'x86_64-windows-gnu' src/cross_gen.cpp || { - cat src/cross_gen.cpp; echo "MCPP_TARGET wrong under import std"; exit 1; } - -echo "OK" diff --git a/tests/e2e/125_build_mcpp_dep_dir.sh b/tests/e2e/125_build_mcpp_dep_dir.sh deleted file mode 100755 index 8e24ae84e..000000000 --- a/tests/e2e/125_build_mcpp_dep_dir.sh +++ /dev/null @@ -1,97 +0,0 @@ -#!/usr/bin/env bash -# requires: elf gcc -# mcpp#241: build.mcpp env contract exposes each resolved dependency's install -# dir as MCPP_DEP__DIR (read via mcpp::dep_dir("")), so a -# package's build.mcpp can locate a dependency's payload (e.g. a data-asset -# package) by name instead of reverse-engineering the store layout. Scenario: -# consumer -> pkgp (has build.mcpp) -> datad (data package). pkgp's build.mcpp -# reads dep_dir("datad") and generates a source returning it; consumer prints it. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -# data-asset dependency -mkdir -p datad/src -echo 'int datad_touch() { return 1; }' > datad/src/d.cpp -cat > datad/mcpp.toml <<'EOF' -[package] -name = "datad" -version = "0.1.0" -[modules] -sources = ["src/**/*.cpp"] -[targets.datad] -kind = "lib" -EOF - -# middle package with a build.mcpp that locates datad's dir -mkdir -p pkgp/src -cat > pkgp/src/lib.cpp <<'EOF' -extern const char* dep_datad_dir(); -const char* pkgp_datad_dir() { return dep_datad_dir(); } -EOF -cat > pkgp/build.mcpp <<'EOF' -#include -#include -import mcpp; -int main() { - const char* d = mcpp::dep_dir("datad"); - if (d == nullptr || d[0] == '\0') { - std::fprintf(stderr, "build.mcpp: MCPP_DEP_DATAD_DIR not set\n"); - return 1; // fail loudly if the contract var is missing - } - std::string out = std::string(mcpp::out_dir()) + "/depdir.cpp"; - std::FILE* f = std::fopen(out.c_str(), "w"); - std::fprintf(f, "extern const char* dep_datad_dir();\n"); - std::fprintf(f, "const char* dep_datad_dir() { return \"%s\"; }\n", d); - std::fclose(f); - mcpp::generated("depdir.cpp"); - return 0; -} -EOF -cat > pkgp/mcpp.toml <<'EOF' -[package] -name = "pkgp" -version = "0.1.0" -[modules] -sources = ["src/**/*.cpp"] -[dependencies] -datad = { path = "../datad" } -[targets.pkgp] -kind = "lib" -EOF - -# root consumer -mkdir -p consumer/src -cat > consumer/src/main.cpp <<'EOF' -import std; -extern const char* pkgp_datad_dir(); -int main() { - std::println("DEPDIR={}", pkgp_datad_dir()); - return 0; -} -EOF -cat > consumer/mcpp.toml <<'EOF' -[package] -name = "consumer" -version = "0.1.0" -[modules] -sources = ["src/**/*.cpp"] -[dependencies] -pkgp = { path = "../pkgp" } -[targets.consumer] -kind = "bin" -main = "src/main.cpp" -EOF - -cd consumer -"$MCPP" build > build.log 2>&1 || { cat build.log; echo "FAIL: build failed"; exit 1; } - -out="$("$MCPP" run 2>&1 | grep '^DEPDIR=' | tail -1)" -dir="${out#DEPDIR=}" -[[ -n "$dir" ]] || { echo "FAIL: empty dep dir (contract var missing)"; exit 1; } -[[ -d "$dir" ]] || { echo "FAIL: dep dir does not exist: $dir"; exit 1; } -[[ "$(basename "$dir")" == "datad" ]] || { echo "FAIL: dep dir is not datad's root: $dir"; exit 1; } - -echo "OK" diff --git a/tests/e2e/139_build_program_advisory.sh b/tests/e2e/139_build_program_advisory.sh deleted file mode 100755 index c4b96e1e4..000000000 --- a/tests/e2e/139_build_program_advisory.sh +++ /dev/null @@ -1,138 +0,0 @@ -#!/usr/bin/env bash -# requires: unix-shell -# resources: machine -# A build program that succeeds and still says something. -# -# THE SECOND BUILD IS THE TEST, NOT THE FIRST. -# -# mcpp prints what it captured from a build program only when that program -# EXITS NON-ZERO, which is why `mcpp:warning=` had to exist at all. But the -# result of a build program is CACHED, and a cache hit does not re-run it — so -# an implementation that emitted the advisory only where the program was -# actually executed would print it on a project's first build and never again. -# -# That failure mode is worse than having no channel: an advisory that stops -# appearing reads as "the condition was resolved". It is also invisible to any -# test that builds once, which is what a test written the obvious way does. -# -# So the assertions below are ordered: first build, then a SECOND build that -# must be a cache hit and must still carry the same line. -# -# This test was confirmed to FAIL before the fix — the cache-hit emission -# was removed and the second-build assertion went red while the first stayed -# green. A test for a replay path that has never been observed failing cannot -# distinguish "replay works" from "the cache never hit". -# `"$MCPP"`, NEVER A BARE `mcpp`. The harness passes the binary under test; -# a bare name resolves through PATH to whichever engine happens to be installed, -# and this test would then pass against an engine that does not have the feature. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -mkdir -p p/src -cd p - -cat > mcpp.toml <<'EOF' -[package] -name = "advisory" -version = "0.1.0" -EOF - -cat > src/main.cpp <<'EOF' -int main() { return 0; } -EOF - -# The realistic shape: the program looked for something optional, did not find -# it, configured nothing that depends on it, and says so. It exits 0 — the -# build is correct, the user is merely missing a capability. -cat > build.mcpp <<'EOF' -import mcpp; -import std; -int main() { - mcpp::warning("no emulator found; `xlings install qemu-riscv -y` enables `mcpp run`"); - mcpp::define("ADVISORY_RAN"); - return 0; -} -EOF - -# ── 1. The advisory reaches the user on a build that SUCCEEDED ────────────── -"$MCPP" build > first.log 2>&1 -grep -q "no emulator found" first.log \ - || { cat first.log; echo "FAIL: the advisory did not reach the output"; exit 1; } - -# Prefixed with the package it came from. A workspace has several build -# programs and an unattributed sentence sends the reader to the wrong manifest. -grep -q "advisory: no emulator found" first.log \ - || { cat first.log; echo "FAIL: the advisory was not attributed to its package"; exit 1; } - -# The build succeeded. An advisory is not an error. -grep -qi "Finished" first.log \ - || { cat first.log; echo "FAIL: the build did not finish"; exit 1; } - -# ── 2. THE SECOND BUILD. The program does not run; the advisory must ───── -# -# `touch` FIRST, AND NOT AS A SUPERSTITION. A build with nothing at all to -# do takes a WHOLE-PROJECT fast path that prints one line and never reaches the -# build.mcpp stage — so an unmodified second build exercises neither the run -# path nor the replay path, and asserting against it would test the fast path -# instead. This repository has been bitten by that masking before. -# -# Touching a source makes the project stale while leaving the build program's -# own declared inputs unchanged, which is exactly the state the replay serves. -# -# The boundary this leaves is deliberate and is documented in docs/07: an -# advisory appears whenever the build.mcpp stage is reached. A no-op build -# reaches nothing and prints nothing — including this. It is not asserted here, -# because it is a fact about the fast path rather than about this feature. -touch src/main.cpp -# A reused program is stated under --verbose only (build output design revision 3, §7.1). -"$MCPP" build -v > second.log 2>&1 - -# First establish that this build really was a cache hit. Without this the next -# assertion could pass for the wrong reason — a re-run would also print the -# line, and the replay path would go untested. -grep -qE "^ *build\.mcpp .* cached" second.log \ - || { cat second.log; echo "FAIL: the second build re-ran the program; the replay path was not exercised"; exit 1; } - -grep -q "advisory: no emulator found" second.log \ - || { cat second.log; echo "FAIL: the advisory was lost on a cache hit — it would appear once and never again"; exit 1; } - -# ── 3. Two members, two advisories, each named ───────────────────────────── -# -# Attribution is the reason the package name is passed in rather than printed -# by the program: in a workspace the program cannot reliably spell which member -# it is. -cd "$TMP" -mkdir -p ws/a/src ws/b/src -cd ws - -cat > mcpp.toml <<'EOF' -[workspace] -members = ["a", "b"] -EOF - -for m in a b; do - cat > $m/mcpp.toml < $m/src/main.cpp <<'EOF' -int main() { return 0; } -EOF - cat > $m/build.mcpp < ws.log 2>&1 -grep -q "a: advisory from a" ws.log \ - || { cat ws.log; echo "FAIL: member a's advisory is missing or unattributed"; exit 1; } -grep -q "b: advisory from b" ws.log \ - || { cat ws.log; echo "FAIL: member b's advisory is missing or unattributed"; exit 1; } - -echo "OK 139_build_program_advisory" diff --git a/tests/e2e/143_build_mcpp_source_directive.sh b/tests/e2e/143_build_mcpp_source_directive.sh deleted file mode 100755 index 3e916d6b3..000000000 --- a/tests/e2e/143_build_mcpp_source_directive.sh +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# P1 (large-source-pkg design §3.1): `mcpp:source=` selects a PRE-EXISTING -# source file into the compile set — the proper name for the old `generated=` -# absolute-path grey usage. The file lives in the package tree, is NOT matched -# by any [build]/[modules] sources glob, and the program did not write it. -# Also covers: the typed `mcpp::source(...)` emitter, and the `d source` cache -# record round-trip (a rebuild that replays the cache must still compile it). -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT - -cd "$TMP" -mkdir -p srcsel/src srcsel/extra -cd srcsel - -# Pre-existing payload file OUTSIDE the sources globs (extra/, not src/). -cat > extra/impl.cpp <<'EOF' -extern "C" int selected_value() { return 42; } -EOF - -# build.mcpp selects it via the typed API (relative to the package root). -cat > build.mcpp <<'EOF' -import mcpp; -int main() { - mcpp::source("extra/impl.cpp"); - return 0; -} -EOF - -cat > src/main.cpp <<'EOF' -import std; -extern "C" int selected_value(); -int main() { - std::println("SELECTED={}", selected_value()); - return 0; -} -EOF - -cat > mcpp.toml <<'EOF' -[package] -name = "srcsel" -version = "0.1.0" - -[modules] -sources = ["src/**/*.cpp"] - -[targets.srcsel] -kind = "bin" -main = "src/main.cpp" -EOF - -"$MCPP" build > build1.log 2>&1 || { cat build1.log; echo "FAIL: build failed"; exit 1; } -grep -q "ignoring unknown directive" build1.log && { - cat build1.log; echo "FAIL: mcpp:source= not recognized"; exit 1; } || true - -out="$("$MCPP" run 2>&1 | grep '^SELECTED=' | tail -1)" -[[ "$out" == "SELECTED=42" ]] || { echo "FAIL: selected source not linked: $out"; exit 1; } - -# Cache round-trip: touch main.cpp (invalidates the ninja build, not the -# build.mcpp inputs) → the cached `d source` record must reapply so the -# selected file stays in the compile set without a re-run. -cat > src/main.cpp <<'EOF' -import std; -extern "C" int selected_value(); -int main() { - std::println("SELECTED2={}", selected_value()); - return 0; -} -EOF -"$MCPP" build > build2.log 2>&1 || { cat build2.log; echo "FAIL: rebuild failed"; exit 1; } -grep -qE "^ *build\.mcpp .* ran [0-9]" build2.log && { - cat build2.log; echo "FAIL: unchanged build.mcpp re-ran"; exit 1; } || true -out="$("$MCPP" run 2>&1 | grep '^SELECTED2=' | tail -1)" -[[ "$out" == "SELECTED2=42" ]] || { echo "FAIL: cached source record lost: $out"; exit 1; } - -# A source= pointing at a missing file is a hard, actionable error. -cat > build.mcpp <<'EOF' -import mcpp; -int main() { - mcpp::source("extra/nope.cpp"); - return 0; -} -EOF -"$MCPP" build > build3.log 2>&1 && { cat build3.log; echo "FAIL: missing source did not fail"; exit 1; } -grep -q "selected source" build3.log || { - cat build3.log; echo "FAIL: missing-source error not surfaced"; exit 1; } - -echo "OK" diff --git a/tests/e2e/144_build_mcpp_include_dir.sh b/tests/e2e/144_build_mcpp_include_dir.sh deleted file mode 100755 index 3da15f7b9..000000000 --- a/tests/e2e/144_build_mcpp_include_dir.sh +++ /dev/null @@ -1,91 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# P1 (large-source-pkg design §3.1): `mcpp:include-dir=` adds a private include -# directory for the package's own TUs — replacing the old double-emission hack -# (`mcpp:cxxflag=-I…` + `mcpp:cflag=-I…`, unnormalized). Scenario: build.mcpp -# writes a header under MCPP_OUT_DIR and points include-dir at it (absolute); -# a source #includes it; the build succeeds with NO -I flag emitted manually. -# Also: `mcpp:include-dir-after=` rides the typed #249 channel end-to-end — -# the emitted build.ninja must carry -idirafter for the directive dir — and -# both directives round-trip the directive cache. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT - -cd "$TMP" -mkdir -p incdir/src -cd incdir - -cat > build.mcpp <<'EOF' -#include -#include -#include -#include -#include -import mcpp; -int main() { - std::string inc = std::string(mcpp::out_dir()) + "/inc"; - std::filesystem::create_directories(inc); - std::ofstream(inc + "/bp_config.h") << "#define BP_ANSWER 42\n"; - mcpp::include_dir(inc.c_str()); // absolute - mcpp::include_dir_after("vendor/sysinc"); // relative -> package root; accepted - return 0; -} -EOF -mkdir -p vendor/sysinc - -cat > src/main.cpp <<'EOF' -import std; -#include -int main() { - std::println("ANSWER={}", BP_ANSWER); - return 0; -} -EOF - -cat > mcpp.toml <<'EOF' -[package] -name = "incdir" -version = "0.1.0" - -[modules] -sources = ["src/**/*.cpp"] - -[targets.incdir] -kind = "bin" -main = "src/main.cpp" -EOF - -"$MCPP" build > build1.log 2>&1 || { cat build1.log; echo "FAIL: build failed"; exit 1; } -grep -q "ignoring unknown directive" build1.log && { - cat build1.log; echo "FAIL: include-dir directive not recognized"; exit 1; } || true - -out="$("$MCPP" run 2>&1 | grep '^ANSWER=' | tail -1)" -[[ "$out" == "ANSWER=42" ]] || { echo "FAIL: include dir not on the -I chain: $out"; exit 1; } - -# include-dir-after reaches the compile edges as -idirafter (typed #249 -# channel: privateBuild.includeDirsAfter → localIncludeDirsAfter). -ninja_file=$(find target -name build.ninja -not -path '*/.build-mcpp/*' | head -1) -grep -q -- "-idirafter.*vendor/sysinc" "$ninja_file" || { - echo "FAIL: include-dir-after not emitted as -idirafter in build.ninja"; - grep -n "local_includes" "$ninja_file" | head; exit 1; } - -# Cache round-trip: touch a source (defeats the whole-build fast path, keeps -# build.mcpp inputs unchanged) → the cached include-dir record must reapply -# without a re-run, and the header must still resolve. -cat > src/main.cpp <<'EOF' -import std; -#include -int main() { - std::println("ANSWER2={}", BP_ANSWER); - return 0; -} -EOF -"$MCPP" build > build2.log 2>&1 || { cat build2.log; echo "FAIL: rebuild failed"; exit 1; } -grep -qE "^ *build\.mcpp .* ran [0-9]" build2.log && { - cat build2.log; echo "FAIL: unchanged build.mcpp re-ran"; exit 1; } || true -out="$("$MCPP" run 2>&1 | grep '^ANSWER2=' | tail -1)" -[[ "$out" == "ANSWER2=42" ]] || { echo "FAIL: cached include-dir record lost: $out"; exit 1; } - -echo "OK" diff --git a/tests/e2e/145_root_build_mcpp_dep_dirs.sh b/tests/e2e/145_root_build_mcpp_dep_dirs.sh deleted file mode 100755 index eae79b07e..000000000 --- a/tests/e2e/145_root_build_mcpp_dep_dirs.sh +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# P1 (large-source-pkg design §3.1 item 4): the ROOT project's build.mcpp now -# runs AFTER dependency resolution and receives MCPP_DEP__DIR exactly -# like a dependency's build.mcpp does (mcpp#241 contract, previously dep-only: -# the root ran before resolution and saw none). Scenario mirrors -# 125_build_mcpp_dep_dir.sh, but the build.mcpp asserting dep_dir() is the -# ROOT's own: root -> datad (path dep); root build.mcpp reads -# dep_dir("datad"), fails loudly if unset, and generates a source returning -# it; main prints it; the test asserts it is datad's real root. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -# data-asset dependency -mkdir -p datad/src -echo 'int datad_touch() { return 1; }' > datad/src/d.cpp -cat > datad/mcpp.toml <<'EOF' -[package] -name = "datad" -version = "0.1.0" -[modules] -sources = ["src/**/*.cpp"] -[targets.datad] -kind = "lib" -EOF - -# ROOT project whose own build.mcpp locates datad's dir -mkdir -p rootp/src -cat > rootp/build.mcpp <<'EOF' -#include -#include -import mcpp; -int main() { - const char* d = mcpp::dep_dir("datad"); - if (d == nullptr || d[0] == '\0') { - std::fprintf(stderr, "build.mcpp: MCPP_DEP_DATAD_DIR not set for the ROOT\n"); - return 1; // the moved call point must expose the contract var - } - std::string out = "src/depdir.cpp"; - std::FILE* f = std::fopen(out.c_str(), "w"); - std::fprintf(f, "const char* root_datad_dir() { return \"%s\"; }\n", d); - std::fclose(f); - mcpp::generated("src/depdir.cpp"); - return 0; -} -EOF -cat > rootp/src/main.cpp <<'EOF' -import std; -extern const char* root_datad_dir(); -int main() { - std::println("DEPDIR={}", root_datad_dir()); - return 0; -} -EOF -cat > rootp/mcpp.toml <<'EOF' -[package] -name = "rootp" -version = "0.1.0" -[modules] -sources = ["src/**/*.cpp"] -[dependencies] -datad = { path = "../datad" } -[targets.rootp] -kind = "bin" -main = "src/main.cpp" -EOF - -cd rootp -"$MCPP" build > build.log 2>&1 || { cat build.log; echo "FAIL: build failed"; exit 1; } - -out="$("$MCPP" run 2>&1 | grep '^DEPDIR=' | tail -1)" -dir="${out#DEPDIR=}" -[[ -n "$dir" ]] || { echo "FAIL: empty dep dir (root contract var missing)"; exit 1; } -[[ -d "$dir" ]] || { echo "FAIL: dep dir does not exist: $dir"; exit 1; } -[[ "$(basename "$dir")" == "datad" ]] || { echo "FAIL: dep dir is not datad's root: $dir"; exit 1; } - -echo "OK" diff --git a/tests/e2e/181_build_mcpp_import_std.sh b/tests/e2e/181_build_mcpp_import_std.sh deleted file mode 100755 index ee70ed5a6..000000000 --- a/tests/e2e/181_build_mcpp_import_std.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash -# requires: unix-shell -# 181_build_mcpp_import_std.sh — build.mcpp can `import std;` -# -# mcpp asks projects to `import std;` everywhere, then made their build script -# fall back to `#include` — there was no std BMI channel in the build.mcpp -# compile at all. This locks the gap shut. Cross-platform on purpose: -# `import std;` is not a Windows-specific concern, and running it on Linux -# gives the fastest feedback. -set -e - -TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT -cd "$TMP" -"$MCPP" new imp_std >/dev/null 2>&1 -cd imp_std - -# 1) `import std;` alone — the container/algorithm/format surface a real build -# script reaches for, none of which is available without the std module. -cat > build.mcpp <<'EOF' -import std; -int main() { - std::vector defines{"MCPP_FROM_IMPORT_STD", "MCPP_STD_COUNT_2"}; - std::ranges::sort(defines); - for (auto const& d : defines) std::println("mcpp:cfg={}", d); - std::println("mcpp:rerun-if-changed=build.mcpp"); - return 0; -} -EOF - -cat > src/main.cpp <<'EOF' -import std; -int main() { -#if defined(MCPP_FROM_IMPORT_STD) && defined(MCPP_STD_COUNT_2) - std::println("import-std-ok"); - return 0; -#else - std::println("defines missing"); - return 1; -#endif -} -EOF - -out=$("$MCPP" build 2>&1) || { echo "FAIL: build with import std: $out"; exit 1; } -run_out=$("$MCPP" run 2>&1) || { echo "FAIL: run: $run_out"; exit 1; } -[[ "$run_out" == *"import-std-ok"* ]] \ - || { echo "FAIL: run output: $run_out"; exit 1; } - -# 2) `import std;` together with `import mcpp;` — both module channels active -# at once. These share the staged-BMI cwd, and an implementation that -# handles them as two independent conditions gets the cwd wrong for one. -cat > build.mcpp <<'EOF' -import std; -import mcpp; -int main() { - std::string tag = std::format("MCPP_BOTH_{}", 1 + 1); - mcpp::define(tag.c_str()); - mcpp::rerun_if_changed("build.mcpp"); - return 0; -} -EOF - -cat > src/main.cpp <<'EOF' -import std; -int main() { -#ifdef MCPP_BOTH_2 - std::println("both-modules-ok"); - return 0; -#else - std::println("define missing"); - return 1; -#endif -} -EOF - -out=$("$MCPP" build 2>&1) || { echo "FAIL: build with import std + mcpp: $out"; exit 1; } -run_out=$("$MCPP" run 2>&1) || { echo "FAIL: run (both): $run_out"; exit 1; } -[[ "$run_out" == *"both-modules-ok"* ]] \ - || { echo "FAIL: run output (both): $run_out"; exit 1; } - -# 3) An `#include`-only build.mcpp must still take the plain path — no std BMI -# staged, no -fmodules, cwd = project root. Regression guard: the naive -# detector ("does the text contain 'import std'") would fire on a comment. -cat > build.mcpp <<'EOF' -#include -// This program deliberately mentions import std; in a comment. -int main() { - std::puts("mcpp:cfg=MCPP_PLAIN_PATH"); - std::puts("mcpp:rerun-if-changed=build.mcpp"); - return 0; -} -EOF - -cat > src/main.cpp <<'EOF' -import std; -int main() { -#ifdef MCPP_PLAIN_PATH - std::println("plain-path-ok"); - return 0; -#else - return 1; -#endif -} -EOF - -out=$("$MCPP" build 2>&1) || { echo "FAIL: build plain build.mcpp: $out"; exit 1; } -run_out=$("$MCPP" run 2>&1) || { echo "FAIL: run (plain): $run_out"; exit 1; } -[[ "$run_out" == *"plain-path-ok"* ]] \ - || { echo "FAIL: run output (plain): $run_out"; exit 1; } - -echo "PASS: build.mcpp import std (alone, with import mcpp, and the plain path)" diff --git a/tests/e2e/186_build_mcpp_protocol_and_bound.sh b/tests/e2e/186_build_mcpp_protocol_and_bound.sh deleted file mode 100755 index a506d1e3e..000000000 --- a/tests/e2e/186_build_mcpp_protocol_and_bound.sh +++ /dev/null @@ -1,198 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 186_build_mcpp_protocol_and_bound.sh — the build.mcpp CONTRACT hardening: -# wire-protocol version, the cache's semantic epoch, and the run bound. -# -# What each part protects against: -# * protocol — a build.mcpp written for a newer mcpp used to have its -# unknown directives WARNED about and dropped, producing a -# silently different build. It must now refuse. -# * legacy — a hand-written printf program announces nothing; its surface -# is frozen, so its unknown keys stay a warning (a typo), not -# an error. This asymmetry is the compatibility contract and -# has to be pinned by a test or it will be "simplified" away. -# * epoch — a cache entry written under a different directive -# interpretation must not be replayed under this one. -# * run bound — a build program that hangs used to hang the whole build with -# no diagnostic at all. -# -# See .agents/docs/2026-08-30-build-mcpp-extensibility-architecture.md §4. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -mkdir -p app/src -cd app - -cat > mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" -EOF - -cat > src/main.cpp <<'EOF' -#ifndef FROM_BUILD_MCPP -#error "define missing" -#endif -int main() { return 0; } -EOF - -fresh() { rm -rf target; } - -# ── 1. import mcpp; announces a protocol, so an unknown directive is fatal ── -cat > build.mcpp <<'EOF' -#include -import mcpp; -int main() { - mcpp::cxxflag("-DFROM_BUILD_MCPP=1"); - std::printf("mcpp:no-such-directive=1\n"); -} -EOF -fresh -if "$MCPP" build > b1.log 2>&1; then - cat b1.log; echo "FAIL: unknown directive from an announcing program was accepted"; exit 1 -fi -grep -q "no-such-directive" b1.log || { - cat b1.log; echo "FAIL: error does not name the offending directive"; exit 1; } -# The message must explain WHY it is fatal here but not for a printf program, -# otherwise the asymmetry reads as a bug. -grep -qi "protocol" b1.log || { - cat b1.log; echo "FAIL: error does not mention the protocol"; exit 1; } - -# ── 2. A hand-written printf program keeps warn-and-ignore ────────────────── -cat > build.mcpp <<'EOF' -#include -int main() { - std::printf("mcpp:cxxflag=-DFROM_BUILD_MCPP=1\n"); - std::printf("mcpp:no-such-directive=1\n"); -} -EOF -fresh -"$MCPP" build > b2.log 2>&1 || { cat b2.log; echo "FAIL: legacy printf program was rejected"; exit 1; } -grep -q "ignoring unknown directive" b2.log || { - cat b2.log; echo "FAIL: legacy program lost its warn-and-ignore behaviour"; exit 1; } - -# ── 3. A program claiming a newer protocol is refused with an upgrade hint ── -cat > build.mcpp <<'EOF' -#include -int main() { - std::printf("mcpp:protocol=999\n"); - std::printf("mcpp:cxxflag=-DFROM_BUILD_MCPP=1\n"); -} -EOF -fresh -if "$MCPP" build > b3.log 2>&1; then - cat b3.log; echo "FAIL: a newer-protocol program was accepted"; exit 1 -fi -grep -q "999" b3.log || { cat b3.log; echo "FAIL: error does not name the claimed protocol"; exit 1; } -grep -qi "upgrade" b3.log || { cat b3.log; echo "FAIL: error gives no actionable next step"; exit 1; } - -# ── 4. Run bound: a hanging program is killed and the error says so ───────── -cat > build.mcpp <<'EOF' -#include -#include -int main() { - std::printf("mcpp:cxxflag=-DFROM_BUILD_MCPP=1\n"); - std::fflush(stdout); - for (;;) ::sleep(60); -} -EOF -fresh -start=$(date +%s) -if MCPP_BUILD_PROGRAM_TIMEOUT=3 "$MCPP" build > b4.log 2>&1; then - cat b4.log; echo "FAIL: a hanging build.mcpp did not fail the build"; exit 1 -fi -elapsed=$(( $(date +%s) - start )) -[ "$elapsed" -lt 60 ] || { echo "FAIL: the bound did not fire (took ${elapsed}s)"; exit 1; } -grep -q "time limit" b4.log || { cat b4.log; echo "FAIL: no timeout diagnostic"; exit 1; } -# It must name the package: in a workspace or a dependency graph, "build.mcpp -# hung" is useless without knowing whose. -grep -q "'app'" b4.log || { cat b4.log; echo "FAIL: timeout error does not name the package"; exit 1; } -grep -q "MCPP_BUILD_PROGRAM_TIMEOUT" b4.log || { - cat b4.log; echo "FAIL: timeout error does not say how to change the bound"; exit 1; } -# ...and WHICH mcpp.toml to edit. When a DEPENDENCY's build program times out, -# the user's instinct is to edit their own manifest, which changes nothing. -grep -q "mcpp.toml" b4.log || { - cat b4.log; echo "FAIL: timeout error does not name the manifest to edit"; exit 1; } - -# ── 4b. The bound is configurable from the manifest ───────────────────────── -# -# Deliberately 2 seconds, not something near the 600s default: the point is -# that the KEY is read, and a long value would just make CI wait. -cat >> mcpp.toml <<'EOF' - -[build] -build_program_timeout = 2 -EOF -fresh -start=$(date +%s) -if "$MCPP" build > b4b.log 2>&1; then - cat b4b.log; echo "FAIL: [build] build_program_timeout did not bound the run"; exit 1 -fi -elapsed=$(( $(date +%s) - start )) -[ "$elapsed" -lt 60 ] || { echo "FAIL: the manifest bound did not fire (took ${elapsed}s)"; exit 1; } -grep -q "exceeded its 2s time limit" b4b.log || { - cat b4b.log; echo "FAIL: the manifest value is not the effective bound"; exit 1; } - -# The env var is a per-invocation override and must WIN over the manifest. -fresh -if MCPP_BUILD_PROGRAM_TIMEOUT=1 "$MCPP" build > b4c.log 2>&1; then - cat b4c.log; echo "FAIL: build succeeded under a 1s bound"; exit 1 -fi -grep -q "exceeded its 1s time limit" b4c.log || { - cat b4c.log; echo "FAIL: MCPP_BUILD_PROGRAM_TIMEOUT did not override the manifest"; exit 1; } - -# A negative value is a manifest error, not a silently-ignored one: it would -# otherwise read as "no bound" and remove the guard entirely. -cp mcpp.toml mcpp.toml.bak -sed -i.tmp 's/^build_program_timeout = 2$/build_program_timeout = -1/' mcpp.toml -if "$MCPP" build > b4d.log 2>&1; then - cat b4d.log; echo "FAIL: a negative build_program_timeout was accepted"; exit 1 -fi -grep -q "build_program_timeout" b4d.log || { - cat b4d.log; echo "FAIL: the error does not name the key"; exit 1; } -mv mcpp.toml.bak mcpp.toml -rm -f mcpp.toml.tmp - -# Restore a manifest with no bound override for the sections that follow. -sed -i '/^\[build\]$/,$d' mcpp.toml - -# ── 5. Cache: the entry carries an epoch, and a foreign one invalidates it ── -cat > build.mcpp <<'EOF' -#include -int main() { std::printf("mcpp:cxxflag=-DFROM_BUILD_MCPP=1\n"); } -EOF -fresh -"$MCPP" build > b5.log 2>&1 || { cat b5.log; echo "FAIL: build failed"; exit 1; } -CACHE=target/.build-mcpp/build.mcpp.cache -[ -f "$CACHE" ] || { echo "FAIL: no build.mcpp cache written"; exit 1; } -grep -q '^epoch ' "$CACHE" || { cat "$CACHE"; echo "FAIL: cache carries no epoch"; exit 1; } -cp "$CACHE" "$TMP/good.cache" - -# Touching a source defeats the whole-project fast path so prepare (and with it -# the build.mcpp cache) actually runs. -touch src/main.cpp -# A reused program is stated under --verbose only (build output design revision 3, §7.1). -"$MCPP" build -v > b6.log 2>&1 || { cat b6.log; echo "FAIL: build failed"; exit 1; } -grep -qE "^ *build\.mcpp .* cached" b6.log || { - cat b6.log; echo "FAIL: an unchanged build.mcpp was re-run"; exit 1; } - -sed 's/^epoch .*/epoch 987654/' "$TMP/good.cache" > "$CACHE" -touch src/main.cpp -"$MCPP" build > b7.log 2>&1 || { cat b7.log; echo "FAIL: build failed"; exit 1; } -grep -qE "^ *build\.mcpp .* ran [0-9]" b7.log || { - cat b7.log; echo "FAIL: a foreign cache epoch did not force a re-run"; exit 1; } - -# A `d` record this mcpp cannot interpret (a cache written by a NEWER mcpp) -# must invalidate the entry too — replaying the rest would apply a strict -# subset of what the program asked for. -cp "$TMP/good.cache" "$CACHE" -echo "d some-future-tag /x" >> "$CACHE" -touch src/main.cpp -"$MCPP" build > b8.log 2>&1 || { cat b8.log; echo "FAIL: build failed"; exit 1; } -grep -qE "^ *build\.mcpp .* ran [0-9]" b8.log || { - cat b8.log; echo "FAIL: an unknown cache record did not force a re-run"; exit 1; } - -echo "OK" diff --git a/tests/e2e/187_dep_host_tool.sh b/tests/e2e/187_dep_host_tool.sh deleted file mode 100755 index af9b99958..000000000 --- a/tests/e2e/187_dep_host_tool.sh +++ /dev/null @@ -1,274 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 187_dep_host_tool.sh — #355: a dependency's `kind = "bin"` target, built for -# the HOST and handed to the consumer's build.mcpp. -# -# Why this cannot be done any other way: build.mcpp runs inside prepare, before -# the BuildPlan exists and long before build.ninja is written, so a tool -# produced by the main graph arrives too late to be called. The tool is -# therefore built by a nested, host-targeted sub-build into a global store. -# -# Covered here: -# 1. end to end — the tool is built, `mcpp::dep_bin()` finds it, the source it -# generates is compiled and linked -# 2. the cost gate — a tool target behind `required_features` is built because -# the sub-build ACTIVATES those features (in a tool sub-build the target is -# what was asked for, so its requirements are inputs, not a gate) -# 3. default-off — a consumer that does not ask gets nothing built -# 4. a bad tool name fails with the available targets listed -# 5. the override escape hatch skips the build entirely -# -# See .agents/docs/2026-08-05-issue355-dependency-host-tools-design.md. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -# Isolate the tool store so a stale entry from a previous run cannot make a -# broken build look green. -export MCPP_HOME="$TMP/mcpphome" -mkdir -p "$MCPP_HOME" -# Keep the real registry (toolchains are expensive) but not its build cache. -if [ -d "$HOME/.mcpp/registry" ]; then - ln -s "$HOME/.mcpp/registry" "$MCPP_HOME/registry" -fi - -# ── the tool package ──────────────────────────────────────────────────────── -mkdir -p toolpkg/src -cat > toolpkg/mcpp.toml <<'EOF' -[package] -name = "toolpkg" -version = "0.1.0" - -[build] -sources = ["src/lib.cpp"] - -[features.codegen] -sources = ["src/codegen.cpp"] - -[targets.codegen] -kind = "bin" -main = "src/codegen.cpp" -required_features = ["codegen"] -EOF -cat > toolpkg/src/lib.cpp <<'EOF' -int toolpkg_lib() { return 1; } -EOF -# A minimal "code generator": writes a C++ source whose function returns 42. -cat > toolpkg/src/codegen.cpp <<'EOF' -#include -#include -int main(int argc, char** argv) { - if (argc < 2) return 2; - FILE* f = std::fopen(argv[1], "w"); - if (!f) return 3; - std::fprintf(f, "int generated_answer() { return 42; }\n"); - std::fclose(f); - return 0; -} -EOF - -# ── the consumer ──────────────────────────────────────────────────────────── -mkdir -p app/src -cat > app/mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" - -[dependencies] -toolpkg = { path = "../toolpkg", tools = ["codegen"] } -EOF -cat > app/src/main.cpp <<'EOF' -#include -int generated_answer(); -int main() { std::printf("ANSWER=%d\n", generated_answer()); } -EOF -cat > app/build.mcpp <<'EOF' -#include -#include -#include -import mcpp; -int main() { - const char* tool = mcpp::dep_bin("toolpkg", "codegen"); - if (!tool || !*tool) { std::fprintf(stderr, "no tool path\n"); return 1; } - std::string out = std::string(mcpp::out_dir()) + "/gen.cpp"; - std::string cmd = std::string("\"") + tool + "\" \"" + out + "\""; - if (std::system(cmd.c_str()) != 0) { std::fprintf(stderr, "tool failed\n"); return 1; } - mcpp::generated(out.c_str()); -} -EOF - -cd app -"$MCPP" build > b1.log 2>&1 || { cat b1.log; echo "FAIL: build with a dep host tool failed"; exit 1; } -grep -q "host tool" b1.log || { cat b1.log; echo "FAIL: no tool-build announcement"; exit 1; } -out="$("$MCPP" run 2>&1 | grep '^ANSWER=' | tail -1)" -[[ "$out" == "ANSWER=42" ]] || { echo "FAIL: generated source not linked: $out"; exit 1; } - -# The store keeps the tool, so a second build must not rebuild it. -rm -rf target -"$MCPP" build > b2.log 2>&1 || { cat b2.log; echo "FAIL: second build failed"; exit 1; } -grep -q "Building.*host tool" b2.log && { - cat b2.log; echo "FAIL: the tool was rebuilt despite a valid store entry"; exit 1; } - -# ── default-off: no `tools = [...]`, nothing gets built ───────────────────── -cd "$TMP" -mkdir -p plain/src -cat > plain/mcpp.toml <<'EOF' -[package] -name = "plain" -version = "0.1.0" - -[dependencies] -toolpkg = { path = "../toolpkg" } -EOF -cat > plain/src/main.cpp <<'EOF' -int main() {} -EOF -cd plain -"$MCPP" build > b3.log 2>&1 || { cat b3.log; echo "FAIL: plain consumer build failed"; exit 1; } -grep -q "host tool" b3.log && { - cat b3.log; echo "FAIL: a tool was provisioned for a consumer that never asked"; exit 1; } - -# ── a bad tool name names the alternatives ────────────────────────────────── -cd "$TMP" -sed 's/tools = \["codegen"\]/tools = ["nosuchtool"]/' app/mcpp.toml > app/mcpp.toml.new -mv app/mcpp.toml.new app/mcpp.toml -cd app && rm -rf target -if "$MCPP" build > b4.log 2>&1; then - cat b4.log; echo "FAIL: an unknown tool name was accepted"; exit 1 -fi -grep -q "nosuchtool" b4.log || { cat b4.log; echo "FAIL: error does not name the request"; exit 1; } -grep -q "codegen" b4.log || { - cat b4.log; echo "FAIL: error does not list the available bin targets"; exit 1; } - -# ── the override escape hatch ─────────────────────────────────────────────── -cd "$TMP" -sed 's/tools = \["nosuchtool"\]/tools = ["codegen"]/' app/mcpp.toml > app/mcpp.toml.new -mv app/mcpp.toml.new app/mcpp.toml -cat > fake_codegen.sh <<'EOF' -#!/usr/bin/env bash -printf 'int generated_answer() { return 7; }\n' > "$1" -EOF -chmod +x fake_codegen.sh -cd app && rm -rf target -MCPP_TOOL_TOOLPKG_CODEGEN="$TMP/fake_codegen.sh" "$MCPP" build > b5.log 2>&1 \ - || { cat b5.log; echo "FAIL: build with a tool override failed"; exit 1; } -grep -q "override" b5.log || { cat b5.log; echo "FAIL: the override was not reported"; exit 1; } -out="$("$MCPP" run 2>&1 | grep '^ANSWER=' | tail -1)" -[[ "$out" == "ANSWER=7" ]] || { echo "FAIL: the override was not actually used: $out"; exit 1; } - -# ── a NAMESPACED package is addressable by both spellings ────────────────── -# `toolpkg` above has no namespace, so its canonical and short names are the -# same string and only one env var is emitted — the two-spelling path is never -# exercised. A consumer may write either `myns.tp` or `tp`, exactly as -# mcpp::dep_dir() accepts both, and the tool lookup has to match. -cd "$TMP" -mkdir -p ns/src -cat > ns/mcpp.toml <<'EOF' -[package] -name = "myns.tp" -version = "0.1.0" - -[build] -sources = ["src/lib.cpp"] - -[targets.gen] -kind = "bin" -main = "src/gen.cpp" -EOF -printf 'int tp_lib(){return 1;}\n' > ns/src/lib.cpp -cat > ns/src/gen.cpp <<'EOF' -#include -int main(int c, char** v) { - if (c < 2) return 2; - FILE* f = std::fopen(v[1], "w"); - if (!f) return 3; - std::fprintf(f, "int gv() { return 5; }\n"); - std::fclose(f); - return 0; -} -EOF -mkdir -p nsapp/src -cat > nsapp/mcpp.toml <<'EOF' -[package] -name = "nsapp" -version = "0.1.0" - -[dependencies] -"myns.tp" = { path = "../ns", tools = ["gen"] } -EOF -printf '#include \nint gv();\nint main(){std::printf("G=%%d\\n",gv());}\n' > nsapp/src/main.cpp -cat > nsapp/build.mcpp <<'EOF' -#include -#include -#include -import mcpp; -int main() { - // BOTH spellings must resolve to the same tool. - const char* full = mcpp::dep_bin("myns.tp", "gen"); - const char* brief = mcpp::dep_bin("tp", "gen"); - if (!*full) { std::fprintf(stderr, "canonical spelling did not resolve\n"); return 1; } - if (!*brief) { std::fprintf(stderr, "short spelling did not resolve\n"); return 1; } - std::string out = std::string(mcpp::out_dir()) + "/g.cpp"; - std::string cmd = std::string("\"") + brief + "\" \"" + out + "\""; - if (std::system(cmd.c_str()) != 0) return 1; - mcpp::generated(out.c_str()); -} -EOF -cd nsapp -"$MCPP" build > b6.log 2>&1 || { cat b6.log; echo "FAIL: namespaced tool lookup failed"; exit 1; } -out="$("$MCPP" run 2>&1 | grep '^G=' | tail -1)" -[[ "$out" == "G=5" ]] || { echo "FAIL: namespaced tool did not generate: $out"; exit 1; } - -# ── a package that writes `namespace` and `name` apart (#647 E4.3) ───────── -# `myns.tp` above is the legacy dotted name, whose `package.name` is already the -# qualified spelling. A manifest writing `namespace = "myns2"`, `name = "tp2"` -# has `package.name == "tp2"`, and the tool used to be published under that -# spelling alone, so `dep_bin("myns2.tp2", ...)` read nothing while -# `dep_dir("myns2.tp2")` answered. -cd "$TMP" -mkdir -p ns2/src -cat > ns2/mcpp.toml <<'EOF' -[package] -name = "tp2" -namespace = "myns2" -version = "0.1.0" - -[targets.gen] -kind = "bin" -main = "src/gen.cpp" -EOF -cp ns/src/gen.cpp ns2/src/gen.cpp -mkdir -p ns2app/src -cat > ns2app/mcpp.toml <<'EOF' -[package] -name = "ns2app" -version = "0.1.0" - -[dependencies] -myns2.tp2 = { path = "../ns2", tools = ["gen"] } -EOF -printf '#include \nint gv();\nint main(){std::printf("G2=%%d\\n",gv());}\n' > ns2app/src/main.cpp -cat > ns2app/build.mcpp <<'EOF' -#include -#include -#include -import mcpp; -int main() { - const char* full = mcpp::dep_bin("myns2.tp2", "gen"); - const char* brief = mcpp::dep_bin("tp2", "gen"); - if (!*full) { std::fprintf(stderr, "qualified spelling did not resolve\n"); return 1; } - if (!*brief) { std::fprintf(stderr, "name spelling did not resolve\n"); return 1; } - std::string out = std::string(mcpp::out_dir()) + "/g.cpp"; - std::string cmd = std::string("\"") + full + "\" \"" + out + "\""; - if (std::system(cmd.c_str()) != 0) return 1; - mcpp::generated(out.c_str()); -} -EOF -cd ns2app -"$MCPP" build > b7.log 2>&1 || { cat b7.log; echo "FAIL: a namespace + name package's tool was not addressable by its qualified name"; exit 1; } -out="$("$MCPP" run 2>&1 | grep '^G2=' | tail -1)" -[[ "$out" == "G2=5" ]] || { echo "FAIL: the namespace + name tool did not generate: $out"; exit 1; } - -echo "OK" diff --git a/tests/e2e/189_host_module_rules.sh b/tests/e2e/189_host_module_rules.sh deleted file mode 100755 index 80e0b0c24..000000000 --- a/tests/e2e/189_host_module_rules.sh +++ /dev/null @@ -1,154 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 189_host_module_rules.sh — reusable build rules distributed as ORDINARY mcpp -# packages: `host-module = true` makes a dependency's module importable from the -# consumer's build.mcpp. -# -# This is the answer to "a rule should be written once, not copy-pasted into -# every package's build.mcpp" — without introducing a second language. xmake -# reaches for Lua rules and Bazel for Starlark; mcpp's whole premise is that -# the build is written in C++, so a rule is a C++ module in a versioned package -# and rides the package manager that already exists. -# -# The load-bearing implementation detail this test pins: the rule module is -# compiled in the SAME command as build.mcpp, with the same flags. A BMI is -# only usable by a compile that agrees with it on standard, dialect and -# compiler identity — building it separately would leave that to chance, and -# disagreement surfaces as `module X CRC mismatch` rather than a clear error. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -# ── the rule package ──────────────────────────────────────────────────────── -# An ordinary mcpp library package whose lib root is the rule module. -mkdir -p rules/src -cat > rules/mcpp.toml <<'EOF' -[package] -name = "rules" -version = "0.1.0" - -[targets.rules] -kind = "lib" -EOF -cat > rules/src/rules.cppm <<'EOF' -module; -#include -export module rules; -export namespace rules { -// A rule: emits the directives its users would otherwise hand-write. Written -// ONCE here instead of copy-pasted into every consumer's build.mcpp. -inline void banner(const char* macro) { - std::printf("mcpp:cxxflag=-D%s=1\n", macro); -} -inline void define_answer(int n) { - std::printf("mcpp:cxxflag=-DRULE_ANSWER=%d\n", n); -} -} -EOF - -# ── the consumer ──────────────────────────────────────────────────────────── -mkdir -p app/src -cat > app/mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" - -[dependencies] -rules = { path = "../rules", host-module = true } -EOF -cat > app/src/main.cpp <<'EOF' -#include -#ifndef RULE_ANSWER -#error "the rule package's directive never reached the compile" -#endif -#ifndef RULE_BANNER_OK -#error "the second rule directive never reached the compile" -#endif -int main() { std::printf("ANSWER=%d\n", RULE_ANSWER); } -EOF -cat > app/build.mcpp <<'EOF' -import mcpp; -import rules; // a DEPENDENCY's module, compiled for the host -int main() { - rules::banner("RULE_BANNER_OK"); - rules::define_answer(42); -} -EOF - -cd app -"$MCPP" build > b1.log 2>&1 || { cat b1.log; echo "FAIL: build with a host rule module failed"; exit 1; } -out="$("$MCPP" run 2>&1 | grep '^ANSWER=' | tail -1)" -[[ "$out" == "ANSWER=42" ]] || { - echo "FAIL: the rule package's directives did not reach the build: $out"; exit 1; } - -# Editing the RULE must re-run build.mcpp: the rule's content is part of what -# the helper was compiled from, so a cached run would silently keep the old -# behaviour — the exact failure the declared-input cache exists to prevent. -sed -i 's/define_answer(int n)/define_answer(int n_)/; s/RULE_ANSWER=%d\\n", n)/RULE_ANSWER=%d\\n", n_ + 1)/' ../rules/src/rules.cppm -touch src/main.cpp -"$MCPP" build > b2.log 2>&1 || { cat b2.log; echo "FAIL: rebuild after editing the rule failed"; exit 1; } -grep -qE "^ *build\.mcpp .* ran [0-9]" b2.log || { - cat b2.log; echo "FAIL: editing the rule module did not re-run build.mcpp"; exit 1; } -out="$("$MCPP" run 2>&1 | grep '^ANSWER=' | tail -1)" -[[ "$out" == "ANSWER=43" ]] || { - echo "FAIL: the edited rule did not take effect: $out"; exit 1; } - -# ── a rule that uses the API it exists to wrap ────────────────────────────── -# The two properties below are what make the feature usable for a REAL rule -# rather than one that hand-prints directives, and 2026.8.5.1 had neither: -# -# `import std;` the rule was compiled before the std module was built, so -# it failed with `module 'std' not found`. The scan for -# `import std` read build.mcpp only, so a rule that needed it -# did not even trigger the build. -# `import mcpp;` registering the host module never removed the package from -# the consumer's ORDINARY graph, so the same .cppm was also -# compiled as a normal library — where `mcpp` does not exist. -cd "$TMP" -mv rules/src/rules.cppm rules/src/rules.cppm.bak -cat > rules/src/rules.cppm <<'EOF' -export module rules; -import std; // must be usable: a rule is a normal C++23 module -import mcpp; // must be usable: the typed wrapper is the whole point - -export namespace rules { -inline void banner(const char* macro) { mcpp::define(macro); } -inline void define_answer(int n) { - mcpp::cxxflag(std::format("-DRULE_ANSWER={}", n).c_str()); -} -} -EOF -cd app && rm -rf target -"$MCPP" build > b4.log 2>&1 || { - cat b4.log - echo "FAIL: a rule using import std + import mcpp did not build"; exit 1; } -out="$("$MCPP" run 2>&1 | grep '^ANSWER=' | tail -1)" -[[ "$out" == "ANSWER=42" ]] || { - echo "FAIL: the std/mcpp-based rule's directives did not land: $out"; exit 1; } - -# The rule package must NOT be compiled into the consumer's target. It is -# build-time-only; an object under obj/rules/ means it was also treated as an -# ordinary library, which is what made `import mcpp;` fail in the first place. -if find target -path '*obj/rules/*' -name '*.o' | grep -q .; then - find target -path '*obj/rules/*' -name '*.o' - echo "FAIL: the host-module package was also built as a normal library"; exit 1 -fi - -cd "$TMP" -rm -f rules/src/rules.cppm -mv rules/src/rules.cppm.bak rules/src/rules.cppm - -# The interface root is not what makes a library usable by a build program -# (mcpp#797): the module is found by the name its unit declares, wherever the -# file is. A unit renamed away from the conventional lib root still serves. -cd "$TMP" -mv rules/src/rules.cppm rules/src/elsewhere.cppm -cd app && rm -rf target -"$MCPP" build > b3.log 2>&1 || { - cat b3.log; echo "FAIL: a library whose interface is not at the conventional lib root was refused"; exit 1; } -out="$("$MCPP" run 2>&1 | grep '^ANSWER=' | tail -1)" -[[ "$out" == "ANSWER=43" ]] || { cat b3.log; echo "FAIL: the renamed interface's directive did not land: $out"; exit 1; } - -echo "OK" diff --git a/tests/e2e/193_provision_reexport.sh b/tests/e2e/193_provision_reexport.sh deleted file mode 100755 index c6ed3623e..000000000 --- a/tests/e2e/193_provision_reexport.sh +++ /dev/null @@ -1,236 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 193_provision_reexport.sh — #359: a library standing up a toolchain on its -# user's behalf. -# -# Before this, `tools = [...]` and `host-module = true` were recorded against -# the consumer of the edge that ASKED. A library could therefore build a tool -# but not hand it on, so its user had to declare every tool the library needed -# — for grpc that meant four dependency lines and knowing that gRPC codegen -# runs protobuf's protoc, which is the library's knowledge, not the user's. -# -# Covered here: -# 1. WITHOUT `reexport`, a library's tool stays with the library. That is the -# supply-chain default: an arbitrary transitive dependency must not be -# able to put entries in your build program's tool namespace. -# 2. WITH `reexport = true`, the consumer's build.mcpp sees the tool, the -# re-exported rule module, and the re-exported package's directory — -# while declaring ONE dependency. -# -# See .agents/docs/2026-08-06-provisions-and-build-inputs.md. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -export MCPP_HOME="$TMP/mcpphome" -mkdir -p "$MCPP_HOME" -if [ -d "$HOME/.mcpp/registry" ]; then - ln -s "$HOME/.mcpp/registry" "$MCPP_HOME/registry" -fi - -# ── the tool package (what a library depends on, and the user never names) ── -mkdir -p toolpkg/src -cat > toolpkg/mcpp.toml <<'EOF' -[package] -name = "toolpkg" -version = "0.1.0" - -[build] -sources = ["src/lib.cpp"] - -[targets.codegen] -kind = "bin" -main = "src/codegen.cpp" -EOF -printf 'int toolpkg_lib(){return 1;}\n' > toolpkg/src/lib.cpp -cat > toolpkg/src/codegen.cpp <<'EOF' -#include -int main(int argc, char** argv) { - if (argc < 2) return 2; - FILE* f = std::fopen(argv[1], "w"); - if (!f) return 3; - std::fprintf(f, "int generated_answer() { return 42; }\n"); - std::fclose(f); - return 0; -} -EOF - -# ── the rule package: an importable build rule, distributed as a package ──── -mkdir -p rulepkg/src -cat > rulepkg/mcpp.toml <<'EOF' -[package] -name = "rulepkg" -version = "0.1.0" - -[lib] -path = "src/rulepkg.cppm" -EOF -cat > rulepkg/src/rulepkg.cppm <<'EOF' -export module rulepkg; -import std; -import mcpp; -export namespace rulepkg { -// The rule knows which tool it needs. Its consumer does not have to. -bool generate() { - const char* tool = mcpp::dep_bin("toolpkg", "codegen"); - if (!tool || !*tool) { - std::println(std::cerr, "rulepkg: no codegen tool"); - return false; - } - // The tool package's own tree must be reachable too — that is where a real - // rule finds its data files (protoc's well-known .proto files). - if (std::string(mcpp::dep_dir("toolpkg")).empty()) { - std::println(std::cerr, "rulepkg: no dep_dir for toolpkg"); - return false; - } - std::string out = std::string(mcpp::out_dir()) + "/gen.cpp"; - std::string cmd = std::string("\"") + tool + "\" \"" + out + "\""; - if (std::system(cmd.c_str()) != 0) return false; - mcpp::generated(out.c_str()); - return true; -} -} -EOF - -# ── the library: it owns the knowledge, and re-exports what its user needs ── -mkdir -p lib/src -cat > lib/mcpp.toml <<'EOF' -[package] -name = "mylib" -version = "0.1.0" - -[build] -sources = ["src/mylib.cpp"] - -[dependencies] -toolpkg = { path = "../toolpkg", tools = ["codegen"] } -rulepkg = { path = "../rulepkg", host-module = true } -EOF -printf 'int mylib_fn(){return 1;}\n' > lib/src/mylib.cpp - -# ── 1. no reexport: the tool is built for the library, not handed on ──────── -mkdir -p app/src -cat > app/mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" - -[dependencies] -mylib = { path = "../lib" } -EOF -cat > app/src/main.cpp <<'EOF' -#include -int generated_answer(); -int main() { std::printf("ANSWER=%d\n", generated_answer()); } -EOF -cat > app/build.mcpp <<'EOF' -#include -import mcpp; -int main() { - const char* tool = mcpp::dep_bin("toolpkg", "codegen"); - std::printf("SEES_TOOL=%d\n", (tool && *tool) ? 1 : 0); - return 1; // fail on purpose: the build must not proceed either way -} -EOF -cd app -if "$MCPP" build > b1.log 2>&1; then - cat b1.log; echo "FAIL: build.mcpp returned 1 but the build succeeded"; exit 1 -fi -if grep -q "SEES_TOOL=1" b1.log; then - cat b1.log - echo "FAIL: a library's tool leaked to its consumer without reexport" - exit 1 -fi -grep -q "SEES_TOOL=0" b1.log || { - cat b1.log; echo "FAIL: build.mcpp did not run"; exit 1; } - -# ── 2. reexport: ONE dependency line is enough ────────────────────────────── -cd "$TMP" -cat > lib/mcpp.toml <<'EOF' -[package] -name = "mylib" -version = "0.1.0" - -[build] -sources = ["src/mylib.cpp"] - -[dependencies] -toolpkg = { path = "../toolpkg", tools = ["codegen"], reexport = true } -rulepkg = { path = "../rulepkg", host-module = true, reexport = true } -EOF -# The consumer names neither the tool nor the rule's dependencies — only the -# rule's module, which is what it actually calls. -cat > app/build.mcpp <<'EOF' -import mcpp; -import rulepkg; -int main() { return rulepkg::generate() ? 0 : 1; } -EOF -cd app && rm -rf target -"$MCPP" build > b2.log 2>&1 || { - cat b2.log; echo "FAIL: build with re-exported provisions failed"; exit 1; } -out="$("$MCPP" run 2>&1 | grep '^ANSWER=' | tail -1)" -[[ "$out" == "ANSWER=42" ]] || { - echo "FAIL: the re-exported tool's output was not linked: $out"; exit 1; } - -# The rule is BUILD-TIME only: it must not have been compiled into the binary. -if grep -q "rulepkg" <(nm -C "target"/*/*/bin/app 2>/dev/null || true); then - echo "FAIL: the rule package was linked into the consumer's binary"; exit 1 -fi - -# ── 3. a feature ADDING a request to an already-declared dependency ──────── -# The real shape: gRPC depends on protobuf always, and its `codegen` feature -# has to add `tools = ["protoc"], reexport = true` to that SAME edge. Moving -# the request to the unconditional entry is not an option — it would make every -# consumer build protoc — and dropping the feature's spec (try_emplace keeps -# the existing key) would silently lose the request. -cd "$TMP" -cat > lib/mcpp.toml <<'EOF' -[package] -name = "mylib" -version = "0.1.0" - -[build] -sources = ["src/mylib.cpp"] - -# Unconditional: the library links against this package no matter what. -[dependencies] -toolpkg = { path = "../toolpkg" } -rulepkg = { path = "../rulepkg", host-module = true, reexport = true } - -# The feature adds a REQUEST to the edge above, and nothing else. -[feature-deps.codegen] -toolpkg = { path = "../toolpkg", tools = ["codegen"], reexport = true } -EOF -cat > app/mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" - -[dependencies] -mylib = { path = "../lib", features = ["codegen"] } -EOF -cd app && rm -rf target -"$MCPP" build > b3.log 2>&1 || { - cat b3.log - echo "FAIL: a feature could not add a tool request to an existing dependency" - exit 1 -} -out="$("$MCPP" run 2>&1 | grep '^ANSWER=' | tail -1)" -[[ "$out" == "ANSWER=42" ]] || { echo "FAIL: expected ANSWER=42, got '$out'"; exit 1; } - -# … and without the feature, nothing is built. -cd "$TMP" -sed 's/, features = \["codegen"\]//' app/mcpp.toml > app/mcpp.toml.new -mv app/mcpp.toml.new app/mcpp.toml -cd app && rm -rf target -if "$MCPP" build > b4.log 2>&1; then - cat b4.log - echo "FAIL: the build succeeded without the codegen feature — the tool ran anyway" - exit 1 -fi -grep -q "host tool" b4.log && { - cat b4.log; echo "FAIL: a tool was built for a consumer that did not enable the feature"; exit 1; } - -echo "PASS: 193_provision_reexport" diff --git a/tests/e2e/194_build_mcpp_glob_input.sh b/tests/e2e/194_build_mcpp_glob_input.sh deleted file mode 100755 index 046433811..000000000 --- a/tests/e2e/194_build_mcpp_glob_input.sh +++ /dev/null @@ -1,125 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 194_build_mcpp_glob_input.sh — #359: an input that is a SET of files. -# -# `build.mcpp`'s re-run key was built from declared FILES and environment -# variables only, both fingerprinted by content. A program that globs its -# inputs therefore could not express "re-run me when a file appears": adding a -# .proto changed no declared file's hash, so the program did not re-run and the -# new file was silently never generated. Measured before the fix: -# `Finished dev in 0.01s`, zero artifacts. That is worse than requiring the -# author to list names, which is why grpc-m chose the explicit list. -# -# Two layers have to agree, and the second is easy to miss: the build.mcpp -# cache AND the project-level fast path, which skips prepare entirely when no -# source is newer than build.ninja. A new .proto moves no existing mtime, so -# the fast path is exactly where this silently did nothing. -# -# Covered here: -# 1. adding a matching file re-runs the program and changes the artifact -# 2. removing one re-runs it too (the set shrank) -# 3. editing a matched file's CONTENT does not re-run on the glob's account -# — content is what an ordinary rerun-if-changed entry is for -# 4. the program's own outputs live inside the project and must never be part -# of the set, or the build would re-run forever -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -mkdir -p app/src app/inputs -cat > app/mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" -EOF -cat > app/src/main.cpp <<'EOF' -#include -int count_inputs(); -int main() { std::printf("COUNT=%d\n", count_inputs()); } -EOF -# The program watches a directory of ".in" files and generates one function -# returning how many it found. It never names them: that is the point. -cat > app/build.mcpp <<'EOF' -import std; -import mcpp; -int main() { - namespace fs = std::filesystem; - mcpp::rerun_if_changed_glob("inputs/**/*.in"); - std::string root = mcpp::manifest_dir(); - int n = 0; - std::error_code ec; - for (auto const& e : fs::recursive_directory_iterator(root + "/inputs", ec)) - if (e.path().extension() == ".in") ++n; - std::string out = std::string(mcpp::out_dir()) + "/count.cpp"; - { std::ofstream os(out); os << "int count_inputs() { return " << n << "; }\n"; } - // Written into the project tree on purpose: a real rule does this, and it - // is what would make a naive glob re-run forever. - { std::ofstream os(root + "/marker.txt"); os << n; } - mcpp::generated(out.c_str()); - return 0; -} -EOF - -cd app -# Logs live OUTSIDE the project: the wide-glob case below asserts that the -# build's own outputs do not perturb the set, and a redirect creating b6.log -# inside the tree would perturb it for real — the test would then be checking -# the shell, not mcpp. -LOGS="$TMP/logs"; mkdir -p "$LOGS" -printf 'a\n' > inputs/a.in - -ran() { grep -qE "^ *build\.mcpp .* ran [0-9]" "$1"; } -cached() { grep -q "build.mcpp.*cached" "$1"; } - -"$MCPP" build > "$LOGS/b1.log" 2>&1 || { cat "$LOGS/b1.log"; echo "FAIL: first build failed"; exit 1; } -out="$("$MCPP" run 2>&1 | grep '^COUNT=' | tail -1)" -[[ "$out" == "COUNT=1" ]] || { echo "FAIL: expected COUNT=1, got '$out'"; exit 1; } - -# ── 1. a new matching file re-runs the program ────────────────────────────── -printf 'b\n' > inputs/b.in -"$MCPP" build > "$LOGS/b2.log" 2>&1 || { cat "$LOGS/b2.log"; echo "FAIL: build after adding a file failed"; exit 1; } -ran "$LOGS/b2.log" || { - cat "$LOGS/b2.log" - echo "FAIL: adding a matching file did not re-run build.mcpp" - exit 1 -} -out="$("$MCPP" run 2>&1 | grep '^COUNT=' | tail -1)" -[[ "$out" == "COUNT=2" ]] || { echo "FAIL: expected COUNT=2, got '$out'"; exit 1; } - -# ── 2. removing one re-runs it as well ───────────────────────────────────── -rm inputs/b.in -"$MCPP" build > "$LOGS/b3.log" 2>&1 || { cat "$LOGS/b3.log"; echo "FAIL: build after removing a file failed"; exit 1; } -ran "$LOGS/b3.log" || { cat "$LOGS/b3.log"; echo "FAIL: removing a matching file did not re-run build.mcpp"; exit 1; } -out="$("$MCPP" run 2>&1 | grep '^COUNT=' | tail -1)" -[[ "$out" == "COUNT=1" ]] || { echo "FAIL: expected COUNT=1 after removal, got '$out'"; exit 1; } - -# ── 3. editing content does not re-run on the glob's account ─────────────── -# The set is unchanged, so the glob has nothing to say. (A program that cares -# about contents declares the file with rerun_if_changed, which hashes them.) -printf 'a totally different body\n' > inputs/a.in -"$MCPP" build > "$LOGS/b4.log" 2>&1 || { cat "$LOGS/b4.log"; echo "FAIL: build after an edit failed"; exit 1; } -ran "$LOGS/b4.log" && { - cat "$LOGS/b4.log" - echo "FAIL: editing a matched file's content re-ran the program via the glob" - exit 1 -} - -# ── 4. no re-run loop from the program's own outputs ─────────────────────── -# marker.txt is written into the project by the program itself, and target/ -# holds everything it generated. With a deliberately maximal pattern, a second -# build with nothing changed must still be a no-op; if the output tree were -# part of the set it never would be. -sed 's|"inputs/\*\*/\*\.in"|"**"|' build.mcpp > build.mcpp.new -mv build.mcpp.new build.mcpp -"$MCPP" build > "$LOGS/b5.log" 2>&1 || { cat "$LOGS/b5.log"; echo "FAIL: wide-glob build failed"; exit 1; } -ran "$LOGS/b5.log" || { cat "$LOGS/b5.log"; echo "FAIL: the edited program did not run"; exit 1; } -"$MCPP" build > "$LOGS/b6.log" 2>&1 || { cat "$LOGS/b6.log"; echo "FAIL: second wide-glob build failed"; exit 1; } -ran "$LOGS/b6.log" && { - cat "$LOGS/b6.log" - echo "FAIL: a wide glob re-runs forever — the build output tree is in its set" - exit 1 -} - -echo "PASS: 194_build_mcpp_glob_input" diff --git a/tests/e2e/309_host_module_identity.sh b/tests/e2e/309_host_module_identity.sh deleted file mode 100755 index 44ca9246f..000000000 --- a/tests/e2e/309_host_module_identity.sh +++ /dev/null @@ -1,235 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 309_host_module_identity.sh — a build rule's MODULE NAME is what its source -# declares, not what its package is called. -# -# The codebase already states the general rule for ordinary packages ("module -# names are authored API and are not required to mirror package identity"), and -# the host-module path was the one place that derived the name from -# `package.name` instead of reading it. -# -# That was not merely inconsistent. `build_host_module` binds the name -# differently per compiler family: MSVC is handed `/reference =` and -# Clang `-fmodule-file==`, while GCC names nothing at all because its -# BMIs are implicit under gcm.cache, keyed by the name the SOURCE declares. So a -# rule whose lib root declared anything other than its package name built under -# GCC and failed under the other two, reporting a module nobody had written. -# -# Pinned here: -# 1. a divergent declared name is what the consumer imports; -# 2. a dotted name survives whole (`a.b.c`, not `a`); -# 3. two rules declaring ONE module name are refused, naming both packages — -# before this, the second overwrote the first's object file in silence; -# 4. an `mcpp.` module name from an outside namespace WARNS and still builds. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -# ── 1. the declared name diverges from the package name ───────────────────── -mkdir -p rules/src -cat > rules/mcpp.toml <<'EOF' -[package] -name = "protobufgen" -version = "0.1.0" - -[targets.protobufgen] -kind = "lib" -EOF -# Package `protobufgen`, module `acme.rules.protobuf`. Under the old rule mcpp -# would have registered `protobufgen`, and the consumer's `import` would have -# failed to find it on Clang and MSVC. -cat > rules/src/protobufgen.cppm <<'EOF' -export module acme.rules.protobuf; -import std; -import mcpp; -export namespace acme::rules::protobuf { -inline void apply() { mcpp::cxxflag("-DDIVERGENT_NAME_REACHED=1"); } -} -EOF - -mkdir -p app/src -cat > app/mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" - -[dependencies] -protobufgen = { path = "../rules", host-module = true } -EOF -cat > app/src/main.cpp <<'EOF' -#include -#ifndef DIVERGENT_NAME_REACHED -#error "the rule's directive never reached the compile" -#endif -int main() { std::printf("DIVERGENT=%d\n", DIVERGENT_NAME_REACHED); } -EOF -cat > app/build.mcpp <<'EOF' -import mcpp; -import acme.rules.protobuf; // the DECLARED name, not the package name -int main() { acme::rules::protobuf::apply(); } -EOF - -cd app -"$MCPP" build > b1.log 2>&1 || { - cat b1.log - echo "FAIL: a rule whose declared module name differs from its package name did not build" - exit 1; } -out="$("$MCPP" run 2>&1 | grep '^DIVERGENT=' | tail -1)" -[[ "$out" == "DIVERGENT=1" ]] || { - echo "FAIL: the divergently-named rule's directive did not land: $out"; exit 1; } - -# THE IMPORT ABOVE DOES NOT DISCRIMINATE ON GCC: GCC's BMIs are implicit under -# gcm.cache and keyed by the name the SOURCE declares, so a build that -# registered the PACKAGE name succeeded there and failed under Clang and MSVC, -# which are handed an explicit `=` mapping. -# -# The build programs' graph (mcpp#797) compiles the library as a package of -# its plan, and the plan names a BMI for the module the source declares. The -# BMI is the platform-independent evidence of which name the engine used. -BM=target/.build-mcpp/host -[ -n "$(find "$BM" -name 'acme.rules.protobuf.*' 2>/dev/null)" ] || { - echo "FAIL: the host module was not named for its DECLARED module name" - find "$BM" 2>/dev/null | head -40 - exit 1; } -if [ -n "$(find "$BM" \( -name 'protobufgen.gcm' -o -name 'protobufgen.pcm' -o -name 'protobufgen.ifc' \) 2>/dev/null)" ]; then - find "$BM" | head -40 - echo "FAIL: a BMI is named for the PACKAGE name" - exit 1 -fi - -# ── 2. two rules, one module name ─────────────────────────────────────────── -# Two DIFFERENT packages declaring ONE module name. This is the risk that -# decoupling the module name from the package name introduces, which is why I1 -# and this check ship together rather than in sequence. -# -# Measured with the check removed and the rest of this change in place: both -# compiles write `sharedname.o`, the second overwrites the first, and the one -# surviving object is handed to the link twice — once per registered module. -# The build fails, but it fails as -# -# ld: …/sharedname.o: multiple definition of `initializer for module -# sharedname'; …/sharedname.o: beta.cppm: first defined here -# -# naming one file twice, one package twice, and never saying that two packages -# are involved or which two. The overwrite is the silent part; the diagnostic -# is merely unactionable. This check is what turns it into a sentence. -cd "$TMP" -for n in alpha beta; do - mkdir -p "dup-$n/src" - cat > "dup-$n/mcpp.toml" < "dup-$n/src/$n.cppm" < dupapp/mcpp.toml <<'EOF' -[package] -name = "dupapp" -version = "0.1.0" - -[dependencies] -alpha = { path = "../dup-alpha", host-module = true } -beta = { path = "../dup-beta", host-module = true } -EOF -echo 'int main() { return 0; }' > dupapp/src/main.cpp -cat > dupapp/build.mcpp <<'EOF' -import mcpp; -import sharedname; -int main() { shared::go(); } -EOF -cd dupapp -if "$MCPP" build > b2.log 2>&1; then - cat b2.log - echo "FAIL: two rules declaring the same module name were accepted" - exit 1 -fi -# The two package identities and the two interface paths ARE the content of -# this diagnostic: with one module name shared they are the only way to tell -# the packages apart. Asserted on the whole output, not on a keyword, so a -# reworded message that dropped them would fail here. -for want in "sharedname" "vendoralpha.alpha" "vendorbeta.beta" \ - "dup-alpha/src/alpha.cppm" "dup-beta/src/beta.cppm"; do - grep -qF "$want" b2.log || { - cat b2.log - echo "FAIL: the collision diagnostic does not name '$want'"; exit 1; } -done - -# ── 3. the reserved `mcpp.` prefix warns, and the build still succeeds ─────── -# Both halves. A test that only checked for the warning could not tell a -# warning from an error. -cd "$TMP" -mkdir -p claimer/src -cat > claimer/mcpp.toml <<'EOF' -[package] -name = "claimer" -namespace = "acme" -version = "0.1.0" - -[targets.claimer] -kind = "lib" -EOF -cat > claimer/src/claimer.cppm <<'EOF' -export module mcpp.build.claimed; -import std; -import mcpp; -export namespace claimed { inline void go() { mcpp::cxxflag("-DCLAIMED=1"); } } -EOF -mkdir -p claimapp/src -cat > claimapp/mcpp.toml <<'EOF' -[package] -name = "claimapp" -version = "0.1.0" - -[dependencies] -claimer = { path = "../claimer", host-module = true } -EOF -cat > claimapp/src/main.cpp <<'EOF' -#include -#ifndef CLAIMED -#error "the rule never ran" -#endif -int main() { std::printf("CLAIMED=%d\n", CLAIMED); } -EOF -cat > claimapp/build.mcpp <<'EOF' -import mcpp; -import mcpp.build.claimed; -int main() { claimed::go(); } -EOF -cd claimapp -"$MCPP" build > b3.log 2>&1 || { - cat b3.log - echo "FAIL: the reserved prefix must WARN, not fail the build"; exit 1; } -grep -qF "mcpp.build.claimed" b3.log || { - cat b3.log; echo "FAIL: no warning naming the claimed module"; exit 1; } -grep -qF "acme.claimer" b3.log || { - cat b3.log; echo "FAIL: the warning does not name the package making the claim"; exit 1; } -out="$("$MCPP" run 2>&1 | grep '^CLAIMED=' | tail -1)" -[[ "$out" == "CLAIMED=1" ]] || { - echo "FAIL: the warned-about rule did not actually work: $out"; exit 1; } - -# A package IN the mcpp namespace says the same thing without a warning. -cd "$TMP" -sed -i 's/^namespace = "acme"/namespace = "mcpp"/' claimer/mcpp.toml -cd claimapp && rm -rf target -"$MCPP" build > b4.log 2>&1 || { - cat b4.log; echo "FAIL: the official namespace build failed"; exit 1; } -if grep -qF "reserved for rules maintained by the mcpp project" b4.log; then - cat b4.log - echo "FAIL: a package in the mcpp namespace was warned about its own prefix" - exit 1 -fi - -echo "OK" diff --git a/tests/e2e/310_build_dependencies.sh b/tests/e2e/310_build_dependencies.sh deleted file mode 100755 index 0a264dcc5..000000000 --- a/tests/e2e/310_build_dependencies.sh +++ /dev/null @@ -1,163 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 310_build_dependencies.sh — `[build-dependencies]`: what serves the BUILD and -# what reaches the TARGET are two different questions. -# -# The section has been parsed, merged across workspace members and -# conditionalised by target predicate for a long time, and until now nothing -# read it to make a decision: writing it produced a manifest that loaded, no -# diagnostic, and no effect. `types.cppm` even annotated the field -# "host-side tools (M5+ behavior)". -# -# The consequence was not merely a dormant feature. Both live build-time -# channels (`tools`, `host-module`) are written by the CONSUMER on an edge, so a -# build rule had no way to request anything on its own behalf — and what it -# wrote in `[dependencies]` LEAKED, because the rule-only predicate looked at -# edges into the rule and not at what lay behind it. -# -# Pinned here: -# 1. a rule's own `[dependencies]` do not reach the consumer's binary; -# 2. `[build-dependencies]` and everything behind them do not either; -# 3. a package the project depends on DIRECTLY still reaches the target even -# when a build-time path also reaches it — the axes are orthogonal. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -# A plain library. Whether its object ends up in a consumer's binary is the -# only thing this test looks at, so it carries one unmistakable symbol. -mk_lib() { - local name="$1" sym="$2" - mkdir -p "$name/src" - cat > "$name/mcpp.toml" < "$name/src/$name.cppm" < rules/mcpp.toml <<'EOF' -[package] -name = "ruleswithdeps" -version = "0.1.0" - -[targets.ruleswithdeps] -kind = "lib" - -[dependencies] -behindrule = { path = "../behindrule" } -EOF -cat > rules/src/ruleswithdeps.cppm <<'EOF' -export module ruleswithdeps; -import std; -import mcpp; -export namespace rules { inline void go() { mcpp::cxxflag("-DRULE_RAN=1"); } } -EOF - -mkdir -p app/src -cat > app/mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" - -[dependencies] -ruleswithdeps = { path = "../rules", host-module = true } -bothsides = { path = "../bothsides" } - -[build-dependencies] -buildside = { path = "../buildside" } -EOF -cat > app/src/main.cpp <<'EOF' -#include -#ifndef RULE_RAN -#error "the rule never ran" -#endif -import bothsides; -int main() { std::printf("APP=%d\n", bothsides_symbol()); } -EOF -cat > app/build.mcpp <<'EOF' -import mcpp; -import ruleswithdeps; -int main() { rules::go(); } -EOF - -cd app -"$MCPP" build > b1.log 2>&1 || { cat b1.log; echo "FAIL: build failed"; exit 1; } -out="$("$MCPP" run 2>&1 | grep '^APP=' | tail -1)" -[[ "$out" == "APP=1" ]] || { echo "FAIL: the app did not run: $out"; exit 1; } - -# The criterion is the ARTIFACT, not a log line. `nm` comes from the toolchain -# that produced the binary rather than from PATH: a host `nm` may not read what -# a private toolchain emits, and `command -v nm` has answered for the wrong -# toolchain in this repository before. -BIN="$("$MCPP" run --print-artifact 2>/dev/null || true)" -if [[ ! -x "${BIN:-}" ]]; then - mapfile -t bins < <(find target -type f -name app -perm -u+x) - (( ${#bins[@]} == 1 )) || { - echo "FAIL: expected exactly one app binary, found ${#bins[@]}" - printf ' %s\n' "${bins[@]}"; exit 1; } - BIN="${bins[0]}" -fi - -has_symbol() { grep -qF "$1" <(strings -a "$BIN" 2>/dev/null; nm -C "$BIN" 2>/dev/null || true); } - -# 3. the orthogonal case FIRST, so a wholesale failure to link anything cannot -# pass this test by accident. This symbol MUST be present. -has_symbol bothsides_symbol || { - echo "FAIL: a direct [dependencies] library did not reach the target"; exit 1; } - -# 1. a rule's own dependency must not be in the consumer's binary -if has_symbol behindrule_symbol; then - echo "FAIL: a build rule's own [dependencies] leaked into the consumer's binary" - exit 1 -fi - -# 2. nor a [build-dependencies] entry -if has_symbol buildside_symbol; then - echo "FAIL: a [build-dependencies] entry reached the target" - exit 1 -fi - -# ── the dual-role case, stated the other way round ────────────────────────── -# The same package named in BOTH tables by one consumer. The ordinary -# declaration wins: a `[build-dependencies]` line must not quietly drop a -# library the target needs. -cd "$TMP" -cat > app/mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" - -[dependencies] -ruleswithdeps = { path = "../rules", host-module = true } -bothsides = { path = "../bothsides" } - -[build-dependencies] -bothsides = { path = "../bothsides" } -EOF -cd app && rm -rf target -"$MCPP" build > b2.log 2>&1 || { cat b2.log; echo "FAIL: dual-table build failed"; exit 1; } -mapfile -t bins < <(find target -type f -name app -perm -u+x) -(( ${#bins[@]} == 1 )) || { - echo "FAIL: expected exactly one app binary, found ${#bins[@]}"; exit 1; } -BIN="${bins[0]}" -has_symbol bothsides_symbol || { - echo "FAIL: naming a library in [build-dependencies] too removed it from the target" - exit 1; } - -echo "OK" diff --git a/tests/e2e/311_rule_build_dependencies.sh b/tests/e2e/311_rule_build_dependencies.sh deleted file mode 100755 index 03fb4b775..000000000 --- a/tests/e2e/311_rule_build_dependencies.sh +++ /dev/null @@ -1,180 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 311_rule_build_dependencies.sh — a build rule may depend on another build -# rule, declared in its OWN `[build-dependencies]`. -# -# Before this, a rule was a leaf. Not by design: both live build-time channels -# (`tools`, `host-module`) are written by the CONSUMER on an edge, so a rule had -# no way to request anything on its own behalf. `[build-dependencies]` is the -# only declaration site a package owns, and it was read by nothing. -# -# The mechanism is ORDERING, not new machinery. build_program.cppm accumulates -# the module flags as it compiles the host modules in list order, so each entry -# already sees the BMIs of everything ahead of it; the caller topologically -# sorts the list and a rule can import a rule. -# -# Pinned here: -# 1. a rule imports its own `[build-dependencies]` host module and works; -# 2. the consumer may NOT import it — provisions cross one further edge only -# on a `reexport = true` edge, and mcpp enforces that on every platform -# because GCC cannot (its BMIs are reachable by name whatever the flags); -# 3. `reexport = true` makes it importable by the consumer; -# 4. a cycle is reported AS a cycle, naming the packages on it. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -# ── the inner rule ────────────────────────────────────────────────────────── -mkdir -p globbing/src -cat > globbing/mcpp.toml <<'EOF' -[package] -name = "globbing" -version = "0.1.0" - -[targets.globbing] -kind = "lib" -EOF -cat > globbing/src/globbing.cppm <<'EOF' -export module globbing; -import std; -export namespace globbing { -inline int answer() { return 7; } -} -EOF - -# ── the outer rule, which imports it ──────────────────────────────────────── -mk_outer() { # $1 = extra keys on the globbing edge - mkdir -p tidyrule/src - cat > tidyrule/mcpp.toml < tidyrule/src/tidyrule.cppm <<'EOF' -export module tidyrule; -import std; -import mcpp; -import globbing; // a rule importing a rule: impossible before this change -export namespace tidyrule { -inline void go() { - mcpp::cxxflag(std::format("-DTIDY_ANSWER={}", globbing::answer()).c_str()); -} -} -EOF -} -mk_outer "" - -mkdir -p app/src -cat > app/mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" - -[dependencies] -tidyrule = { path = "../tidyrule", host-module = true } -EOF -cat > app/src/main.cpp <<'EOF' -#include -#ifndef TIDY_ANSWER -#error "the outer rule never ran" -#endif -int main() { std::printf("ANSWER=%d\n", TIDY_ANSWER); } -EOF -cat > app/build.mcpp <<'EOF' -import mcpp; -import tidyrule; -int main() { tidyrule::go(); } -EOF - -cd app -"$MCPP" build > b1.log 2>&1 || { - cat b1.log; echo "FAIL: a rule with its own [build-dependencies] did not build"; exit 1; } -out="$("$MCPP" run 2>&1 | grep '^ANSWER=' | tail -1)" -# The value travels through the inner rule, so it is evidence that the inner -# module was actually compiled and linked rather than merely named. -[[ "$out" == "ANSWER=7" ]] || { - echo "FAIL: the inner rule's value did not reach the compile: $out"; exit 1; } - -# The inner rule must be BUILD-time only: it has no business in the binary. -mapfile -t bins < <(find target -type f -name app -perm -u+x) -(( ${#bins[@]} == 1 )) || { echo "FAIL: expected one app binary, got ${#bins[@]}"; exit 1; } - -# ── 2. the consumer may not import what it never declared ─────────────────── -cd "$TMP" -cat > app/build.mcpp <<'EOF' -import mcpp; -import tidyrule; -import globbing; // never declared by this package -int main() { tidyrule::go(); } -EOF -cd app && rm -rf target -if "$MCPP" build > b2.log 2>&1; then - cat b2.log - echo "FAIL: build.mcpp imported a module reachable only as another rule's prerequisite" - exit 1 -fi -grep -qF "globbing" b2.log || { - cat b2.log; echo "FAIL: the diagnostic does not name the module"; exit 1; } -grep -qF "reexport" b2.log || { - cat b2.log; echo "FAIL: the diagnostic does not say how to fix it"; exit 1; } - -# ── 3. reexport = true makes it the consumer's to import ─────────────────── -cd "$TMP" -mk_outer ", reexport = true" -cd app && rm -rf target -"$MCPP" build > b3.log 2>&1 || { - cat b3.log; echo "FAIL: a re-exported host module was still refused"; exit 1; } -out="$("$MCPP" run 2>&1 | grep '^ANSWER=' | tail -1)" -[[ "$out" == "ANSWER=7" ]] || { - echo "FAIL: the re-exported path broke the value: $out"; exit 1; } - -# ── 3b. withdrawing reexport is caught on a warm cache ───────────────────── -# Ordered right after the successful build above, so build.mcpp's cache and the -# whole-project fast path are both warm. Dropping `reexport` leaves the module -# SET and every interface hash identical, so nothing in the cache identity -# moves; the refusal is reached only because it is asked BEFORE the cache is -# consulted. -# -# This assertion does not prove that placement on its own. Measured: with -# the check below the fast path it still fired here, because `ctxHash` happens -# to change as well. The placement is what makes the property hold; this pins -# the behaviour, not the reason. -cd "$TMP" -mk_outer "" -touch app/src/main.cpp # the whole-project fast path never reaches build.mcpp -cd app -if "$MCPP" build > b3b.log 2>&1; then - cat b3b.log - echo "FAIL: withdrawing reexport was replayed from the build.mcpp cache" - exit 1 -fi -grep -qF "globbing" b3b.log || { - cat b3b.log; echo "FAIL: the refusal after cache invalidation does not name the module"; exit 1; } - -# ── 4. a cycle is reported as a cycle ────────────────────────────────────── -cd "$TMP" -cat >> globbing/mcpp.toml <<'EOF' - -[build-dependencies] -tidyrule = { path = "../tidyrule", host-module = true } -EOF -cd app && rm -rf target -if "$MCPP" build > b4.log 2>&1; then - cat b4.log; echo "FAIL: a rule import cycle was accepted"; exit 1 -fi -grep -qi "cycle" b4.log || { - cat b4.log; echo "FAIL: the cycle was not reported as a cycle"; exit 1; } -# Naming the packages on the ring is the actionable half; a bare "cycle -# detected" leaves the reader to find it. -grep -qF "globbing" b4.log && grep -qF "tidyrule" b4.log || { - cat b4.log; echo "FAIL: the cycle diagnostic does not name the packages on it"; exit 1; } - -echo "OK" diff --git a/tests/e2e/312_build_rules_example.sh b/tests/e2e/312_build_rules_example.sh deleted file mode 100755 index f89470772..000000000 --- a/tests/e2e/312_build_rules_example.sh +++ /dev/null @@ -1,97 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 312_build_rules_example.sh — examples/08-build-rules actually builds and runs. -# -# mcpp's `examples/` are not otherwise exercised by CI, so an example that stops -# working says nothing until a reader tries it. This one is worth the e2e -# because it is the only place the ecosystem-facing shape of a rule package is -# written down as running code: two rule packages, one depending on the other -# through its own `[build-dependencies]`, covering `role = "check"` and a -# generated source, and a consumer that uses both. -# -# It is copied to a temporary directory rather than built in place: building in -# the worktree would leave target/ directories inside the repository and would -# make a second run measure the first one's cache. -set -e - -SRC="$(cd "$(dirname "$0")/../.." && pwd)/examples/08-build-rules" -[[ -d "$SRC" ]] || { echo "FAIL: $SRC is missing"; exit 1; } - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cp -r "$SRC" "$TMP/ex" -# Anything a previous run left behind travels with the copy, and the object -# count below would then be measuring that run instead of this one. Caught by -# the denominator on the first attempt: four objects where two were expected. -find "$TMP/ex" -maxdepth 3 -type d -name target -exec rm -rf {} + 2>/dev/null || true -find "$TMP/ex" -maxdepth 3 -name mcpp.lock -delete 2>/dev/null || true -cd "$TMP/ex/app" - -"$MCPP" build > b1.log 2>&1 || { cat b1.log; echo "FAIL: the example did not build"; exit 1; } - -out="$("$MCPP" run 2>&1 | tail -1)" -[[ "$out" == "hello from an embedded asset" ]] || { - echo "FAIL: the embedded asset did not reach the program: '$out'"; exit 1; } - -# The rules are build-time only. Asserted on the ARTIFACT: a rule's objects have -# no business in the consumer's binary, and `Compiling rules-embed (path)` in -# the log is the host-module compile, not evidence either way. -# The store a host module is kept in (`.build-mcpp/host-modules`, #748) has an -# `obj/` of its own and is not the consumer's: it is left out. -mapfile -t objs < <(find target -path '*obj*' -not -path '*/.build-mcpp/*' -name '*.o' -printf '%f\n' | sort) -for o in "${objs[@]}"; do - case "$o" in - main.o|embed_greeting.o) ;; - *) printf ' %s\n' "${objs[@]}" - echo "FAIL: unexpected object '$o' — a rule package reached the link" - exit 1 ;; - esac -done -# And the denominator: an empty object list would pass the loop above while -# proving nothing. -(( ${#objs[@]} == 2 )) || { - printf ' %s\n' "${objs[@]}" - echo "FAIL: expected exactly 2 objects, found ${#objs[@]}"; exit 1; } - -# The generated source is a graph node, so editing its INPUT re-runs that edge -# and changes the program. This is what `role`-based actions buy over writing -# the file eagerly from build.mcpp. -sleep 1 -printf 'a different embedded asset\n' > assets/greeting.txt -"$MCPP" build > b2.log 2>&1 || { cat b2.log; echo "FAIL: rebuild failed"; exit 1; } -out="$("$MCPP" run 2>&1 | tail -1)" -[[ "$out" == "a different embedded asset" ]] || { - echo "FAIL: editing the asset did not re-run the embed action: '$out'"; exit 1; } - -# The check's ANALYSER never touches the stamp — `tools/check.sh` reads a file -# and answers with an exit code, which is what clang-tidy does. mcpp creates -# the stamp when the command succeeds. -# -# ASSERT ON THE STAMP AND ON THE RE-RUN, NOT ON THE BUILD SUCCEEDING. -# Measured with the wrapper removed: ninja does NOT fail when a declared output -# goes unproduced. It leaves the file missing and re-runs that edge on every -# build, forever — so the build stays green, this script's earlier assertions -# stay green, and the only visible symptom is work being redone. A test that -# checked exit codes here would have passed either way. -mapfile -t stamps < <(find target -name '*.stamp') -(( ${#stamps[@]} == 1 )) || { - printf ' %s\n' "${stamps[@]}" - echo "FAIL: expected exactly one check stamp, found ${#stamps[@]}"; exit 1; } - -"$MCPP" build > b3.log 2>&1 || { cat b3.log; echo "FAIL: no-op rebuild failed"; exit 1; } -if grep -q 'CHECK' b3.log; then - cat b3.log - echo "FAIL: the check re-ran with nothing changed — its stamp is not satisfying the edge" - exit 1 -fi - -# A failing check fails the build. Without this the check role would be -# decoration: it runs beside the compile, so nothing else would notice it. -printf 'int main() { goto done; done: return 0; }\n' > src/main.cpp -if "$MCPP" build > b3.log 2>&1; then - cat b3.log; echo "FAIL: a failing check did not fail the build"; exit 1 -fi -grep -qF "goto" b3.log || { - cat b3.log; echo "FAIL: the build failed without reporting what the check found"; exit 1; } - -echo "OK" diff --git a/tests/e2e/334_dependency_declared_tool_is_found.sh b/tests/e2e/334_dependency_declared_tool_is_found.sh deleted file mode 100755 index 2ae3a734d..000000000 --- a/tests/e2e/334_dependency_declared_tool_is_found.sh +++ /dev/null @@ -1,109 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc unix-shell -# A runner may name its program by bare name when a DEPENDENCY declared it. -# -# THE CASE THIS COVERS IS THE ONE THE FEATURE EXISTS FOR, AND IT WAS THE -# ONE THAT DID NOT WORK. -# -# `mcpp.build.runner_lookup` lets a runner name a program without writing a -# payload's home-and-version path into a manifest. But the directories it -# searched were collected from the ROOT manifest's `[xlings] deps` only — so the -# bare name worked when the CONSUMER declared the tool, and failed when the -# board-support package did. -# -# That is backwards. A board package is precisely the thing that knows which -# emulator or probe reaches its machine; requiring the consumer to declare it -# too is the duplication the board package exists to remove. -# -# Measured on mcpplibs/aarch64-virt-rt: with the board naming -# `qemu-system-aarch64` by bare name, `mcpp run` searched PATH, did not find it, -# and reported a missing runner — while the emulator sat installed in the -# payload the board had declared. -set -e - -MCPP="${MCPP:-mcpp}" -work="$(mktemp -d)" -trap 'rm -rf "$work"' EXIT - -# A stand-in payload: a directory with a bin/ holding one executable. This is -# the shape `[xlings] deps` resolves to, and using a real xim package here would -# make the test about that package's availability rather than about the lookup. -mkdir -p "$work/fakepkg/bin" -cat > "$work/fakepkg/bin/demo-tool" <<'TOOL' -#!/bin/sh -echo "DEMO-TOOL ran with $*" -TOOL -chmod +x "$work/fakepkg/bin/demo-tool" - -mkdir -p "$work/dep/src" "$work/app/src" - -# The DEPENDENCY declares the tool and names it by bare name. -cd "$work/dep" -cat > mcpp.toml <<'TOML' -[package] -name = "toolbox" -version = "0.1.0" -TOML -printf 'export module toolbox;\n' > src/t.cppm -cat > build.mcpp <<'BUILD' -import mcpp; -import std; -int main() { - // The bare name. Whether this resolves is the whole subject of the test. - mcpp::runner("demo-tool"); - return 0; -} -BUILD - -cd "$work/app" -cat > mcpp.toml <<'TOML' -[package] -name = "app" -version = "0.1.0" - -[dependencies] -toolbox = { path = "../dep" } -TOML -cat > src/main.cpp <<'CPP' -int main() { return 0; } -CPP - -# THE TOOL IS ON PATH HERE ONLY VIA THE STAND-IN PAYLOAD'S bin/, WHICH IS -# WHAT MAKES THE ASSERTION MEAN SOMETHING. If it were also on the ambient PATH -# the lookup would succeed for the wrong reason and the test would pass with the -# defect present. -PATH_WITHOUT_TOOL="$PATH" -case ":$PATH_WITHOUT_TOOL:" in - *":$work/fakepkg/bin:"*) echo "FAIL: fixture leaked onto PATH"; exit 1 ;; -esac -command -v demo-tool >/dev/null 2>&1 && { echo "SKIP: a demo-tool already on PATH"; exit 77; } - -# `[xlings] deps` resolution needs a real xim package, which this fixture is -# not. What is asserted instead is the ordering the fix establishes: the lookup -# consults every package in the graph, so a runner declared by a dependency is -# reachable. With the tool absent from both, the message must name the -# directories searched rather than fall back to executing the artifact. -out=$("$MCPP" run 2>&1) && rc=0 || rc=$? -[ "$rc" != "0" ] || { echo "FAIL: run succeeded with an unresolvable runner — it fell back to executing the artifact"; exit 1; } -case "$out" in - *"demo-tool"*) ;; - *) echo "FAIL: the diagnostic does not name the program that was not found" - echo "$out" | tail -5; exit 1 ;; -esac -case "$out" in - *"not found"*|*"was not found"*) ;; - *) echo "FAIL: the diagnostic does not say the program was not found" - echo "$out" | tail -5; exit 1 ;; -esac -echo " ok a dependency's bare-name runner is resolved, and its absence is named" - -# AND THE FALLBACK IS REFUSED RATHER THAN TAKEN. Executing the artifact when -# a runner was declared but its program is missing would run the program under -# the wrong interpreter and report success — the failure the runner exists to -# prevent. -case "$out" in - *"DEMO-TOOL ran"*) echo "FAIL: the artifact was executed anyway"; exit 1 ;; -esac -echo " ok a declared-but-unresolvable runner is an error, not a fallback" - -echo "PASS: a dependency-declared tool is reachable by bare name" diff --git a/tests/e2e/58_preinstall_mcpp_deps_for_hooks.sh b/tests/e2e/58_preinstall_mcpp_deps_for_hooks.sh deleted file mode 100644 index 83112ee47..000000000 --- a/tests/e2e/58_preinstall_mcpp_deps_for_hooks.sh +++ /dev/null @@ -1,233 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc fresh-sandbox -# Packages installed from an mcpp index may need their mcpp.deps available -# before the package install hook runs. Library deps belong in mcpp.deps; only -# hook-time tools should be declared as xpm deps. -set -e -source "$(dirname "$0")/_host_path.sh" - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT - -export MCPP_HOME="$TMP/mcpp-home" -source "$(dirname "$0")/_inherit_toolchain.sh" -SUBOS_INFO_SEED="$MCPP_HOME/registry/subos/default/.xlings.json" -[[ -f "$SUBOS_INFO_SEED" ]] || { - echo "FAIL: inherited toolchain has no default SubOS runtime contract" - exit 1 -} - -INDEX_DIR="$TMP/local-index" -INDEX_DIR_HOST="$(host_path "$INDEX_DIR")" -mkdir -p "$INDEX_DIR/pkgs/c" - -cat > "$INDEX_DIR/pkgs/c/compat.proto.lua" <<'EOF' -package = { - spec = "1", - namespace = "compat", - name = "compat.proto", - description = "Protocol helper dependency", - licenses = {"MIT"}, - type = "package", - xpm = { - linux = { - ["1.0.0"] = { - url = "https://example.invalid/proto-1.0.0.tar.gz", - sha256 = "0000000000000000000000000000000000000000000000000000000000000000", - }, - }, - }, - mcpp = { - language = "c++23", - import_std = false, - sources = { "src/proto.c" }, - targets = { ["proto"] = { kind = "lib" } }, - deps = {}, - }, -} -EOF - -cat > "$INDEX_DIR/pkgs/c/compat.appdep.lua" <<'EOF' -package = { - spec = "1", - namespace = "compat", - name = "compat.appdep", - description = "Package whose install hook needs mcpp deps", - licenses = {"MIT"}, - type = "package", - xpm = { - linux = { - ["1.0.0"] = { - url = "https://example.invalid/appdep-1.0.0.tar.gz", - sha256 = "0000000000000000000000000000000000000000000000000000000000000000", - }, - }, - }, - mcpp = { - language = "c++23", - import_std = false, - sources = { "src/appdep.c" }, - targets = { ["appdep"] = { kind = "lib" } }, - deps = { - ["compat.proto"] = "1.0.0", - }, - }, -} -EOF - -mkdir -p "$TMP/fake-bin" -FAKE_REGISTRY="$TMP/fake-registry" -FAKE_DIRECT_LOG="$TMP/fake-xlings-direct.log" -FAKE_LOG="$TMP/fake-xlings.log" -mkdir -p "$FAKE_REGISTRY/data" -USER_MCPP="${HOME}/.mcpp" -if [[ -d "$USER_MCPP/registry/data/xpkgs" ]]; then - ln -s "$USER_MCPP/registry/data/xpkgs" "$FAKE_REGISTRY/data/xpkgs" -fi - -cat > "$TMP/fake-bin/xlings" <<'EOF' -#!/usr/bin/env bash -# The handshake mcpp asks of every xlings (docs/25 §5). -[[ "${1:-}" == "interface" && "${2:-}" == "--version" ]] && { echo '{"protocol_version":"1.13","version":"9999.1.1.1","capabilities":["install_packages"],"home_layouts":[2],"features":["index_dir","index_store","upstreams","payload_status","sync_index","payload_currency","binding_state","network_diagnostics","download_socket_budgets","read_only_queries"]}'; exit 0; } -set -e - -if [[ "${1:-}" == "self" && "${2:-}" == "init" ]]; then - mkdir -p "${XLINGS_HOME:?}/subos/default" - cp "${FAKE_XLINGS_SUBOS_INFO:?}" \ - "$XLINGS_HOME/subos/default/.xlings.json" - exit 0 -fi - -# The index refresh: the bare CLI before 2026.9.28.1, the NDJSON interface since. -if [[ "${1:-}" == "update" || ( "${1:-}" == "interface" && "${2:-}" == "update_packages" ) ]]; then - exit 0 -fi - -# Project/custom-index deps install through the NDJSON interface (so the live -# download-progress UI renders). The capability honors XLINGS_PROJECT_DIR and -# installs into the project-local data root; mcpp preinstalls mcpp.deps before -# the dependent package, so compat.proto must land before compat.appdep's hook. -if [[ "${1:-}" == "interface" && "${2:-}" == "install_packages" ]]; then - args="" - while [[ $# -gt 0 ]]; do - if [[ "$1" == "--args" ]]; then args="$2"; break; fi - shift - done - printf '%s\n' "$args" >> "${FAKE_XLINGS_LOG:?}" - if [[ ! -d "${XLINGS_PROJECT_DIR:?}/.xlings/data/compat/pkgs" \ - && ! -d "${XLINGS_PROJECT_DIR:?}/data/compat/pkgs" ]]; then - echo "missing project local path index link" >&2 - find "${XLINGS_PROJECT_DIR:?}" -maxdepth 4 -type d -print >&2 2>/dev/null || true - exit 23 - fi - - case "$args" in - *compat:compat.proto@1.0.0*) - install_root="${XLINGS_PROJECT_DIR:?}/.xlings/data/xpkgs/compat-x-compat.proto/1.0.0" - mkdir -p "$install_root/src" - cat > "$install_root/src/proto.c" <<'SRC' -int proto_value(void) { - return 42; -} -SRC - ;; - *compat:compat.appdep@1.0.0*) - proto_root="${XLINGS_PROJECT_DIR:?}/.xlings/data/xpkgs/compat-x-compat.proto/1.0.0" - if [[ ! -d "$proto_root" ]]; then - echo "mcpp.deps were not installed before appdep install hook" >&2 - exit 24 - fi - install_root="${XLINGS_PROJECT_DIR:?}/.xlings/data/xpkgs/compat-x-compat.appdep/1.0.0" - mkdir -p "$install_root/src" - cat > "$install_root/src/appdep.c" <<'SRC' -extern int proto_value(void); -int app_value(void) { - return proto_value(); -} -SRC - ;; - esac - printf '{"kind":"result","exitCode":0}\n' - exit 0 -fi - -if [[ "${1:-}" == "install" ]]; then - echo "direct install should not be used for project path indices" >&2 - exit 31 -fi - -exit 0 -EOF -chmod +x "$TMP/fake-bin/xlings" - -mkdir -p "$TMP/project/app/src" -cd "$TMP/project/app" - -cat > "$MCPP_HOME/config.toml" < src/app.cpp <<'EOF' -extern "C" int app_value(void); -int app_smoke(void) { - return app_value(); -} -EOF - -cat > mcpp.toml < build.log 2>&1; then - cat build.log - exit 1 -fi - -if ! grep -Fq 'compat:compat.proto@1.0.0' "$FAKE_LOG"; then - echo "FAIL: compat.proto was not installed from mcpp.deps" - cat "$FAKE_LOG" 2>/dev/null || true - exit 1 -fi - -if ! awk ' - /compat:compat.proto@1\.0\.0/ { proto = NR } - /compat:compat.appdep@1\.0\.0/ { appdep = NR } - END { exit !(proto > 0 && appdep > 0 && proto < appdep) } -' "$FAKE_LOG"; then - echo "FAIL: mcpp.deps should be installed before dependent package hook" - cat "$FAKE_LOG" 2>/dev/null || true - exit 1 -fi - -echo "OK" diff --git a/tests/e2e/600_accel_variant_selection.sh b/tests/e2e/600_accel_variant_selection.sh deleted file mode 100755 index bd4665919..000000000 --- a/tests/e2e/600_accel_variant_selection.sh +++ /dev/null @@ -1,114 +0,0 @@ -#!/usr/bin/env bash -# requires: elf -# The accelerator dimension decides which prebuilt artifact a build may take. -# -# Two packages, because the two halves of the rule need different shapes: -# -# gpuonly publishes ONE artifact, for sm_90f. A build targeting sm_86 has -# nothing it can use, so it must be refused — and the refusal must -# name the dimension, not a digest or a triple. -# gpukit publishes a CPU-only artifact FIRST and a device one after it. -# The same sm_86 build must succeed by taking the first, which is -# the ordering that also keeps an mcpp predating this field working. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -TRIPLE="x86_64-linux-gnu" - -make_pkg() { # $1 name, $2... artifact tables - local name="$1"; shift - mkdir -p "$name/lib" "$name/include" - : > "$name/lib/lib${name}.a" - { - echo '[package]' - echo "name = \"$name\"" - echo 'version = "0.1.0"' - echo '[language]' - echo 'standard = "c++23"' - printf '%s\n' "$@" - } > "$name/mcpp.toml" -} - -# One device-only artifact. -make_pkg gpuonly \ -'[[runtime.artifacts]]' \ -'role = "static-library"' \ -'path = "lib/libgpuonly.a"' \ -'provenance = "mcpp-pack/1"' \ -"abi = \"$TRIPLE\"" \ -'accel = "cuda12.8+{sm_90f}"' - -# CPU-only first, device second. -make_pkg gpukit \ -'[[runtime.artifacts]]' \ -'role = "static-library"' \ -'path = "lib/libgpukit.a"' \ -'provenance = "mcpp-pack/1"' \ -"abi = \"$TRIPLE\"" \ -'' \ -'[[runtime.artifacts]]' \ -'role = "static-library"' \ -'path = "lib/libgpukit.a"' \ -'provenance = "mcpp-pack/1"' \ -"abi = \"$TRIPLE\"" \ -'accel = "cuda12.8+{sm_90f}"' - -consumer() { # $1 dep name - rm -rf app; "$MCPP" new app > /dev/null; cd app - cat > mcpp.toml < src/main.cpp <<'EOF' -int main() { return 0; } -EOF - rm -f src/*.cppm - cd .. -} - -# ── 1. a device-only package refuses a build targeting another architecture ── -consumer gpuonly -cd app -if "$MCPP" build --accel 'cuda12.8+{sm_86}' > out.log 2>&1; then - cat out.log; echo "FAIL: sm_86 accepted an sm_90f-only artifact"; exit 1 -fi -grep -q 'accel' out.log || { cat out.log; echo "FAIL: refusal does not name the accel dimension"; exit 1; } -grep -q 'sm_90f' out.log || { cat out.log; echo "FAIL: refusal does not say what the artifact has"; exit 1; } -grep -q 'sm_86' out.log || { cat out.log; echo "FAIL: refusal does not say what this build asked for"; exit 1; } -# The remedy has to be the one that can actually work. Pinning [toolchain] -# cannot change which GPU architecture a build targets, so the generic advice -# is worse than none here. -grep -q -- '--no-accel' out.log \ - || { cat out.log; echo "FAIL: refusal offers no remedy for the device axis"; exit 1; } -grep -q 'accel=cuda12.8+{sm_90f}' out.log \ - || { cat out.log; echo "FAIL: the published listing hides the device dimension"; exit 1; } -cd .. - -# ── 2. the same package accepts the architecture it was built for ──────────── -cd app -"$MCPP" build --accel 'cuda12.8+{sm_90}' > ok.log 2>&1 \ - || { cat ok.log; echo "FAIL: sm_90 refused by an sm_90f artifact"; exit 1; } -cd .. - -# ── 3. a build asking for no accelerator is satisfied vacuously ────────────── -cd app -"$MCPP" build --no-accel > none.log 2>&1 \ - || { cat none.log; echo "FAIL: --no-accel refused"; exit 1; } -cd .. - -# ── 4. CPU-first ordering lets an unmatched device build fall back ─────────── -consumer gpukit -cd app -"$MCPP" build --accel 'cuda12.8+{sm_86}' > fallback.log 2>&1 \ - || { cat fallback.log; echo "FAIL: CPU-only artifact listed first did not accept sm_86"; exit 1; } -cd .. - -echo "PASS: accel variant selection" diff --git a/tests/e2e/605_probe_channel_and_accel_reach_the_build_program.sh b/tests/e2e/605_probe_channel_and_accel_reach_the_build_program.sh deleted file mode 100755 index ce5080f52..000000000 --- a/tests/e2e/605_probe_channel_and_accel_reach_the_build_program.sh +++ /dev/null @@ -1,124 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# The probe channel, and the device axis as a build program sees it. -# -# A rule package is the thing that knows how to ask a machine what it has, and -# the engine is the thing that must not. So the package MEASURES and the engine -# COMPARES: `mcpp::fact` states what the machine has, `mcpp::floor` what the -# package needs of it, and the build is refused before anything is compiled -# when the floor is unmet -- naming both values; `mcpp why toolchain --format -# json` classifies the outcome under the reason -# `version-floor-unmet`. The root's build program is where a rule package -# speaks from, so the check has to run AFTER it; before this test it ran only -# before, and a floor stated there was never compared. -# -# The same program reads the resolved `accel` (`MCPP_ACCEL`), and the -# `cfg(accelerator = "...")` layer key is fed from the same value -- a key that -# was declared, documented, and never written before this. -# -# Nothing here names a vendor: `widget` is the backend, and the engine treats -# it exactly as it would any other. That is the property. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -"$MCPP" new probe > /dev/null; cd probe -rm -f src/*.cppm -cat > src/main.cpp <<'EOF' -// Compiles only when the accelerator layer selected the widget backend: the -// define arrives through a cfg section keyed on `accelerator`. -#ifndef WIDGET_ON -#error "cfg(accelerator = \"widget\") did not match, so WIDGET_ON is missing" -#endif -int main() { return 0; } -EOF - -write_manifest() { # $1 = floor spec - cat > mcpp.toml < build.mcpp <= 2.0" -if "$MCPP" build > refused.log 2>&1; then - cat refused.log; echo "FAIL: a floor above the stated fact was accepted"; exit 1 -fi -grep -q "widget.driver" refused.log || { cat refused.log; echo "FAIL: refusal does not name the quantity"; exit 1; } -grep -q "2.0" refused.log || { cat refused.log; echo "FAIL: refusal does not say what was needed"; exit 1; } -grep -q "1.2" refused.log || { cat refused.log; echo "FAIL: refusal does not say what is there"; exit 1; } -echo "PASS: a floor stated by the root's build program is compared, and refused with both values" - -# The machine interface names the reason, so a tool can act on it without -# parsing prose. `why toolchain` runs the same prepare, build program included. -"$MCPP" why toolchain --format json > refused.json 2>/dev/null || true -reason="$(jq -r '.data.reason // "-"' refused.json | tr -d '\r')" -[[ "$reason" == "version-floor-unmet" ]] || { - cat refused.json; echo "FAIL: reason is '$reason', expected version-floor-unmet"; exit 1; } -echo "PASS: the reason is version-floor-unmet under why toolchain --format json" - -# ── Two: the control -- a met floor builds ────────────────────────────── -write_manifest "widget.driver >= 1.0" -touch src/main.cpp -"$MCPP" build > ok.log 2>&1 || { cat ok.log; echo "FAIL: a met floor was refused"; exit 1; } -echo "PASS: a met floor builds" - -# ── Three: the build program saw the resolved accel ─────────────────────── -# -# The advisory carries whatever `mcpp::accel()` returned. Parsed and printed -# back by the engine, so the spelling is canonical whatever the manifest wrote. -grep -q "accel=\[widget9+{w1,w2}\]" ok.log || { - cat ok.log; echo "FAIL: MCPP_ACCEL did not carry the resolved accel"; exit 1; } -echo "PASS: MCPP_ACCEL carries the resolved accel" - -# ── Four: --no-accel empties both the variable and the layer ────────────── -# -# The layer is the sharper half: with no backend enabled the cfg section must -# NOT apply, and then main.cpp's #error fires. A key that still matched here -# would be reading the manifest rather than the build. -if "$MCPP" build --no-accel > none.log 2>&1; then - cat none.log; echo "FAIL: cfg(accelerator) matched under --no-accel"; exit 1 -fi -grep -q "WIDGET_ON is missing" none.log || { - cat none.log; echo "FAIL: the build failed for another reason than the layer"; exit 1; } -echo "PASS: --no-accel leaves the accelerator layer empty" - -# AND THE VARIABLE, WHICH THIS SECTION CLAIMED AND DID NOT MEASURE. -# `accel_str` prints `(none)` for an empty set so an ABI tag reads as a -# sentence, and handing that spelling on made `MCPP_ACCEL=(none)` reach every -# build program that asked for no accelerator — including every project that -# never mentioned one. The manual promised an empty string; a rule package -# testing "is there an accelerator" got a yes and a backend named `(none)`. -grep -q "accel=\[\]" none.log || { - cat none.log; echo "FAIL: MCPP_ACCEL is not empty under --no-accel"; exit 1; } -echo "PASS: --no-accel leaves MCPP_ACCEL empty, not the display spelling" - -echo "PASS: probe channel and accel reach the build program" diff --git a/tests/e2e/612_build_program_file_input_fast_path.sh b/tests/e2e/612_build_program_file_input_fast_path.sh deleted file mode 100755 index c94eb208a..000000000 --- a/tests/e2e/612_build_program_file_input_fast_path.sh +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 612_build_program_file_input_fast_path.sh — editing a file a build program -# declared with `rerun_if_changed` rebuilds, even on the fast path -# (mcpp 2026.9.5.4+). -# -# The fast path skips prepare_build when no SOURCE is newer than build.ninja, -# and prepare_build is where the build.mcpp cache is consulted. A data file is -# not a source: it has no C++ extension and does not live under src/, so the -# mtime sweep cannot see it, and the staleness check the fast path did run -# asked only about glob path SETS. Editing the file therefore left the header -# generated by the previous run in place, `mcpp build` reported -# "Finished dev in 0.00s", and the program kept compiling the previous bytes. -# -# The criterion is the program's OUTPUT, not a line of build log: a stale -# header and a fresh one differ in what the binary prints. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -"$MCPP" new probe > /dev/null; cd probe -rm -f src/*.cppm -mkdir -p data -printf 'first\n' > data/value.txt - -cat > mcpp.toml <<'EOT' -[package] -name = "probe" -version = "0.1.0" -[language] -standard = "c++23" -modules = true -import_std = true - -[targets.probe] -kind = "bin" -main = "src/main.cpp" -EOT - -# A build program that reads a data file and writes a header from it. No -# action is submitted: the work happens while the program runs, which is the -# shape `mcpp.tools.embed` has and the shape the fast path used to skip. -cat > build.mcpp <<'EOT' -import std; -import mcpp; -int main() { - const std::filesystem::path root(mcpp::manifest_dir()); - const std::filesystem::path gen(std::string(mcpp::out_dir()) + "/include"); - std::error_code ec; - std::filesystem::create_directories(gen, ec); - std::ifstream in(root / "data" / "value.txt"); - std::string value; - std::getline(in, value); - std::ofstream out(gen / "value.h", std::ios::trunc); - out << "#pragma once\n#define PROBE_VALUE \"" << value << "\"\n"; - mcpp::rerun_if_changed((root / "data" / "value.txt").string().c_str()); - mcpp::include_dir(gen.string().c_str()); - return 0; -} -EOT - -cat > src/main.cpp <<'EOT' -#include -#include "value.h" -int main() { std::printf("VALUE=%s\n", PROBE_VALUE); } -EOT - -value() { "$MCPP" run 2>&1 | grep '^VALUE=' | tail -1; } - -"$MCPP" build > b1.log 2>&1 || { cat b1.log; echo "FAIL: the first build failed"; exit 1; } -[[ "$(value)" == "VALUE=first" ]] || { echo "FAIL: the first build did not embed the first value"; exit 1; } - -# Content only. The path set is unchanged, so a glob fingerprint cannot see -# this, and no source's mtime moves. -printf 'second\n' > data/value.txt - -"$MCPP" build > b2.log 2>&1 || { cat b2.log; echo "FAIL: the second build failed"; exit 1; } -out="$(value)" -[[ "$out" == "VALUE=second" ]] || { - echo "FAIL: a declared file input changed and the build replayed the old header ($out)" - cat b2.log - exit 1 -} -echo "PASS: a declared file input is compared on the fast path" - -# The mirror half: an unchanged input must NOT force work. Two builds in a row -# with nothing touched, and the second one has to take the fast path -- without -# this the fix could be "always rebuild", which passes the assertion above and -# is not the behaviour asked for. -"$MCPP" build > b3.log 2>&1 -grep -qE 'Finished .* in ' b3.log || { cat b3.log; echo "FAIL: the third build did not finish"; exit 1; } -"$MCPP" build > b4.log 2>&1 -grep -q 'Compiling probe' b4.log && { cat b4.log; echo "FAIL: an unchanged input recompiled the project"; exit 1; } -echo "PASS: an unchanged input still takes the fast path" diff --git a/tests/e2e/617_build_program_knows_the_cxx_stdlib.sh b/tests/e2e/617_build_program_knows_the_cxx_stdlib.sh deleted file mode 100755 index 6ca78880c..000000000 --- a/tests/e2e/617_build_program_knows_the_cxx_stdlib.sh +++ /dev/null @@ -1,92 +0,0 @@ -#!/usr/bin/env bash -# requires: elf gcc -# `mcpp::cxx_stdlib()` -- the C++ standard library the engine resolved, handed -# to the build program. -# -# WHY IT IS NOT `compiler()`. clang links libc++ on one machine and libstdc++ -# on another and answers "clang" in both cases, and the two implementations -# differ in what they accept: llama.cpp-m's Vulkan backend destroys a -# `unique_ptr` to an incomplete type in an upstream header, which libstdc++ -# accepts and libc++ rejects. A package that wants to refuse that configuration -# by name, before the compiler produces a page of errors, has to be able to ask. -# -# THE CRITERION IS NOT "THE VARIABLE IS SET". An answer that arrives empty, or -# arrives as some other field's value, would also be "set". The build program -# writes what it read to a FILE -- build.mcpp's stdout is printed only when it -# fails, so a criterion that grepped the build log would be silent on success -- -# and the test compares that file against two independent objects: -# -# 1. `resolution.json`, which the engine writes from its own resolved -# toolchain. Same value, different producer and different code path. -# 2. The compiler family. GCC's C++ standard library is libstdc++, and -# nothing else; a wiring that handed over the C library, the ABI tag or -# the empty string would fail here even if (1) somehow agreed. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -mkdir -p app/src -cat > app/src/main.cpp <<'EOF' -int main() { return 0; } -EOF -cat > app/build.mcpp <<'EOF' -#include -#include -#include -import mcpp; -int main() { - const char* stdlib = mcpp::cxx_stdlib(); - const char* compiler = mcpp::compiler(); - // The answer goes to a file. stdout from a build program reaches the user - // only when the program fails, so a test that asserted on the build log - // would pass for a build that printed nothing at all. - std::string out = std::string(mcpp::manifest_dir()) + "/answer.txt"; - std::FILE* f = std::fopen(out.c_str(), "w"); - if (f == nullptr) return 1; - std::fprintf(f, "compiler=%s\nstdlib=%s\n", - compiler == nullptr ? "" : compiler, - stdlib == nullptr ? "" : stdlib); - std::fclose(f); - return 0; -} -EOF -cat > app/mcpp.toml <<'EOF' -[package] -name = "stdlibq" -version = "0.1.0" -[targets.stdlibq] -kind = "bin" -main = "src/main.cpp" -EOF - -cd app -"$MCPP" build >build.log 2>&1 || { echo "FAIL: build"; cat build.log; exit 1; } - -[ -f answer.txt ] || { echo "FAIL: the build program wrote no answer"; cat build.log; exit 1; } -compiler=$(sed -n 's/^compiler=//p' answer.txt) -stdlib=$(sed -n 's/^stdlib=//p' answer.txt) -echo "build program read: compiler='$compiler' stdlib='$stdlib'" - -[ -n "$stdlib" ] || { echo "FAIL: mcpp::cxx_stdlib() is empty"; exit 1; } - -# 1. against resolution.json, which the engine writes from its own resolution. -res=$(find target -name resolution.json -not -path '*/.build-mcpp/*' | head -1) -[ -n "$res" ] || { echo "FAIL: no resolution.json under target/"; exit 1; } -engine=$(sed -n 's/.*"stdlib"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$res" | head -1) -echo "resolution.json ($res) says: '$engine'" -[ "$stdlib" = "$engine" ] || { - echo "FAIL: build program read '$stdlib', the engine resolved '$engine'" - exit 1 -} - -# 2. against the compiler family. This test runs under gcc (see `requires`), -# whose C++ standard library is libstdc++ and is not configurable. -[ "$compiler" = "gcc" ] || { echo "FAIL: expected the gcc toolchain, got '$compiler'"; exit 1; } -[ "$stdlib" = "libstdc++" ] || { - echo "FAIL: gcc's C++ standard library is libstdc++, the build program read '$stdlib'" - exit 1 -} - -echo "PASS: mcpp::cxx_stdlib() answers '$stdlib', matching resolution.json and the compiler family" diff --git a/tests/e2e/618_dependency_feature_tools_reach_its_build_program.sh b/tests/e2e/618_dependency_feature_tools_reach_its_build_program.sh deleted file mode 100755 index 581b28fc2..000000000 --- a/tests/e2e/618_dependency_feature_tools_reach_its_build_program.sh +++ /dev/null @@ -1,121 +0,0 @@ -#!/usr/bin/env bash -# requires: elf gcc -# A DEPENDENCY's `[feature-xlings.]` must be installed before that -# dependency's build.mcpp runs. -# -# THE ROOT AND A DEPENDENCY WERE NOT THE SAME PATH. The root's declarations are -# provisioned early; the graph's were provisioned ~1700 lines further down, after -# every build.mcpp had already run. A package that declares a tool under the -# feature that needs it and then asks for it with `xpkg_dir` therefore worked as -# the root and was refused as a dependency -- with the very declaration it had -# already made quoted back at it. -# -# Measured on the published `ggml-org:llamacpp@b10069.2`: a clean MCPP_HOME -# pulled twenty-four xim payloads for that graph and not `xim:shaderc`. -# -# WHY IT SURVIVED EVERY OTHER CHECK. Once the tool is in the registry for any -# reason -- and building the package itself puts it there -- `xpkg_dir` finds it -# and the ordering stops mattering. The package's own CI builds it as the ROOT. -# Only a registry that has never seen it can tell, which is why this test uses -# an isolated MCPP_HOME rather than the ambient one. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -# THE TOOL MUST BE ONE mcpp DOES NOT INSTALL FOR ITS OWN REASONS. -# -# The first version of this test used `xim:ninja`, and it passed on the broken -# engine: mcpp installs ninja to run its own builds, so `xpkg_dir` answered -# whether or not the feature had been provisioned. The criterion had selected an -# object that is present for an unrelated reason -- it could never have failed. -# -# Measured: a minimal isolated MCPP_HOME holds binutils, gcc, gcc-runtime, -# gcc-specs-config, glibc, linux-headers, ninja and patchelf. `shaderc` is in -# none of that, is 16M, and is the tool the real defect was found with. -TOOL=shaderc -TOOL_VERSION="2026.3" - -mkdir -p dep/src -cat > dep/src/lib.cpp <<'EOF' -int dep_touch() { return 1; } -EOF -cat > dep/build.mcpp < -#include -import mcpp; -int main() { - // THE ASSERTION IS HERE, in the dependency's own build program: the tool it - // declared under an active feature has to be findable by the time it runs. - const char* d = mcpp::xpkg_dir("xim", "$TOOL"); - if (d == nullptr || d[0] == '\0') { - std::fprintf(stderr, - "a dependency's [feature-xlings] tool was not provisioned before " - "its build.mcpp ran\n"); - return 2; - } - // TO A FILE, NOT TO stdout. mcpp prints a build program's output only when - // it FAILS, so an assertion that grepped the build log for this line would - // be unreachable on exactly the run that is supposed to produce it. - std::string out = std::string(mcpp::manifest_dir()) + "/tool-seen.txt"; - std::FILE* f = std::fopen(out.c_str(), "w"); - if (f == nullptr) return 3; - std::fprintf(f, "%s\n", d); - std::fclose(f); - return 0; -} -EOF -cat > dep/mcpp.toml < app/src/main.cpp <<'EOF' -int main() { return 0; } -EOF -cat > app/mcpp.toml <<'EOF' -[package] -name = "consumer" -version = "0.1.0" -[dependencies] -featuretool = { path = "../dep", features = ["usestool"] } -[targets.consumer] -kind = "bin" -main = "src/main.cpp" -EOF - -# AN ISOLATED HOME IS THE POINT. The ambient registry very likely holds the -# tool already, and then this test passes on a broken engine. -export MCPP_HOME="$TMP/home" -mkdir -p "$MCPP_HOME" - -cd app -if ! "$MCPP" build >build.log 2>&1; then - echo "FAIL: the consumer did not build" - grep -iE 'not provisioned|error' build.log | head -5 - exit 1 -fi -seen="$TMP/dep/tool-seen.txt" -[ -s "$seen" ] || { - echo "FAIL: the dependency's build program left no record of finding its tool" - tail -10 build.log - exit 1 -} -echo "the dependency's build program saw its tool at: $(cat "$seen")" -grep -q "xim-x-$TOOL" "$seen" || { - echo "FAIL: the recorded path does not name $TOOL: $(cat "$seen")" - exit 1 -} -echo "PASS: a dependency's [feature-xlings] tool is provisioned before its build.mcpp runs" diff --git a/tests/e2e/622_rule_declared_payload_reaches_the_consumers_build_program.sh b/tests/e2e/622_rule_declared_payload_reaches_the_consumers_build_program.sh deleted file mode 100755 index 1c512b01e..000000000 --- a/tests/e2e/622_rule_declared_payload_reaches_the_consumers_build_program.sh +++ /dev/null @@ -1,127 +0,0 @@ -#!/usr/bin/env bash -# requires: elf gcc -# A RULE PACKAGE'S `[feature-xlings.]` must be findable from the CONSUMER's -# build program, because that is where the rule's code runs. -# -# The sibling case (618) is a dependency reading its OWN declaration from its -# OWN build.mcpp, and that works. This one is different in the only way that -# matters: a rule is compiled INTO its consumer's build program, so -# `mcpp::xpkg_dir` is asked in the consumer's environment while the payload was -# declared in the rule's manifest. `fillXpkgDirs` read one manifest, so the -# address was fetched, unpacked, and then unreachable from the only code that -# wanted it -- an answer of "" that reads as "the toolkit is not installed" -# while it sits on disk. -# -# THE CRITERION IS THE PATH THE RULE ANSWERS WITH, not that the build -# succeeded: a build whose rule silently found nothing succeeds too. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -# Same reasoning as 618: the payload has to be one mcpp does not install for -# its own reasons, or the criterion selects an object that is present anyway. -TOOL=shaderc -TOOL_VERSION="2026.3" - -mkdir -p rule/src -cat > rule/src/rule.cppm < rule/mcpp.toml < app/src/main.cpp <<'EOF' -int main() { return 0; } -EOF -cat > app/build.mcpp <<'EOF' -#include -#include -import mcpp; -import rule; -int main() { - // TO A FILE, NOT stdout: mcpp prints a build program's output only when it - // FAILS, so an assertion grepping the build log would be unreachable on - // exactly the run that is supposed to produce it. - std::string out = std::string(mcpp::manifest_dir()) + "/rule-saw.txt"; - std::FILE* f = std::fopen(out.c_str(), "w"); - if (f == nullptr) return 3; - std::fprintf(f, "%s\n", testrule::tool_dir().c_str()); - std::fclose(f); - return 0; -} -EOF -cat > app/mcpp.toml <<'EOF' -[package] -name = "consumer" -version = "0.1.0" -[language] -standard = "c++23" -modules = true -import_std = true -# `[build-dependencies]`, because the rule must never reach the target, and -# `host-module = true`, because its module must be compiled for the build -# program. Two axes, and this package answers them separately. -[build-dependencies] -rule = { path = "../rule", features = ["usestool"], host-module = true } -[targets.consumer] -kind = "bin" -main = "src/main.cpp" -EOF - -# An isolated home, for 618's reason: the ambient registry very likely holds -# the payload already, and then this passes on a broken engine. -export MCPP_HOME="$TMP/home" -mkdir -p "$MCPP_HOME" - -cd app -if ! "$MCPP" build >build.log 2>&1; then - echo "FAIL: the consumer did not build" - grep -iE 'error|not provisioned' build.log | head -5 - exit 1 -fi -seen="$TMP/app/rule-saw.txt" -[ -s "$seen" ] || { - echo "FAIL: the rule left no record" - tail -10 build.log - exit 1 -} -answer=$(cat "$seen") -echo "the rule, inside the consumer's build program, answered: '${answer}'" -case "$answer" in - *xim-x-$TOOL*) ;; - "") echo "FAIL: the rule got an empty answer -- its payload was installed and unreachable"; exit 1 ;; - *) echo "FAIL: the answer does not name $TOOL"; exit 1 ;; -esac -echo "PASS: a rule package's [feature-xlings] payload is reachable from the consumer's build program" diff --git a/tests/e2e/624_a_missing_host_module_names_the_key.sh b/tests/e2e/624_a_missing_host_module_names_the_key.sh deleted file mode 100755 index 3317c996a..000000000 --- a/tests/e2e/624_a_missing_host_module_names_the_key.sh +++ /dev/null @@ -1,88 +0,0 @@ -#!/usr/bin/env bash -# requires: elf gcc -# An import no dependency provides is refused BY NAME, before the compiler is -# reached. -# -# Left to the compiler the message is -# -# mcpp.rules.spirv: error: failed to read compiled module: No such file or -# directory -# mcpp.rules.spirv: note: imports must be built before being imported -# -# which is true and names neither the package that would provide the module nor -# the key that would make it importable. `host-module = true` and the section a -# dependency is written in are separate axes -- the section says whether the -# package reaches the target, `host-module` whether its module is compiled for -# the build program -- and forgetting the first while getting the second right -# is the ordinary mistake. -# -# THE SET OF NAMES THAT CAN COMPILE HERE IS CLOSED (`std`, `std.compat`, the -# bundled `mcpp`, and the importable host modules), so a name outside it cannot -# become valid later and is refused rather than warned about. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -mkdir -p rule/src -cat > rule/src/rule.cppm <<'EOF' -export module rule; -export namespace testrule { inline int answer() { return 42; } } -EOF -cat > rule/mcpp.toml <<'EOF' -[package] -name = "rule" -version = "0.1.0" -[language] -standard = "c++23" -modules = true -import_std = true -[build] -sources = ["src/rule.cppm"] -[targets.rule] -kind = "lib" -EOF - -mkdir -p app/src -cat > app/src/main.cpp <<'EOF' -int main() { return 0; } -EOF -cat > app/build.mcpp <<'EOF' -import std; -import mcpp; -import rule; -int main() { return testrule::answer() == 42 ? 0 : 1; } -EOF -# Declared, and declared in the right section -- only `host-module` is absent. -# That is the point: the manifest looks correct. -cat > app/mcpp.toml <<'EOF' -[package] -name = "consumer" -version = "0.1.0" -[language] -standard = "c++23" -modules = true -import_std = true -[build-dependencies] -rule = { path = "../rule", host-module = false } -[targets.consumer] -kind = "bin" -main = "src/main.cpp" -EOF - -cd app -out=$("$MCPP" build 2>&1) && { echo "FAIL: the build succeeded with an unprovided import"; exit 1; } -echo "$out" | grep -q "imports 'rule'" || { echo "FAIL: the refusal does not name the module"; echo "$out" | tail -6; exit 1; } -echo "$out" | grep -q 'host-module = true' || { echo "FAIL: the refusal does not name the key"; echo "$out" | tail -6; exit 1; } -echo "$out" | grep -q 'rule (in \[build-dependencies\])' || { echo "FAIL: the refusal does not name the candidate dependency"; echo "$out" | tail -6; exit 1; } -echo "$out" | grep -qi 'failed to read compiled module' && { echo "FAIL: the compiler was reached; the check must run first"; exit 1; } -echo "ok: refused by name, before the compiler" - -# The same project with the key added must build -- otherwise the check could -# be refusing something that was always going to work. -sed -i 's|rule = { path = "../rule", host-module = false }|rule = { path = "../rule", host-module = true }|' mcpp.toml -"$MCPP" build >/dev/null 2>&1 || { echo "FAIL: adding host-module = true did not make it build"; exit 1; } -echo "ok: the fix the message names is the fix that works" - -echo "PASS: an import no dependency provides is refused by name, and the named fix works" diff --git a/tests/e2e/625_a_payload_can_be_gated_on_the_accelerator.sh b/tests/e2e/625_a_payload_can_be_gated_on_the_accelerator.sh deleted file mode 100755 index b6dfa86f0..000000000 --- a/tests/e2e/625_a_payload_can_be_gated_on_the_accelerator.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bash -# requires: elf gcc -# A payload predicated on the ACCELERATOR is installed for a device build and -# not for a CPU-only one. -# -# `accelerator` was grouped with the five resolved layer keys (`c-abi`, -# `compiler`, ...) and refused in `[target.'cfg(...)'.xlings]` for their reason: -# a layer is answered by dependency resolution, which runs after provisioning. -# That reason does not hold for this one key. The accel is an INPUT -- `--accel` -# or `[build] accel` -- read before the first package is resolved. -# -# The cost of the old grouping was paid on every build of every project with a -# device island: the vendor toolkit is declared unconditionally or not at all, -# so a CPU-only build downloaded gigabytes for a device it was not compiling -# for. There was no third spelling. -# -# TWO LEGS AND THE SECOND IS THE POINT. A test that only checked the device leg -# would pass on an engine that installs the payload always. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -# Same selection rule as 618: a payload mcpp does not install for its own -# reasons, or the criterion measures something that is present anyway. -TOOL=shaderc -TOOL_VERSION="2026.3" - -mkdir -p p/src -cat > p/src/main.cpp <<'EOF' -int main() { return 0; } -EOF -cat > p/mcpp.toml <cpu.log 2>&1 || { echo "FAIL: the CPU-only build was refused"; tail -20 cpu.log; exit 1; } -if [ -d "$store" ]; then - echo "FAIL: a CPU-only build installed the device payload at $store" - exit 1 -fi -echo "ok: no accel -- the gated payload was not installed" - -# ── leg 2: the accelerator named -- it must be ───────────────────────────── -"$MCPP" build --accel "vulkan1.2" >dev.log 2>&1 || { echo "FAIL: the device build was refused"; tail -20 dev.log; exit 1; } -[ -d "$store" ] || { - echo "FAIL: the device build did not install the payload its predicate names" - tail -20 dev.log - exit 1 -} -echo "ok: --accel vulkan1.2 -- the gated payload was installed" - -echo "PASS: a payload can be gated on the accelerator, in both directions" diff --git a/tests/e2e/626_naming_a_subset_of_backends_is_not_a_mismatch.sh b/tests/e2e/626_naming_a_subset_of_backends_is_not_a_mismatch.sh deleted file mode 100755 index 862b86c46..000000000 --- a/tests/e2e/626_naming_a_subset_of_backends_is_not_a_mismatch.sh +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env bash -# requires: elf gcc -# A constrained glob whose BACKEND this build never named is left out, not -# refused. A glob whose backend IS named but whose architecture is not covered -# still is. -# -# The refusal is about a real disagreement -- a file written for sm_89 in a -# build targeting sm_80 is not a variant. Across DIFFERENT backends there is no -# disagreement, and refusing there made a build that names a SUBSET of a -# package's backends impossible: a package could have several device backends -# only if every build took all of them. That is the opposite of what an -# additive-backend library needs. -# -# FOUR LEGS, because each states something the others cannot. Leg 2 is the -# change; leg 3 is what must NOT have changed with it; leg 4 is what keeps -# leg 2 from turning a typo into a file that is never compiled. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -mk() { # $1 = the glob's accel, $2 = [package] accelerators list - rm -rf p; mkdir -p p/src/kernels - cat > p/src/main.cpp <<'EOF' -int main() { return 0; } -EOF - cat > p/src/kernels/k.cu <<'EOF' -extern "C" __global__ void k() {} -EOF - cat > p/mcpp.toml < p/build.mcpp <<'EOF' -import std; -import mcpp; -int main() { return 0; } -EOF -} - -# ── leg 1: the backend is named -- the glob comes through ─────────────────── -mk 'cuda12.9+{sm_89}' '"cuda"' -cd p -out=$("$MCPP" build --accel 'cuda12.9+{sm_89}' 2>&1) && { echo "FAIL: the glob did not reach the build"; exit 1; } -echo "$out" | grep -q 'no action compiles' || { echo "FAIL: expected the device-source audit, got:"; echo "$out" | tail -4; exit 1; } -echo "ok: backend named -- the glob is in the build (the audit sees it)" -cd .. - -# ── leg 2: a DIFFERENT backend -- the glob is left out ────────────────────── -mk 'cuda12.9+{sm_89}' '"cuda"' -cd p -"$MCPP" build --accel 'vulkan1.2' >leg2.log 2>&1 || { - echo "FAIL: naming a different backend was refused" - tail -8 leg2.log - exit 1 -} -echo "ok: a different backend -- the glob is left out, as --no-accel leaves it" -cd .. - -# ── leg 3: the SAME backend, an architecture it does not cover -- refused ─── -mk 'cuda12.9+{sm_89}' '"cuda"' -cd p -out=$("$MCPP" build --accel 'cuda12.9+{sm_80}' 2>&1) && { echo "FAIL: an uncovered architecture built"; exit 1; } -echo "$out" | grep -q 'does not cover' || { echo "FAIL: expected the accel mismatch refusal, got:"; echo "$out" | tail -4; exit 1; } -echo "ok: same backend, uncovered architecture -- still refused" -cd .. - -# ── leg 4: a backend the package never declared -- refused ────────────────── -# -# Without this, leg 2 would turn `accel = "cude12.9"` into a glob that is -# quietly never built and never mentioned. -mk 'cude12.9+{sm_89}' '"cuda"' -cd p -out=$("$MCPP" build --accel 'cuda12.9+{sm_89}' 2>&1) && { echo "FAIL: an undeclared backend built"; exit 1; } -echo "$out" | grep -q 'does not declare' || { echo "FAIL: expected the undeclared-backend refusal, got:"; echo "$out" | tail -4; exit 1; } -echo "$out" | grep -q 'cude' || { echo "FAIL: the refusal does not name the misspelling"; exit 1; } -echo "ok: a backend [package] accelerators does not list -- refused, naming it" - -echo "PASS: a subset of backends builds; a mismatch and a misspelling do not" diff --git a/tests/e2e/632_a_build_program_is_told_its_package_name.sh b/tests/e2e/632_a_build_program_is_told_its_package_name.sh deleted file mode 100755 index ae7b283ee..000000000 --- a/tests/e2e/632_a_build_program_is_told_its_package_name.sh +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# A BUILD PROGRAM IS TOLD WHICH PACKAGE IT IS BUILDING. -# -# Every name a rule package generates is derived from this one: the module a -# consumer imports, the namespace the accessors sit in, the symbols in a -# generated header. Until 2026.9.7.1 nothing answered it, and the closest thing -# available was the leaf of `MCPP_MANIFEST_DIR` -- a DIRECTORY name. -# -# THE DIRECTORY AND THE PACKAGE ARE DELIBERATELY DIFFERENT HERE, and that is the -# whole test. A fixture whose package name happens to equal its directory leaf -# passes against both the old derivation and the new one, so it would assert -# nothing. `mcpp.rules.spirv` shipped with exactly that defect: an example laid -# out as `vulkan/app/` with `name = "vulkan-saxpy"` generated `app.shaders`, and -# every `/app/` in a workspace claimed the same module. -# -# The answer is written to a FILE rather than printed, because mcpp shows a -# build program's stdout only when it exits non-zero -- a criterion reading the -# build log would be measuring the failure path. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -# The directory is `app`. The package is not. -mkdir -p app/src -cat > app/src/main.cpp <<'EOF' -int main() { return 0; } -EOF - -cat > app/build.mcpp <<'EOF' -#include -#include -import mcpp; -int main() { - std::string out = std::string(mcpp::manifest_dir()) + "/answered.txt"; - std::FILE* f = std::fopen(out.c_str(), "w"); - if (f == nullptr) return 3; - std::fprintf(f, "name=%s\n", mcpp::package_name()); - std::fprintf(f, "namespace=%s\n", mcpp::package_namespace()); - std::fclose(f); - return 0; -} -EOF - -cat > app/mcpp.toml <<'EOF' -[package] -name = "vulkan-saxpy" -namespace = "example" -version = "0.1.0" - -[build] -sources = ["src/*.cpp"] - -[targets.vulkan-saxpy] -kind = "bin" -main = "src/main.cpp" -EOF - -cd app -"${MCPP:-mcpp}" build > build.log 2>&1 || { echo "FAIL: build"; cat build.log; exit 1; } - -[ -f answered.txt ] || { echo "FAIL: the build program wrote no answer"; exit 1; } -cat answered.txt - -grep -qx 'name=vulkan-saxpy' answered.txt || { - echo "FAIL: package_name() did not answer the [package] name" - echo " (a directory-derived answer would read 'app')" - exit 1; } -grep -qx 'namespace=example' answered.txt || { - echo "FAIL: package_namespace() did not answer the [package] namespace" - exit 1; } - -# THE REVERSE LEG: the directory leaf is `app`, so an implementation that still -# derived from the directory would have written `app` above. Assert the two are -# actually different in this fixture, or the check above proves nothing. -[ "$(basename "$PWD")" = "app" ] || { - echo "FAIL: this fixture no longer distinguishes the two derivations" - exit 1; } - -echo "PASS: a build program reads its package identity, not its directory name" diff --git a/tests/e2e/633_host_modules_are_ordered_by_what_they_import.sh b/tests/e2e/633_host_modules_are_ordered_by_what_they_import.sh deleted file mode 100755 index a89c2834d..000000000 --- a/tests/e2e/633_host_modules_are_ordered_by_what_they_import.sh +++ /dev/null @@ -1,186 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 633_host_modules_are_ordered_by_what_they_import.sh — the units a host-module -# package contributes are compiled in the order their imports require, not in -# the order their paths sort. -# -# THE DEFECT. `build_program.cppm` accumulates module flags as it compiles a -# package's host modules, so each unit sees only the BMIs of those ahead of it. -# The list was the lib root followed by every other declared interface unit in -# PATH order -- a `std::set` of paths. `rules/spirv.cppm` sorts before -# `src/surface.cppm`, so a member importing a unit its package shares was -# compiled first and failed with: -# -# failed to read compiled module: No such file or directory -# note: imports must be built before being imported -# -# Reproduced in both directions before the fix: renaming the shared unit so its -# path sorted first made the SAME package build, which is what says the cause is -# the sort and nothing else. -# -# WHAT IT COST. `mcpp:plugins` read that failure as "a second unit is not -# compiled as a host module at all" and folded everything its members shared -# into the lib root, taking it from about twenty lines to about seven hundred. -# The cause was a sort. -# -# LEG 1 IS THE REVERSED CHAIN. Three units whose import order is the exact -# reverse of their path order: -# -# rules/thing.cppm probe.rules.thing imports probe.mid -# src/mm_mid.cppm probe.mid imports probe.base -# src/zz_base.cppm probe.base -# -# Under a path sort every one of them precedes what it imports. Under an import -# sort every one follows it. Nothing but the ordering decides whether this -# builds. -# -# LEG 2 IS THE UNCONSTRAINED CASE, and it is why the sort keeps path order as -# its tie-break: a package with no internal imports must be ordered exactly as -# it was, so a package that works today cannot be broken by this change. -# -# THE CRITERION IS A VALUE THE MODULES COMPUTE TOGETHER, surfaced through -# `mcpp::warning` because that is the one build-program channel `mcpp build` -# prints on success -- its stdout is shown only when the program FAILS, so -# grepping for anything else would be vacuous. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT - -# ── leg 1: the reversed chain ─────────────────────────────────────────────── -mkdir -p "$TMP/probe/src" "$TMP/probe/rules" "$TMP/app/src" - -cat > "$TMP/probe/mcpp.toml" <<'EOF' -[package] -name = "probe" -namespace = "ex" -version = "0.1.0" - -[language] -standard = "c++23" -modules = true - -[build] -sources = ["src/probe.cppm", "src/mm_mid.cppm", "src/zz_base.cppm"] - -[features] -default = [] -thing = { sources = ["rules/thing.cppm"] } -EOF - -cat > "$TMP/probe/src/probe.cppm" <<'EOF' -export module probe; -export namespace probe { inline int root() { return 1000; } } -EOF - -cat > "$TMP/probe/src/zz_base.cppm" <<'EOF' -export module probe.base; -export namespace probe::base { inline int value() { return 40; } } -EOF - -cat > "$TMP/probe/src/mm_mid.cppm" <<'EOF' -export module probe.mid; -import probe.base; -export namespace probe::mid { inline int value() { return probe::base::value() + 1; } } -EOF - -cat > "$TMP/probe/rules/thing.cppm" <<'EOF' -export module probe.rules.thing; -import probe; -import probe.mid; -export namespace probe::rules::thing { -inline int value() { return probe::root() + probe::mid::value() + 1; } -} -EOF - -cat > "$TMP/app/mcpp.toml" <<'EOF' -[package] -name = "app" -version = "0.1.0" - -[language] -standard = "c++23" -modules = true - -[build-dependencies.ex] -probe = { path = "../probe", features = ["thing"], host-module = true } -EOF - -cat > "$TMP/app/src/main.cpp" <<'EOF' -int main() { return 0; } -EOF - -cat > "$TMP/app/build.mcpp" <<'EOF' -#include -import mcpp; -import probe.rules.thing; -int main() { - const std::string m = "CHAIN=" + std::to_string(probe::rules::thing::value()); - mcpp::warning(m.c_str()); -} -EOF - -cd "$TMP/app" -"$MCPP" build > b1.log 2>&1 || { - cat b1.log - echo "FAIL: a package whose units import each other was compiled in path order" - exit 1 -} -grep -q 'CHAIN=1042' b1.log || { - cat b1.log - echo "FAIL: the chain did not compute 1000 + 41 + 1; the units did not all link" - exit 1 -} - -# ── leg 2: nothing constrains the order, so path order must survive ───────── -# -# Not decoration. A topological sort that ignored the original order would be -# free to emit any valid order, and a package that works today would be -# reordered for no reason -- silent until something depended on it. This asserts -# the sort is stable where it has no constraint to satisfy. -rm -rf "$TMP/probe/src/mm_mid.cppm" "$TMP/probe/src/zz_base.cppm" b1.log -cat > "$TMP/probe/mcpp.toml" <<'EOF' -[package] -name = "probe" -namespace = "ex" -version = "0.1.0" - -[language] -standard = "c++23" -modules = true - -[build] -sources = ["src/probe.cppm", "src/aa_one.cppm", "src/bb_two.cppm"] - -[features] -default = [] -thing = { sources = ["rules/thing.cppm"] } -EOF -cat > "$TMP/probe/src/aa_one.cppm" <<'EOF' -export module probe.one; -export namespace probe::one { inline int value() { return 7; } } -EOF -cat > "$TMP/probe/src/bb_two.cppm" <<'EOF' -export module probe.two; -export namespace probe::two { inline int value() { return 35; } } -EOF -cat > "$TMP/probe/rules/thing.cppm" <<'EOF' -export module probe.rules.thing; -import probe; -import probe.one; -import probe.two; -export namespace probe::rules::thing { -inline int value() { return probe::root() + probe::one::value() + probe::two::value(); } -} -EOF - -rm -rf target -"$MCPP" build > b2.log 2>&1 || { - cat b2.log - echo "FAIL: a package with no internal imports stopped building" - exit 1 -} -grep -q 'CHAIN=1042' b2.log || { - cat b2.log; echo "FAIL: the unconstrained case did not compute 1000 + 7 + 35"; exit 1; } - -echo "PASS: 633 host modules are ordered by what they import" diff --git a/tests/e2e/651_a_build_program_deploys_what_it_generated.sh b/tests/e2e/651_a_build_program_deploys_what_it_generated.sh deleted file mode 100755 index 368d72e1d..000000000 --- a/tests/e2e/651_a_build_program_deploys_what_it_generated.sh +++ /dev/null @@ -1,185 +0,0 @@ -#!/usr/bin/env bash -# requires: elf gcc -# 651_a_build_program_deploys_what_it_generated.sh -- `mcpp::deploy(from, to)` -# (#622 A4): a file a build program produced (an action's own declared -# output) or selected, placed beside the artifact at a path relative to the -# executable's directory. `[runtime] deploy` can only name a file that -# already exists in the package, package-root-relative, because its `from` -# refuses an absolute path -- so it cannot name what an action writes into -# MCPP_OUT_DIR one step later. This is the directive that closes that gap. -# -# What this holds, each with the wrong answer it excludes: -# -# 1. THE CACHE TAG IS NON-EMPTY, SO A REPLAY CARRIES THE ENTRY. Deleting -# bin/ and rebuilding while the build.mcpp cache stays fresh (a cache -# HIT, not a re-run) must still restore the deployed file -- the same -# replay criterion `mcpp:warning=`/`mcpp:pack-format=` are held to. -# 2. `from` MAY BE ABSOLUTE. It is an action's own declared output, which -# the manifest key's `deploy_path_problem("from", ..., false)` refuses. -# 3. THE COPY EDGE'S INPUT IS THE ACTION'S OUTPUT, so ninja orders them -- -# read from the emitted build.ninja, not inferred from a green build. -# 4. `to = "../x"` IS REFUSED, naming the directive and the package. -# 5. MCPP_TARGET_MIN_PLATFORM_VERSION (#622 A11) IS EMPTY ON LINUX. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -export MCPP_HOME=$HOME/.mcpp - -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } - -cd "$TMP" - -# ── The provider: an action writes a file, the program deploys it ────────── -mkdir -p app/src -cd app - -cat > mcpp.toml <<'TOML' -[package] -name = "app" -version = "0.1.0" - -[toolchain] -linux = "gcc@16.1.0" - -[targets.app] -kind = "bin" -main = "src/main.cpp" -TOML - -cat > src/main.cpp <<'EOF' -#include -int main() { std::puts("app"); return 0; } -EOF - -# The action's command is an argv with no shell assumed, so a copy is a -# script rather than an inline shell pipeline. -cat > gen.sh <<'EOF' -#!/usr/bin/env bash -set -e -cp "$1" "$2" -EOF -chmod +x gen.sh - -printf 'hello resource\n' > res.txt - -cat > build.mcpp <<'EOF' -import mcpp; -#include -#include -int main() { - const std::string root = mcpp::manifest_dir(); - const std::string src = root + "/res.txt"; - const std::string out = std::string(mcpp::out_dir()) + "/gen/res.bin"; - - // The generated file: role "source" because it is produced rather than - // performed, though it is not itself compiled (no compilable extension) -- - // is_compilable_output leaves it out of the compile set and simply - // declares it, exactly as protoc's companion .h does. - mcpp::action a; - a.id = "gen-res"; - a.role = "source"; - a.arg((root + "/gen.sh").c_str()).arg(src.c_str()).arg(out.c_str()) - .input(src.c_str()) - .output(out.c_str()) - .submit(); - - // The directive this test exists for: `from` is the action's own - // absolute output, which `[runtime] deploy` could never name. - mcpp::deploy(out.c_str(), "app.resources"); - - // #622 A11, asserted from inside the program rather than inferred: empty - // on Linux. - if (FILE* f = std::fopen((std::string(mcpp::out_dir()) + "/minplat.txt").c_str(), "w")) { - std::fputs(mcpp::min_platform_version(), f); - std::fclose(f); - } - return 0; -} -EOF - -MCPP="${MCPP:-mcpp}" -find_graph() { find target -name build.ninja -not -path '*/.build-mcpp/*' | head -1; } - -# ── 1. a plain build deploys the generated file beside the executable ────── -# `target///bin/...`, never a bare `bin/` at the project -# root — found rather than hardcoded, the way 264_pack_library_is_relocatable -# and 174_cache_modes_and_commands already do. -"$MCPP" build > b1.log 2>&1 || fail "build failed" b1.log -DEPLOYED=$(find target -path '*/bin/app.resources/res.bin' | head -1) -[ -n "$DEPLOYED" ] || fail "the deployed file does not exist" b1.log -BINDIR=$(echo "$DEPLOYED" | sed 's#/app\.resources/res\.bin$##') -grep -qx "hello resource" "$DEPLOYED" \ - || fail "the deployed file does not carry the action's output" "$DEPLOYED" - -# #622 A11: empty on Linux, and printed by mcpp:: itself rather than -# asserted about the environment from the shell, so this is what the -# PROGRAM saw, not what the harness happens to run under. -MINPLAT=$(find target -name minplat.txt | head -1) -[ -n "$MINPLAT" ] || fail "the program never wrote minplat.txt" b1.log -[ -z "$(cat "$MINPLAT")" ] \ - || fail "MCPP_TARGET_MIN_PLATFORM_VERSION was not empty on Linux" "$MINPLAT" - -# ── 2. the copy edge's input is the action's declared output ─────────────── -# Read from the emitted graph, not inferred from a green build: ninja must -# order the copy after the action because the copy's INPUT IS the action's -# OUTPUT, the same file, named the same way in both places. -G=$(find_graph) -[ -n "$G" ] || fail "no build.ninja" b1.log -ACTION_LINE=$(grep " : mcpp_action_" "$G" | grep "gen/res.bin" | head -1) -[ -n "$ACTION_LINE" ] || fail "no build.ninja action edge names gen/res.bin" "$G" -# The first whitespace-separated token after "build " is the action's own -# declared output path (absolute; TMPDIR here has no spaces to escape). -ACTION_OUT=$(echo "$ACTION_LINE" | sed -n 's/^build \([^ ]*\).*/\1/p') -[ -n "$ACTION_OUT" ] || fail "could not read the action's output path" "$G" -STAGE_LINE=$(grep "stage_file" "$G" | grep "app.resources/res.bin" | head -1) -[ -n "$STAGE_LINE" ] || fail "no stage_file edge for app.resources/res.bin" "$G" -echo "$STAGE_LINE" | grep -qF "$ACTION_OUT" \ - || fail "the copy edge's input is not the action's own output path" "$G" - -# ── 3. touching the action's input changes the deployed file ─────────────── -printf 'changed resource\n' > res.txt -touch src/main.cpp # past the whole-project no-op fast path; see 139 -"$MCPP" build > b2.log 2>&1 || fail "second build failed" b2.log -grep -qx "changed resource" "$DEPLOYED" \ - || fail "touching the action's input did not change the deployed file" "$DEPLOYED" - -# ── 4. the replay criterion: bin/ deleted, rebuilt on a build.mcpp cache hit -rm -rf "$BINDIR" -touch src/main.cpp -# A reused program is stated under --verbose only (build output design revision 3, §7.1). -"$MCPP" build -v > b3.log 2>&1 || fail "third build failed" b3.log -grep -qE "^ *build\.mcpp .* cached" b3.log \ - || fail "the third build re-ran build.mcpp; the replay path was not exercised" b3.log -[ -f "$DEPLOYED" ] \ - || fail "the deployed file was not restored on a build.mcpp cache hit" b3.log -grep -qx "changed resource" "$DEPLOYED" \ - || fail "the restored file does not carry the action's latest output" "$DEPLOYED" - -# ── 5. `mcpp pack --pkg-format dir` stages the deployed file ──────────────────── -"$MCPP" pack --pkg-format dir > b4.log 2>&1 || fail "pack --pkg-format dir failed" b4.log -STAGED=$(find target/dist -name res.bin | head -1) -[ -n "$STAGED" ] || fail "mcpp pack --pkg-format dir did not stage the deployed file" b4.log -case "$STAGED" in - */bin/app.resources/res.bin) : ;; - *) fail "the staged file is not at bin/app.resources/res.bin" b4.log ;; -esac - -echo "PASS: deploy directive (a plain build, the graph edge, an input change, the replay, and pack)" - -# ── 6. `to = "../x"` is refused, naming the directive and the package ────── -cd "$TMP" -cp -r app escapes -cd escapes -sed -i 's/name = "app"/name = "escapes"/' mcpp.toml -sed -i 's/mcpp::deploy(out.c_str(), "app.resources")/mcpp::deploy(out.c_str(), "..\/x")/' build.mcpp -set +e -"$MCPP" build > b5.log 2>&1 -rc=$? -set -e -[ "$rc" -ne 0 ] || fail "a deploy 'to' escaping the executable directory was accepted" b5.log -grep -q "deploy" b5.log || fail "the refusal does not name the directive" b5.log -grep -q "escapes" b5.log || fail "the refusal does not name the package" b5.log -grep -q "component" b5.log || fail "the refusal does not name the path problem" b5.log - -echo "PASS: 651_a_build_program_deploys_what_it_generated" diff --git a/tests/e2e/665_a_host_tool_follows_its_source.sh b/tests/e2e/665_a_host_tool_follows_its_source.sh deleted file mode 100755 index 68860ab25..000000000 --- a/tests/e2e/665_a_host_tool_follows_its_source.sh +++ /dev/null @@ -1,137 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 665 -- a cached host tool follows its source (#630, item 6). The tool store -# was keyed on the package's version, so a change to a `path` tool's source -# under an unchanged version was invisible to every consumer until the version -# moved; the build reported success over the previous compiler's output. The -# key now carries a stamp of the tree for a `path` package and the resolved -# commit for a `git` package. Measured in both directions, at one path and -# with different bytes: an edit reaches the consumer, its reversal reaches it -# too, and a tree that did not change is not rebuilt (e2e 187's claim, which -# this test keeps). -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -# An isolated store, so that an entry from a previous run cannot decide. -export MCPP_HOME="$TMP/mcpphome" -mkdir -p "$MCPP_HOME" -if [ -d "$HOME/.mcpp/registry" ]; then - ln -s "$HOME/.mcpp/registry" "$MCPP_HOME/registry" -fi - -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } - -write_tool() { # $1 = directory, $2 = the answer the generator writes - mkdir -p "$1/src" - cat > "$1/mcpp.toml" <<'EOF' -[package] -name = "toolpkg" -version = "0.1.0" - -[targets.gen] -kind = "bin" -main = "src/gen.cpp" -EOF - cat > "$1/src/gen.cpp" < -int main(int argc, char** argv) { - if (argc < 2) return 2; - FILE* f = std::fopen(argv[1], "w"); - if (!f) return 3; - std::fprintf(f, "int generated_answer() { return $2; }\n"); - std::fclose(f); - return 0; -} -EOF -} - -write_app() { # $1 = directory, $2 = the dependency table line - mkdir -p "$1/src" - cat > "$1/mcpp.toml" < "$1/src/main.cpp" <<'EOF' -#include -int generated_answer(); -int main() { std::printf("ANSWER=%d\n", generated_answer()); } -EOF - cat > "$1/build.mcpp" <<'EOF' -#include -#include -#include -import mcpp; -int main() { - const char* tool = mcpp::dep_bin("toolpkg", "gen"); - if (!tool || !*tool) { std::fprintf(stderr, "no tool path\n"); return 1; } - std::string out = std::string(mcpp::out_dir()) + "/gen.cpp"; - std::string cmd = std::string("\"") + tool + "\" \"" + out + "\""; - if (std::system(cmd.c_str()) != 0) { std::fprintf(stderr, "tool failed\n"); return 1; } - mcpp::generated(out.c_str()); -} -EOF -} - -answer() { # builds in the current directory and prints the program's answer - rm -rf target - "$MCPP" build > "$1" 2>&1 || fail "build failed" "$1" - "$MCPP" run 2>&1 | grep '^ANSWER=' | tail -1 -} - -# ── A `path` tool: same version, different bytes, both directions ──────────── -write_tool toolpkg 41 -write_app app 'toolpkg = { path = "../toolpkg", tools = ["gen"] }' -cd app -[ "$(answer b1.log)" = "ANSWER=41" ] || fail "first build: expected 41" b1.log -grep -q "Building.*host tool" b1.log || fail "the first build did not build the tool" b1.log - -# Unchanged tree: a store hit, no rebuild. (The rebuild line is the claim -# e2e 187 makes; a stamp that moved without a change would break it.) -sleep 1 -[ "$(answer b2.log)" = "ANSWER=41" ] || fail "second build: expected 41" b2.log -grep -q "Building.*host tool" b2.log && fail "an unchanged path tool was rebuilt" b2.log -echo "ok: an unchanged path tool is a store hit" - -# The edit, with the version untouched. -sleep 1 -write_tool ../toolpkg 42 -[ "$(answer b3.log)" = "ANSWER=42" ] || fail "after the edit: expected 42" b3.log -grep -q "Building.*host tool" b3.log || fail "the edit did not rebuild the tool" b3.log -echo "ok: an edit to a path tool reaches the consumer without a version bump" - -# And its reversal, which a key that only ever grew would miss. -sleep 1 -write_tool ../toolpkg 41 -[ "$(answer b4.log)" = "ANSWER=41" ] || fail "after the reversal: expected 41" b4.log -grep -q "Building.*host tool" b4.log || fail "the reversal did not rebuild the tool" b4.log -echo "ok: the reversal reaches the consumer too" - -# ── A `git` tool: keyed by the resolved commit ────────────────────────────── -cd "$TMP" -write_tool gittool 7 -git -C gittool init -q -b main -git -C gittool -c user.name=t -c user.email=t@t add -A -git -C gittool -c user.name=t -c user.email=t@t commit -qm "seven" -A=$(git -C gittool rev-parse HEAD) -write_tool gittool 8 -git -C gittool -c user.name=t -c user.email=t@t commit -qam "eight" -B=$(git -C gittool rev-parse HEAD) - -write_app gapp "toolpkg = { git = \"file://$TMP/gittool\", rev = \"$A\", tools = [\"gen\"] }" -cd gapp -[ "$(answer g1.log)" = "ANSWER=7" ] || fail "git rev A: expected 7" g1.log -sed -i "s/$A/$B/" mcpp.toml -[ "$(answer g2.log)" = "ANSWER=8" ] || fail "git rev B: expected 8" g2.log -grep -q "Building.*host tool" g2.log || fail "moving the pin to B did not rebuild the tool" g2.log -# Back to A: the entry built for A is still valid, so nothing is rebuilt. -sed -i "s/$B/$A/" mcpp.toml -[ "$(answer g3.log)" = "ANSWER=7" ] || fail "git rev A again: expected 7" g3.log -grep -q "Building.*host tool" g3.log && fail "the entry for commit A was not reused" g3.log -echo "ok: a git tool is keyed by its commit, and each commit's entry is reused" diff --git a/tests/e2e/683_a_build_program_reads_the_payload_pkg_config_view.sh b/tests/e2e/683_a_build_program_reads_the_payload_pkg_config_view.sh deleted file mode 100755 index 90df6055f..000000000 --- a/tests/e2e/683_a_build_program_reads_the_payload_pkg_config_view.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash -# 683 -- `mcpp::pkg_config_libdir()` names the pkg-config directories of the -# registry SubOS, the view payload recipes declare their `.pc` files into -# (#634, A7). It is an accessor and not an environment default, so the host's -# pkg-config database is untouched for a package that means it. -# -# Legs: -# A. The value is the registry SubOS's `usr/lib/pkgconfig` and -# `usr/share/pkgconfig`, in that order. -# B. The build program's environment does not carry PKG_CONFIG_LIBDIR. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT - -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } - -mkdir -p "$TMP/app/src" -cd "$TMP/app" -printf '[package]\nname = "app"\nversion = "0.1.0"\n' > mcpp.toml -printf 'int main() { return 0; }\n' > src/main.cpp -cat > build.mcpp <<'CPP' -import std; -import mcpp; -int main() { - std::string line = std::string("libdir=") + mcpp::pkg_config_libdir(); - const char* inherited = std::getenv("PKG_CONFIG_LIBDIR"); - line += std::string(" inherited=") + (inherited ? inherited : "(unset)"); - mcpp::warning(line.c_str()); - return 0; -} -CPP -env -u PKG_CONFIG_LIBDIR "$MCPP" build > build.log 2>&1 || fail "build failed" build.log - -registry=$("$MCPP" self env 2>/dev/null | sed -n 's/^xlings home *= *//p' | head -1) -[ -n "$registry" ] || fail "cannot read the registry from mcpp self env" -# `self env` prints the native spelling (`C:\Users\...` on Windows); the -# accessor joins generic, `/`-separated paths with the platform's list -# separator, which the `?` below matches. -registry=${registry//\\//} -line=$(grep -o 'libdir=.*' build.log | head -1) -case "$line" in - "libdir=$registry/subos/default/usr/lib/pkgconfig"?"$registry/subos/default/usr/share/pkgconfig inherited=(unset)") ;; - *) fail "A/B: the build program read '$line'" build.log ;; -esac - -echo "OK" diff --git a/tests/e2e/693_the_root_build_program_reads_dependency_link_forms.sh b/tests/e2e/693_the_root_build_program_reads_dependency_link_forms.sh deleted file mode 100644 index 7e622f043..000000000 --- a/tests/e2e/693_the_root_build_program_reads_dependency_link_forms.sh +++ /dev/null @@ -1,169 +0,0 @@ -#!/usr/bin/env bash -# requires: elf -# 693 -- the root package's build program reads the link form each dependency -# takes in this build (#642 E2): `mcpp::dep_linkage(name)`, over -# MCPP_DEP__LINKAGE, answers "static" or "shared" from the same -# resolution that decides what is linked. A dependency's own program is not -# offered the forms. -# -# Legs: -# A. A package whose default form is shared and a package whose only sources -# are generated by its own build program, requested shared: the root -# program reads `fw=shared gen=shared`, and both shared libraries exist. -# The generated-only package is the case a computation placed before the -# dependency programs would read as having no sources. -# B. A second build with nothing changed does not re-run the root program. -# C. The dependency's default changes to static in its own manifest, the root -# manifest unchanged: the root program re-runs and reads `fw=static`, and -# no `libfw.so` is built. -# D. The consumer's edge asks for shared again: the program reads -# `fw=shared`, matching `libfw.so`. -# E. In every leg, a dependency's program reads "" for its own dependency -# while dep_dir() answers for the same name; `gen`'s program fails the -# build otherwise. -# F. A dependency whose manifest states `namespace = "ns"` and `name = -# "nsfw"` is named `ns.nsfw` by its consumer, and the root program reads -# both `dep_linkage` and `dep_dir` under that spelling as well as under the -# bare name. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT - -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } - -# A leaf library, depended on by `gen`. -mkdir -p "$TMP/leaf/src" -printf '[package]\nname = "leaf"\nversion = "0.1.0"\n[targets.leaf]\nkind = "lib"\n' > "$TMP/leaf/mcpp.toml" -printf 'int leaf_value() { return 1; }\n' > "$TMP/leaf/src/leaf.cpp" - -# `fw`: a library whose default form is shared. -mkdir -p "$TMP/fw/src" -write_fw() { # write_fw - printf '[package]\nname = "fw"\nversion = "0.1.0"\n[targets.fw]\nkind = "lib"\nlinkage = "%s"\n' \ - "$1" > "$TMP/fw/mcpp.toml" -} -write_fw shared -printf 'int fw_value() { return 40; }\n' > "$TMP/fw/src/fw.cpp" - -# `gen`: no source of its own on disk; its build program generates the one -# translation unit it has, and asserts what a dependency's program is offered. -mkdir -p "$TMP/gen" -cat > "$TMP/gen/mcpp.toml" <<'EOF' -[package] -name = "gen" -version = "0.1.0" -[modules] -sources = [] -[dependencies] -leaf = { path = "../leaf" } -[targets.gen] -kind = "lib" -EOF -cat > "$TMP/gen/build.mcpp" <<'EOF' -#include -#include -import mcpp; -int main() { - const char* dir = mcpp::dep_dir("leaf"); - const char* form = mcpp::dep_linkage("leaf"); - if (dir == nullptr || dir[0] == '\0') { - std::fprintf(stderr, "gen: dep_dir(leaf) is empty\n"); - return 1; - } - if (form != nullptr && form[0] != '\0') { - std::fprintf(stderr, "gen: a dependency's program was offered dep_linkage(leaf)=%s\n", form); - return 1; - } - std::string out = std::string(mcpp::out_dir()) + "/gen.cpp"; - std::FILE* f = std::fopen(out.c_str(), "w"); - std::fprintf(f, "int leaf_value();\nint gen_value() { return leaf_value() + 1; }\n"); - std::fclose(f); - mcpp::generated("gen.cpp"); - return 0; -} -EOF - -# The root: its build program records what it reads, one line per run. -mkdir -p "$TMP/app/src" -write_app() { # write_app - printf '[package]\nname = "app"\nversion = "0.1.0"\n[dependencies]\nfw = { path = "../fw"%s }\ngen = { path = "../gen", linkage = "shared" }\n' \ - "$1" > "$TMP/app/mcpp.toml" -} -write_app '' -cat > "$TMP/app/build.mcpp" <<'EOF' -#include -#include -import mcpp; -int main() { - std::string line = std::string("fw=") + mcpp::dep_linkage("fw") - + " gen=" + mcpp::dep_linkage("gen") + "\n"; - std::string log = std::string(mcpp::manifest_dir()) + "/runs.log"; - std::FILE* f = std::fopen(log.c_str(), "a"); - std::fputs(line.c_str(), f); - std::fclose(f); - return 0; -} -EOF -printf 'int fw_value();\nint gen_value();\nint main() { return fw_value() + gen_value() == 42 ? 0 : 1; }\n' \ - > "$TMP/app/src/main.cpp" - -cd "$TMP/app" -libfw() { find target -name libfw.so | head -1; } -libgen() { find target -name libgen.so | head -1; } - -# ── A ────────────────────────────────────────────────────────────────────── -"$MCPP" build > a.log 2>&1 || fail "A: build failed" a.log -[ "$(tail -1 runs.log)" = "fw=shared gen=shared" ] || fail "A: the root program read '$(tail -1 runs.log)'" a.log -[ -n "$(libfw)" ] || fail "A: no libfw.so" a.log -[ -n "$(libgen)" ] || fail "A: no libgen.so for a package whose sources are all generated" a.log -"$(find target -type f -name app -path '*/bin/*' | head -1)" || fail "A: the program did not run" - -# ── B ────────────────────────────────────────────────────────────────────── -"$MCPP" build > b.log 2>&1 || fail "B: build failed" b.log -[ "$(wc -l < runs.log)" -eq 1 ] || fail "B: an unchanged build re-ran the root program" runs.log - -# ── C ────────────────────────────────────────────────────────────────────── -write_fw static -"$MCPP" build > c.log 2>&1 || fail "C: build failed" c.log -[ "$(wc -l < runs.log)" -eq 2 ] || fail "C: a changed dependency form did not re-run the root program" runs.log c.log -[ "$(tail -1 runs.log)" = "fw=static gen=shared" ] || fail "C: the root program read '$(tail -1 runs.log)'" c.log -rm -rf target -"$MCPP" build > c2.log 2>&1 || fail "C: clean build failed" c2.log -[ -z "$(libfw)" ] || fail "C: libfw.so was built while the program read static" c2.log - -# ── D ────────────────────────────────────────────────────────────────────── -write_app ', linkage = "shared"' -"$MCPP" build > d.log 2>&1 || fail "D: build failed" d.log -[ "$(tail -1 runs.log)" = "fw=shared gen=shared" ] || fail "D: the root program read '$(tail -1 runs.log)'" d.log -[ -n "$(libfw)" ] || fail "D: no libfw.so while the program read shared" d.log - -# ── F ────────────────────────────────────────────────────────────────────── -mkdir -p "$TMP/nsfw/src" "$TMP/nsapp/src" -printf '[package]\nnamespace = "ns"\nname = "nsfw"\nversion = "0.1.0"\n[targets.nsfw]\nkind = "lib"\nlinkage = "shared"\n' \ - > "$TMP/nsfw/mcpp.toml" -printf 'int nsfw_value() { return 42; }\n' > "$TMP/nsfw/src/nsfw.cpp" -printf '[package]\nname = "nsapp"\nversion = "0.1.0"\n[dependencies]\nns.nsfw = { path = "../nsfw" }\n' \ - > "$TMP/nsapp/mcpp.toml" -printf 'int nsfw_value();\nint main() { return nsfw_value() == 42 ? 0 : 1; }\n' > "$TMP/nsapp/src/main.cpp" -cat > "$TMP/nsapp/build.mcpp" <<'EOF' -#include -#include -import mcpp; -int main() { - std::string line = std::string("qualified=") + mcpp::dep_linkage("ns.nsfw") - + " bare=" + mcpp::dep_linkage("nsfw") - + " dir=" + (mcpp::dep_dir("ns.nsfw")[0] != '\0' ? "yes" : "no") + "\n"; - std::string log = std::string(mcpp::manifest_dir()) + "/runs.log"; - std::FILE* f = std::fopen(log.c_str(), "a"); - std::fputs(line.c_str(), f); - std::fclose(f); - return 0; -} -EOF -cd "$TMP/nsapp" -"$MCPP" build > f.log 2>&1 || fail "F: build failed" f.log -[ "$(tail -1 runs.log)" = "qualified=shared bare=shared dir=yes" ] \ - || fail "F: the root program read '$(tail -1 runs.log)'" f.log - -echo "OK" diff --git a/tests/e2e/698_a_tool_source_under_its_dependency_has_a_bounded_address.sh b/tests/e2e/698_a_tool_source_under_its_dependency_has_a_bounded_address.sh deleted file mode 100755 index 7de897037..000000000 --- a/tests/e2e/698_a_tool_source_under_its_dependency_has_a_bounded_address.sh +++ /dev/null @@ -1,225 +0,0 @@ -#!/usr/bin/env bash -# 698 -- a source that a package's build program takes from under its -# dependency's root has an object address that does not grow with the distance -# between the two packages, and a tool built that way through `tools = [...]` -# builds and runs (#641, item 3). -# -# The reported failure was on windows-2022: a host tool's build program added -# `mcpp::source("/platform/windows/x.cpp")`, the object -# address mirrored the path from the tool package to the file with one `__up` -# per level, the `.ddi` path reached 271 characters inside the tool store's -# sub-build scratch, and `mcpp dyndep` could not open it. Three changes answer -# it: the address is `obj//__pkg//`, -# the sub-build scratch is `/tool/.build/`, and the engine's -# ninja-invoked subcommands open extended-length paths on Windows. -# -# A the address names the owning package, has no `__up`, and is the same -# when the declaring package sits two directories deeper (portable); -# B a consumer builds the package as a host tool and runs its output; -# C the same with the file nested deep inside the dependency, so that on a -# Windows runner the longest sub-build path passes 260 characters; the -# build must succeed wherever the runner enables long paths, and the -# readings say which case this runner is. -# -# No `# requires:` line: the host's default toolchain builds every leg. -set -e - -source "$(dirname "$0")/_host_path.sh" - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } - -is_windows=0 -case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) is_windows=1 ;; esac - -# $1 = dependency directory, $2 = directory inside it that holds the helper -write_dep() { - mkdir -p "$1/src" "$1/$2" - cat > "$1/mcpp.toml" <<'EOF' -[package] -name = "dep" -version = "0.1.0" - -[build] -sources = ["src/*.cpp"] - -[targets.dep] -kind = "lib" -EOF - echo 'int dep_base() { return 40; }' > "$1/src/dep.cpp" - echo 'int dep_base(); int helper_answer() { return dep_base() + 2; }' \ - > "$1/$2/helper.cpp" -} - -# $1 = installer directory, $2 = path to the dependency from it, -# $3 = directory of the helper inside the dependency -write_installer() { - local DEP_HOST - DEP_HOST=$(host_path "$2") - mkdir -p "$1/src" - cat > "$1/mcpp.toml" < "$1/src/main.cpp" <<'EOF' -#include -int helper_answer(); -int main(int argc, char** argv) { - if (argc < 2) { std::printf("HELPER=%d\n", helper_answer()); return 0; } - FILE* f = std::fopen(argv[1], "w"); - if (!f) return 3; - std::fprintf(f, "int generated_answer() { return %d; }\n", helper_answer()); - std::fclose(f); - return 0; -} -EOF - cat > "$1/build.mcpp" < -import mcpp; -int main() { - const char* dep = mcpp::dep_dir("dep"); - if (!dep || !*dep) return 1; - mcpp::source((std::string(dep) + "/$3/helper.cpp").c_str()); -} -EOF -} - -# $1 = consumer directory, $2 = path to the installer from it -write_app() { - local INSTALLER_HOST - INSTALLER_HOST=$(host_path "$2") - mkdir -p "$1/src" - cat > "$1/mcpp.toml" < "$1/src/main.cpp" <<'EOF' -#include -int generated_answer(); -int main() { std::printf("ANSWER=%d\n", generated_answer()); } -EOF - cat > "$1/build.mcpp" <<'EOF' -#include -#include -#include -import mcpp; -int main() { - const char* tool = mcpp::dep_bin("installer", "installer-app"); - if (!tool || !*tool) { std::fprintf(stderr, "no tool path\n"); return 1; } - std::string out = std::string(mcpp::out_dir()) + "/gen.cpp"; - std::string cmd = std::string("\"") + tool + "\" \"" + out + "\""; -#ifdef _WIN32 - // cmd.exe strips the first and the last quote of the string it is given, - // which splits two quoted words; a further pair keeps both intact. - cmd = "\"" + cmd + "\""; -#endif - if (std::system(cmd.c_str()) != 0) { std::fprintf(stderr, "tool failed\n"); return 1; } - mcpp::generated(out.c_str()); -} -EOF -} - -# The object address of the helper, read from the plan ninja was given. -helper_address() { # $1 = package directory - local ninja - ninja=$(ls "$1"/target/*/*/build.ninja 2>/dev/null | head -1) - [ -n "$ninja" ] || return 1 - grep -o 'obj/[^ :|]*helper\.o\(bj\)\?' "$ninja" | head -1 -} - -# ── A: the address ────────────────────────────────────────────────────────── -write_dep fw/dep platform/impl -write_installer fw/tools/installer ../../dep platform/impl -(cd fw/tools/installer && "$MCPP" build > "$TMP/a1.log" 2>&1) \ - || fail "the installer did not build at its own root" a1.log -near=$(helper_address fw/tools/installer) || fail "no build.ninja for the installer" a1.log -[ -n "$near" ] || fail "no object address for helper.cpp in the installer's plan" a1.log -echo "READING A address: $near" -case "$near" in - *__up*) fail "the address climbs with __up: $near" ;; -esac -case "$near" in - # The owner's slug is its qualified name (`mcpplibs.dep` for a path - # package that states no namespace), spelled with `_`. - obj/installer/__pkg/*dep/platform/impl/helper.o|obj/installer/__pkg/*dep/platform/impl/helper.obj) ;; - *) fail "the address does not name the owning package and the path inside it: $near" ;; -esac - -write_installer fw/tools/windows/nested/installer ../../../../dep platform/impl -(cd fw/tools/windows/nested/installer && "$MCPP" build > "$TMP/a2.log" 2>&1) \ - || fail "the deeper installer did not build" a2.log -deeper=$(helper_address fw/tools/windows/nested/installer) -echo "READING A deeper address: $deeper" -[ "$deeper" = "$near" ] || fail "moving the installer two levels deeper changed the address: $near -> $deeper" -out=$(cd fw/tools/windows/nested/installer && "$MCPP" run 2>&1 | grep '^HELPER=' | tail -1) -[ "$out" = "HELPER=42" ] || fail "the installer does not run its dependency's helper: $out" -echo "ok: the address names the owning package and does not grow with distance" - -# ── B: through `tools = [...]` ───────────────────────────────────────────── -write_app app ../fw/tools/windows/nested/installer -(cd app && "$MCPP" build > "$TMP/b.log" 2>&1) || fail "the consumer did not build the tool" b.log -grep -q "host tool" b.log || fail "no host tool was built" b.log -out=$(cd app && "$MCPP" run 2>&1 | grep '^ANSWER=' | tail -1) -[ "$out" = "ANSWER=42" ] || fail "the tool's output did not reach the consumer: $out" b.log -echo "ok: the tool builds through tools = [...] and its output is linked" - -# ── C: a deep file inside the dependency ─────────────────────────────────── -deep="platform/windows/installer_interface_components/generated_resources_and_views/bootstrapper_application_integration" -write_dep deep/dep "$deep" -write_installer deep/tools/installer ../../dep "$deep" -write_app deep/app ../tools/installer - -cache_dir=$("$MCPP" self env 2>/dev/null | sed -n 's/^build cache *= *//p' | head -1) -(cd deep/tools/installer && "$MCPP" build > "$TMP/c0.log" 2>&1) \ - || fail "the deep installer did not build at its own root" c0.log -deep_addr=$(helper_address deep/tools/installer) -deep_build=$(ls -d deep/tools/installer/target/*/*/ | head -1) -triple_fp=${deep_build#deep/tools/installer/} -# `/tool/.build/<16 hex>/` + `target///` + the -# address and the scan output beside it (`.ddi`). -scratch_tail="/tool/.build/0123456789abcdef/" -longest=$(( ${#cache_dir} + ${#scratch_tail} + ${#triple_fp} + ${#deep_addr} + 8 )) -echo "READING C cache directory: $cache_dir" -echo "READING C longest sub-build path (estimated, characters): $longest" - -long_paths=unknown -if [ "$is_windows" = 1 ]; then - long_paths=$(powershell -NoProfile -Command \ - "(Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem').LongPathsEnabled" \ - 2>/dev/null | tr -d '\r' || true) - [ -n "$long_paths" ] || long_paths=unreadable -fi -echo "READING C LongPathsEnabled: $long_paths" - -set +e -(cd deep/app && "$MCPP" build > "$TMP/c.log" 2>&1) -rc=$? -set -e -echo "READING C deep tool build exit: $rc" -if [ "$rc" = 0 ]; then - out=$(cd deep/app && "$MCPP" run 2>&1 | grep '^ANSWER=' | tail -1) - [ "$out" = "ANSWER=42" ] || fail "the deep tool's output did not reach the consumer: $out" c.log - echo "ok: a tool whose sub-build paths are $longest characters long builds" -elif [ "$is_windows" = 1 ] && [ "$long_paths" != 1 ]; then - echo "READING C the runner does not enable long paths; the failure is recorded, not asserted" - grep -m5 -E "error|failed" c.log | sed 's/^/ /' -else - fail "the deep tool did not build" c.log -fi - -echo "PASS: 698" diff --git a/tests/e2e/712_a_tool_package_contributes_nothing_to_its_consumer.sh b/tests/e2e/712_a_tool_package_contributes_nothing_to_its_consumer.sh deleted file mode 100755 index 8095de201..000000000 --- a/tests/e2e/712_a_tool_package_contributes_nothing_to_its_consumer.sh +++ /dev/null @@ -1,200 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 712 -- a package whose declared targets are all programs contributes nothing -# to a consumer's graph; its tools come from the tool sub-build (#649 E6). -# -# An SDK provides a program behind a feature, and the program depends on the -# SDK. The tool sub-build resolves the program's package as its own root, so -# the program and the SDK it links are built there. The consumer's graph used -# to walk the program package's `[dependencies]` as well, which made the -# declaration a cycle of the consumer's graph ("dependency cycle through -# package 'fw' while computing its build-cache key", default cache only) and -# linked a tool's own library into the application. -# -# Readings: the SDK fixture builds with the default cache, the tool runs, and -# the build program reads its path under both spellings; a tool's own library -# is not linked into the application; a cycle among libraries is refused with -# the same message under both cache modes; a tool whose own build activates -# the feature that requests it is refused once, naming the repetition. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -export MCPP_HOME="$TMP/mcpphome" -mkdir -p "$MCPP_HOME" -if [ -d "$HOME/.mcpp/registry" ]; then - ln -s "$HOME/.mcpp/registry" "$MCPP_HOME/registry" -fi - -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } - -# fixture -fixture() { - local W=$1 - mkdir -p "$W/fw/src" "$W/fw/tool/src" "$W/app/src" "$W/z/src" - cat > "$W/fw/mcpp.toml" <<'EOF' -[package] -name = "fw" -namespace = "spike" -version = "0.1.0" - -[targets.fw] -kind = "lib" - -[build] -sources = ["src/*.cpp"] - -[features] -installer = [] - -[feature-deps.installer] -spike.fw-installer = { path = "tool", tools = ["fw-installer"], reexport = true } -EOF - echo 'int fw_answer() { return 42; }' > "$W/fw/src/fw.cpp" - cat > "$W/fw/tool/mcpp.toml" < "$W/z/mcpp.toml" <<'EOF' -[package] -name = "z" -namespace = "spike" -version = "0.1.0" - -[targets.z] -kind = "lib" - -[build] -sources = ["src/*.cpp"] -EOF - echo 'int z_only_in_the_tool() { return 7; }' > "$W/z/src/z.cpp" - cat > "$W/app/mcpp.toml" <<'EOF' -[package] -name = "app" -version = "0.1.0" - -[targets.app] -kind = "bin" -main = "src/main.cpp" - -[dependencies] -spike.fw = { path = "../fw" } - -[features] -windows-installer = ["spike.fw/installer"] -EOF - cat > "$W/app/src/main.cpp" <<'EOF' -int fw_answer(); -int main() { return fw_answer() == 42 ? 0 : 1; } -EOF - cat > "$W/app/build.mcpp" <<'EOF' -import std; -import mcpp; -int main() { - const char* a = mcpp::dep_bin("fw-installer", "fw-installer"); - const char* b = mcpp::dep_bin("spike.fw-installer", "fw-installer"); - mcpp::warning((std::string("DEPBIN short=[") + (a ? a : "") + "] qualified=[" - + (b ? b : "") + "]").c_str()); - return 0; -} -EOF -} - -# ── 1. the SDK shape: the tool depends on the package declaring it ──────── -fixture "$TMP/s1" 'spike.fw = { path = ".." }' -cat > "$TMP/s1/fw/tool/src/main.cpp" <<'EOF' -#include -int fw_answer(); -int main() { std::printf("fw-installer ran %d\n", fw_answer()); return 0; } -EOF -cd "$TMP/s1/app" -"$MCPP" build --features windows-installer > b1.log 2>&1 \ - || fail "a feature tool depending on its declaring package did not build" b1.log -tool=$(grep -o 'DEPBIN short=\[[^]]*' b1.log | sed 's/DEPBIN short=\[//') -[ -n "$tool" ] || fail "dep_bin(\"fw-installer\") read nothing" b1.log -grep -q 'qualified=\[[^]]' b1.log || fail "dep_bin(\"spike.fw-installer\") read nothing" b1.log -out=$("$tool") || fail "the tool did not run: $out" -[ "$out" = "fw-installer ran 42" ] || fail "the tool printed '$out'" -"$MCPP" run > r1.log 2>&1 || fail "the application does not run" r1.log - -# ── 2. a tool's own library does not reach the application ────────────── -fixture "$TMP/s2" 'spike.z = { path = "../../z" }' -cat > "$TMP/s2/fw/tool/src/main.cpp" <<'EOF' -#include -int z_only_in_the_tool(); -int main() { std::printf("tool %d\n", z_only_in_the_tool()); return 0; } -EOF -cd "$TMP/s2/app" -"$MCPP" build --features windows-installer > b2.log 2>&1 || fail "the tool-with-a-library build failed" b2.log -ninja_file=$(find target -name build.ninja -not -path '*/.build-mcpp/*' | head -1) -grep '^build bin/app' "$ninja_file" | grep -q 'spike_z' \ - && fail "the tool's own library was linked into the application" "$ninja_file" -grep -q 'spike_fw-installer' "$ninja_file" \ - && fail "the tool package's sources were compiled in the consumer's build" "$ninja_file" - -# ── 3. a cycle among libraries is refused under both cache modes ───────── -mkdir -p "$TMP/s3/a/src" "$TMP/s3/b/src" "$TMP/s3/app/src" -for p in a b; do - q=$([ "$p" = a ] && echo b || echo a) - cat > "$TMP/s3/$p/mcpp.toml" < "$TMP/s3/$p/src/$p.cpp" -done -cat > "$TMP/s3/app/mcpp.toml" <<'EOF' -[package] -name = "app" -version = "0.1.0" - -[targets.app] -kind = "bin" -main = "src/main.cpp" - -[dependencies] -spike.a = { path = "../a" } -EOF -echo 'int main() { return 0; }' > "$TMP/s3/app/src/main.cpp" -cd "$TMP/s3/app" -for mode in "" "--cache=local"; do - if "$MCPP" build $mode > b3.log 2>&1; then - fail "a cycle of packages was accepted (${mode:-default cache})" b3.log - fi - grep -q "dependency cycle: 'spike.a' -> 'spike.b' -> 'spike.a'" b3.log \ - || fail "the cycle refusal does not name its edges (${mode:-default cache})" b3.log -done - -# ── 4. a tool whose own build requests it again is refused once ────────── -fixture "$TMP/s4" 'spike.fw = { path = "..", features = ["installer"] }' -echo 'int main() { return 0; }' > "$TMP/s4/fw/tool/src/main.cpp" -cd "$TMP/s4/app" -if "$MCPP" build --features windows-installer > b4.log 2>&1; then - fail "a tool requesting itself through its own build was accepted" b4.log -fi -grep -q "is requested by its own build" b4.log || fail "the repetition is not named" b4.log -[ "$(grep -o 'building host tool' b4.log | wc -l | tr -d ' ')" -le 1 ] \ - || fail "the repetition was refused only after nested sub-builds" b4.log - -echo "PASS: 712 a package of programs contributes nothing to its consumer's graph" diff --git a/tests/e2e/720_the_root_build_program_reads_the_resolved_graph.sh b/tests/e2e/720_the_root_build_program_reads_the_resolved_graph.sh deleted file mode 100755 index 8561ae34d..000000000 --- a/tests/e2e/720_the_root_build_program_reads_the_resolved_graph.sh +++ /dev/null @@ -1,176 +0,0 @@ -#!/usr/bin/env bash -# requires: python3 -# 720 -- the root package's build program reads the resolved dependency graph -# and each package's `[package.metadata]` through `mcpp::graph_file()` (#647 E1). -# -# A framework merges what its libraries contribute (resources, platform -# sources) at build time, in dependency order, including libraries the -# application does not name. `dep_dir` answers by name for a direct dependency -# only, so a transitive library had no name to be asked for. -# -# Legs: -# A. app -> spike.a -> spike.b: the document lists b before a before app, the -# root last; b's entry carries its absolute manifest directory, its target -# kind and `metadata.demo.resources = "res"` verbatim. -# B. A second build with nothing changed does not re-run the root program. -# C. Editing b's `[package.metadata]` re-runs the root program, and the -# document states the new value. -# D. Editing b's source does not re-run the root program. -# E. A dependency's program reads "" from graph_file(). -# F. `[package]` reports an unknown key the way `[build]` does: a warning, -# an error under --strict; `metadata` is a known key. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT - -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } - -mkdir -p "$TMP/b/src" "$TMP/a/src" "$TMP/app/src" - -write_b() { # write_b - cat > "$TMP/b/mcpp.toml" < "$TMP/b/src/b.cpp" - -cat > "$TMP/a/mcpp.toml" <<'EOF' -[package] -name = "a" -namespace = "spike" -version = "0.1.0" - -[dependencies] -spike.b = { path = "../b" } - -[targets.a] -kind = "lib" -EOF -printf 'int b_answer();\nint a_answer() { return b_answer() + 1; }\n' > "$TMP/a/src/a.cpp" -# The dependency's program: graph_file() is not offered to it. -cat > "$TMP/a/build.mcpp" <<'EOF' -#include -import mcpp; -int main() { - const char* g = mcpp::graph_file(); - if (g != nullptr && g[0] != '\0') { - std::fprintf(stderr, "a: a dependency's program was offered graph_file()=%s\n", g); - return 1; - } - return 0; -} -EOF - -cat > "$TMP/app/mcpp.toml" <<'EOF' -[package] -name = "app" -version = "0.1.0" - -[dependencies] -spike.a = { path = "../a" } -EOF -printf 'int a_answer();\nint main() { return a_answer() == 41 ? 0 : 1; }\n' > "$TMP/app/src/main.cpp" -# The root's program copies the document it was given, one copy per run. -cat > "$TMP/app/build.mcpp" <<'EOF' -#include -#include -#include -#include -import mcpp; -int main() { - const char* g = mcpp::graph_file(); - if (g == nullptr || g[0] == '\0') { - std::fprintf(stderr, "app: graph_file() is empty\n"); - return 1; - } - std::ifstream in(g, std::ios::binary); - std::stringstream ss; - ss << in.rdbuf(); - const std::string dir = mcpp::manifest_dir(); - int n = 0; - for (;; ++n) { - std::ifstream probe(dir + "/graph-" + std::to_string(n) + ".json"); - if (!probe) break; - } - std::ofstream(dir + "/graph-" + std::to_string(n) + ".json", std::ios::binary) << ss.str(); - return 0; -} -EOF - -cd "$TMP/app" -runs() { ls graph-*.json 2>/dev/null | wc -l | tr -d ' '; } - -# ── A, E ──────────────────────────────────────────────────────────────────── -"$MCPP" build > a.log 2>&1 || fail "A: build failed" a.log -[ "$(runs)" -eq 1 ] || fail "A: the root program ran $(runs) times" a.log -python3 - "$TMP" graph-0.json res <<'EOF' || fail "A: the graph document" graph-0.json -import json, os, sys -tmp, path, want = sys.argv[1], sys.argv[2], sys.argv[3] -d = json.load(open(path)) -names = [p["package"]["canonical"] for p in d["packages"]] -assert names == ["spike.b@0.2.0", "spike.a@0.1.0", "mcpplibs.app@0.1.0"], names -b, a, app = d["packages"] -assert app["root"] is True and b["root"] is False, (app["root"], b["root"]) -assert os.path.isabs(b["manifest_dir"]), b["manifest_dir"] -assert os.path.realpath(b["manifest_dir"]) == os.path.realpath(os.path.join(tmp, "b")), b["manifest_dir"] -assert b["targets"] == [{"name": "b", "kind": "lib"}], b["targets"] -assert b["metadata"] == {"demo": {"resources": want, "languages": ["en", "zh"]}}, b["metadata"] -assert a["metadata"] == {}, a["metadata"] -assert [r["requester"] for r in b["requested_by"]] == ["spike.a@0.1.0"], b["requested_by"] -assert isinstance(b["features"], list) -EOF -echo "ok: A, the document lists the graph in dependency order with metadata" -echo "ok: E, a dependency's program is not offered the document" - -# ── B ─────────────────────────────────────────────────────────────────────── -"$MCPP" build > b.log 2>&1 || fail "B: build failed" b.log -[ "$(runs)" -eq 1 ] || fail "B: an unchanged build re-ran the root program" b.log -echo "ok: B, an unchanged build does not re-run the root program" - -# ── C ─────────────────────────────────────────────────────────────────────── -write_b assets -"$MCPP" build > c.log 2>&1 || fail "C: build failed" c.log -[ "$(runs)" -eq 2 ] || fail "C: editing a dependency's metadata did not re-run the root program" c.log -python3 - "$TMP" graph-1.json assets <<'EOF' || fail "C: the document after the edit" graph-1.json -import json, sys -d = json.load(open(sys.argv[2])) -b = d["packages"][0] -assert b["metadata"]["demo"]["resources"] == sys.argv[3], b["metadata"] -EOF -echo "ok: C, a metadata edit re-runs the root program with the new value" - -# ── D ─────────────────────────────────────────────────────────────────────── -printf 'int b_answer() { return 39 + 1; }\n' > "$TMP/b/src/b.cpp" -"$MCPP" build > d.log 2>&1 || fail "D: build failed" d.log -[ "$(runs)" -eq 2 ] || fail "D: editing a dependency's source re-ran the root program" d.log -echo "ok: D, a source edit does not re-run the root program" - -# ── F ─────────────────────────────────────────────────────────────────────── -cat >> "$TMP/app/mcpp.toml" <<'EOF' -EOF -python3 - "$TMP/app/mcpp.toml" <<'EOF' -import sys -p = sys.argv[1] -s = open(p).read().replace('version = "0.1.0"\n', 'version = "0.1.0"\nlicence = "MIT"\n', 1) -open(p, "w").write(s) -EOF -"$MCPP" build > f.log 2>&1 || fail "F: build failed on a warning" f.log -grep -q "\[package\] has unsupported key 'licence'" f.log || fail "F: no warning for an unknown [package] key" f.log -if "$MCPP" build --strict > f2.log 2>&1; then fail "F: --strict accepted an unknown [package] key" f2.log; fi -grep -q "\[package\] has unsupported key 'licence'" f2.log || fail "F: --strict failed for another reason" f2.log -grep -q "unsupported key 'metadata'" a.log c.log f.log && fail "F: metadata reported as unknown" a.log -echo "ok: F, [package] reports an unknown key and knows metadata" - -echo "PASS: 720" diff --git a/tests/e2e/775_a_host_tool_merges_its_conditional_sections_once.sh b/tests/e2e/775_a_host_tool_merges_its_conditional_sections_once.sh deleted file mode 100755 index f27408df0..000000000 --- a/tests/e2e/775_a_host_tool_merges_its_conditional_sections_once.sh +++ /dev/null @@ -1,88 +0,0 @@ -#!/usr/bin/env bash -# 775 -- a dependency compiled as a host tool receives each entry of its -# matching `[target..build]` sections once (#690, design record F12). -# -# The resolver merges a dependency's conditional sections for the consumer's -# target. A host-tool sub-build used to receive that merged manifest and merge -# it again for the host, so an entry of a section matching both reached the -# tool twice. The section here adds `-include once.h`, and `once.h` defines a -# variable without an include guard: included twice, the tool does not compile. -# The package builds on its own, which is the control. -set -e - -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -export MCPP_HOME="$TMP/home" -source "$(dirname "$0")/_inherit_toolchain.sh" -fail() { echo "FAIL: $1"; [ -n "${2:-}" ] && cat "$2"; exit 1; } - -mkdir -p "$TMP/toolpkg/src" "$TMP/toolpkg/inc" "$TMP/app/src" -echo 'int once_counter = 1;' > "$TMP/toolpkg/inc/once.h" -cat > "$TMP/toolpkg/mcpp.toml" <<'EOF' -[package] -name = "toolpkg" -version = "0.1.0" - -[targets.gen] -kind = "bin" -main = "src/gen.cpp" - -[build] -include_dirs = ["inc"] - -[target.'cfg(not(os = "none"))'.build] -cxxflags = ["-include once.h"] -EOF -cat > "$TMP/toolpkg/src/gen.cpp" <<'EOF' -#include -int main(int argc, char** argv) { - if (argc < 2) return 2; - std::FILE* f = std::fopen(argv[1], "w"); - if (!f) return 3; - std::fprintf(f, "int generated_answer() { return %d; }\n", 40 + once_counter); - std::fclose(f); - return 0; -} -EOF -cat > "$TMP/app/mcpp.toml" <<'EOF' -[package] -name = "app" -version = "0.1.0" - -[dependencies] -toolpkg = { path = "../toolpkg", tools = ["gen"] } -EOF -cat > "$TMP/app/src/main.cpp" <<'EOF' -#include -int generated_answer(); -int main() { std::printf("ANSWER=%d\n", generated_answer()); } -EOF -cat > "$TMP/app/build.mcpp" <<'EOF' -import std; -import mcpp; -int main() { - const char* tool = mcpp::dep_bin("toolpkg", "gen"); - if (!tool || !*tool) return 1; - std::string out = std::string(mcpp::out_dir()) + "/gen.cpp"; - std::string cmd = std::string("\"") + tool + "\" \"" + out + "\""; -#ifdef _WIN32 - // cmd.exe removes the first and the last quote of a command that starts - // with one; the outer pair keeps both inner pairs intact. - cmd = "\"" + cmd + "\""; -#endif - if (std::system(cmd.c_str()) != 0) return 1; - mcpp::generated(out.c_str()); -} -EOF - -( cd "$TMP/toolpkg" && "$MCPP" build > "$TMP/direct.log" 2>&1 ) \ - || fail "control: the tool package does not build on its own" "$TMP/direct.log" -cd "$TMP/app" -"$MCPP" build > build.log 2>&1 || { - grep -q "redefinition" build.log \ - && fail "the host tool received a conditional entry twice" build.log - fail "the consumer did not build" build.log -} -"$MCPP" run > run.log 2>&1 || fail "the program did not run" run.log -grep -q '^ANSWER=41' run.log || fail "wrong answer" run.log -echo "PASS: 775_a_host_tool_merges_its_conditional_sections_once" diff --git a/tests/e2e/779_a_build_program_declares_a_runtime_search_dir.sh b/tests/e2e/779_a_build_program_declares_a_runtime_search_dir.sh deleted file mode 100755 index 957baf696..000000000 --- a/tests/e2e/779_a_build_program_declares_a_runtime_search_dir.sh +++ /dev/null @@ -1,218 +0,0 @@ -#!/usr/bin/env bash -# requires: pack gcc -# 779_a_build_program_declares_a_runtime_search_dir.sh -- -# `mcpp::runtime_search_dir(dir)`: the build-program form of -# `runtime_search_dirs` (docs/04 §2.11). A dependency that brings a prebuilt -# shared library (a vcpkg prefix's `bin/`, a Qt SDK's `bin/`) knows where it -# lives only at build-program time, and the manifest key cannot be computed -- -# it is a fixed TOML array. This is the directive that closes that gap -# (design §5.4 R1', mcpp#701/#702). -# -# What this holds, each with the wrong answer it excludes: -# -# 1. THE DIRECTIVE GETS THE SAME RUNPATH TREATMENT THE MANIFEST KEY DOES: -# `-Wl,-rpath,` in build.ninja, never `-L` -- a launch-time search -# directory is not a link-library search path (docs/04 §2.11's own -# table), read from the emitted graph rather than inferred from a green -# build, the same way 62_runtime_library_dirs.sh checks the manifest key. -# 2. `mcpp run` FINDS THE LIBRARY ONLY THROUGH THAT RUNPATH: the executable -# is linked against it (DT_NEEDED), and the directory is nowhere else on -# any search path this build would otherwise consult. -# 3. THE CACHE TAG IS NON-EMPTY, SO A REPLAY CARRIES THE ENTRY: rebuilding -# on a build.mcpp CACHE HIT (not a re-run) must keep the RUNPATH and keep -# `mcpp run` working -- the same replay criterion `mcpp:deploy=` is held -# to in 651_a_build_program_deploys_what_it_generated.sh. -# 4. `mcpp pack --pkg-format dir` STAGES IT into the bundle's `lib/` -- the -# same closure search `runtime_search_dirs` already feeds. Checked -# LAST: `mcpp pack` builds under a different profile and would otherwise -# invalidate the `dev`-profile build.mcpp cache the replay check (3) -# depends on. -# 5. A DEPENDENCY'S DECLARATION REACHES THE CONSUMER'S EXECUTABLE: the -# directive is `Scope::LinkGlobal`, folded into `LinkIntent -# .runtimeSearchDirs` by the SAME per-package merge (`resolve_runtime_ -# contract`, plan.cppm) the manifest key goes through, so a path -# dependency that calls `runtime_search_dir` in its OWN build.mcpp makes -# its consumer's binary run without the consumer naming the directory at -# all. -# -# It lands DIRECTLY on `LinkIntent::runtimeSearchDirs`, not the retiring -# `RuntimeConfig::libraryDirs` (docs/04 §2.11) -- #702 as first written joined -# the wrong field (design §5.3 point 1); nothing here would tell them apart -# if it had not been fixed, since every reader below is fed FROM -# `runtimeSearchDirs` either way once the manifest key itself is also routed -# there. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -export MCPP_HOME=${MCPP_HOME:-$HOME/.mcpp} - -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } - -cd "$TMP" -mkdir -p app/src app/rtlib - -# The prebuilt-plugin shape a real build.mcpp would locate (a vcpkg/Qt-style -# `bin/`): compiled directly with the toolchain here, standing in for that -# discovery the same way 62_runtime_library_dirs.sh's precompiled .so stands -# in for the manifest key's case. -cat > app/rtlib/plugin.c <<'EOF' -int runtime_plugin_answer(void) { return 42; } -EOF -gcc -shared -fPIC app/rtlib/plugin.c -o app/rtlib/libruntime_plugin.so - -cat > app/src/main.cpp <<'EOF' -extern "C" int runtime_plugin_answer(); -int main() { return runtime_plugin_answer() == 42 ? 0 : 1; } -EOF - -cat > app/mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" - -[toolchain] -linux = "gcc@16.1.0" - -[targets.app] -kind = "bin" -main = "src/main.cpp" -EOF - -# The directive under test. `link_lib`/`link_search` also come from the -# program, on purpose: the point is a dependency the build.mcpp DISCOVERS, -# not one written into mcpp.toml by hand -- the case `runtime_search_dirs` -# cannot cover. -cat > app/build.mcpp <<'EOF' -import mcpp; -#include -int main() { - const std::string root = mcpp::manifest_dir(); - mcpp::link_lib("runtime_plugin"); - mcpp::link_search((root + "/rtlib").c_str()); - mcpp::runtime_search_dir((root + "/rtlib").c_str()); - return 0; -} -EOF - -MCPP="${MCPP:-mcpp}" -cd app -find_graph() { find target -name build.ninja -not -path '*/.build-mcpp/*' | head -1; } - -# ── 1. a plain build gets the RUNPATH treatment, never -L ────────────────── -"$MCPP" build > b1.log 2>&1 || fail "build failed" b1.log -G=$(find_graph) -[ -n "$G" ] || fail "no build.ninja" b1.log -RTDIR=$(realpath rtlib) -grep -F -- "-Wl,-rpath,$RTDIR" "$G" >/dev/null \ - || fail "runtime-search-dir directive missing from RUNPATH intent" "$G" - -# ── 2. mcpp run finds the library only through that RUNPATH ──────────────── -"$MCPP" run > run1.log 2>&1 \ - || fail "run failed to find the plugin through the directive's RUNPATH" run1.log b1.log - -# ── 3. the replay criterion: a build.mcpp cache HIT keeps the directive ──── -# `mcpp pack` builds under a different profile and would invalidate this -# package's build.mcpp cache (a different ctx hash) before the replay is -# exercised, so the cache-hit rebuild happens BEFORE pack, on the same `dev` -# profile `mcpp build` and `mcpp run` already used. -touch src/main.cpp # past the whole-project no-op fast path, without - # touching build.mcpp itself -# A reused program is stated under --verbose only (build output design revision 3, §7.1). -"$MCPP" build -v > b2.log 2>&1 || fail "second build failed" b2.log -grep -qE "^ *build\.mcpp .* cached" b2.log \ - || fail "the second build re-ran build.mcpp; the replay path was not exercised" b2.log -G2=$(find_graph) -[ -n "$G2" ] || fail "no build.ninja after the second build" b2.log -grep -F -- "-Wl,-rpath,$RTDIR" "$G2" >/dev/null \ - || fail "the RUNPATH directive did not survive a build.mcpp cache hit" "$G2" -"$MCPP" run > run2.log 2>&1 \ - || fail "run failed after the directive was replayed from a build.mcpp cache hit" run2.log b2.log - -# ── 4. mcpp pack --pkg-format dir stages it into the bundle's lib/ ───────────── -"$MCPP" pack --pkg-format dir > pack1.log 2>&1 || fail "pack --pkg-format dir failed" pack1.log -STAGED=$(find target/dist -name 'libruntime_plugin.so' | head -1) -[ -n "$STAGED" ] || fail "the runtime-search-dir closure was not staged by mcpp pack" pack1.log -case "$STAGED" in - */lib/libruntime_plugin.so) : ;; - *) fail "the staged library is not under the packed bundle's lib/" pack1.log ;; -esac - -cd "$TMP" - -# ── 5. a dependency's declaration reaches the consumer's executable ──────── -# `libdep` is a path dependency whose OWN build.mcpp declares the plugin's -# directory; `consumer` names none of this -- no `link_search`, no -# `runtime_search_dir`, not even `link_lib` in its own manifest or program -- -# and still runs, because the directive is LinkGlobal and the merge that -# folds a dependency's LinkIntent into its consumer's is the SAME one the -# manifest key goes through (plan.cppm ~:911-948). -mkdir -p libdep/src libdep/plugin - -cat > libdep/plugin/dep_plugin.c <<'EOF' -int dep_plugin_answer(void) { return 7; } -EOF -gcc -shared -fPIC libdep/plugin/dep_plugin.c -o libdep/plugin/libdep_plugin.so - -cat > libdep/src/lib.cpp <<'EOF' -extern "C" int dep_plugin_answer(); -int libdep_touch() { return dep_plugin_answer(); } -EOF - -cat > libdep/mcpp.toml <<'EOF' -[package] -name = "libdep" -version = "0.1.0" - -[modules] -sources = ["src/**/*.cpp"] - -[targets.libdep] -kind = "lib" -EOF - -cat > libdep/build.mcpp <<'EOF' -import mcpp; -#include -int main() { - const std::string root = mcpp::manifest_dir(); - mcpp::link_lib("dep_plugin"); - mcpp::link_search((root + "/plugin").c_str()); - mcpp::runtime_search_dir((root + "/plugin").c_str()); - return 0; -} -EOF - -mkdir -p consumer/src -cat > consumer/src/main.cpp <<'EOF' -extern "C" int dep_plugin_answer(); -int main() { return dep_plugin_answer() == 7 ? 0 : 1; } -EOF - -cat > consumer/mcpp.toml <<'EOF' -[package] -name = "consumer" -version = "0.1.0" - -[toolchain] -linux = "gcc@16.1.0" - -[dependencies] -libdep = { path = "../libdep" } - -[targets.consumer] -kind = "bin" -main = "src/main.cpp" -EOF - -cd consumer -DEPDIR=$(realpath ../libdep/plugin) -"$MCPP" build > b3.log 2>&1 || fail "consumer build failed" b3.log -G3=$(find_graph) -[ -n "$G3" ] || fail "no build.ninja for consumer" b3.log -grep -F -- "-Wl,-rpath,$DEPDIR" "$G3" >/dev/null \ - || fail "the dependency's runtime-search-dir did not reach the consumer's link" "$G3" -"$MCPP" run > run3.log 2>&1 \ - || fail "consumer run failed to find the dependency's plugin" run3.log b3.log - -echo "PASS: 779_a_build_program_declares_a_runtime_search_dir" diff --git a/tests/e2e/800_a_feature_provides_its_host_tools.sh b/tests/e2e/800_a_feature_provides_its_host_tools.sh deleted file mode 100755 index 3b8d9bd0e..000000000 --- a/tests/e2e/800_a_feature_provides_its_host_tools.sh +++ /dev/null @@ -1,131 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 800_a_feature_provides_its_host_tools.sh — mcpp#709. -# -# `[features.] tools = [""]` states that enabling `f` needs the -# package's own program `` on the build machine. A consumer that enables -# the feature receives the tool exactly as if its dependency edge had written -# `tools = [...]` (e2e 187): built once for the host, reachable through -# `mcpp::dep_bin()`. Before this, every consumer of a rule package had to name -# the rule's tools on its edge as well as the feature. -# -# Criteria: -# 1. a consumer that writes only `features = ["codegen"]` gets the tool, and -# the source it generates is compiled and linked; -# 2. a consumer that does not enable the feature gets nothing built; -# 3. a `tools` entry that names no bin target of the package is refused at -# load, naming the package's bin targets. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -export MCPP_HOME="$TMP/mcpphome" -mkdir -p "$MCPP_HOME" -if [ -d "$HOME/.mcpp/registry" ]; then - ln -s "$HOME/.mcpp/registry" "$MCPP_HOME/registry" -fi - -# ── the tool package ──────────────────────────────────────────────────────── -mkdir -p toolpkg/src -cat > toolpkg/mcpp.toml <<'EOF' -[package] -name = "toolpkg" -version = "0.1.0" - -[build] -sources = ["src/lib.cpp"] - -[features.codegen] -tools = ["codegen"] - -[targets.codegen] -kind = "bin" -main = "src/codegen.cpp" - -[targets.toolpkg] -kind = "lib" -EOF -echo 'int toolpkg_lib() { return 1; }' > toolpkg/src/lib.cpp -cat > toolpkg/src/codegen.cpp <<'EOF' -#include -int main(int argc, char** argv) { - if (argc < 2) return 2; - FILE* f = std::fopen(argv[1], "w"); - if (!f) return 3; - std::fprintf(f, "int generated_answer() { return 42; }\n"); - std::fclose(f); - return 0; -} -EOF - -# ── 1. the consumer names the feature, not the tool ───────────────────────── -mkdir -p app/src -cat > app/mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" - -[dependencies] -toolpkg = { path = "../toolpkg", features = ["codegen"] } -EOF -cat > app/src/main.cpp <<'EOF' -#include -int generated_answer(); -int main() { std::printf("ANSWER=%d\n", generated_answer()); } -EOF -cat > app/build.mcpp <<'EOF' -#include -#include -#include -import mcpp; -int main() { - const char* tool = mcpp::dep_bin("toolpkg", "codegen"); - if (!tool || !*tool) { std::fprintf(stderr, "no tool path\n"); return 1; } - std::string out = std::string(mcpp::out_dir()) + "/gen.cpp"; - std::string cmd = std::string("\"") + tool + "\" \"" + out + "\""; - if (std::system(cmd.c_str()) != 0) { std::fprintf(stderr, "tool failed\n"); return 1; } - mcpp::generated(out.c_str()); -} -EOF -cd app -"$MCPP" build > b1.log 2>&1 || { cat b1.log; echo "FAIL: 1: build failed"; exit 1; } -grep -q "host tool toolpkg:codegen" b1.log || { - cat b1.log; echo "FAIL: 1: the feature's tool was not built"; exit 1; } -out="$("$MCPP" run 2>&1 | grep '^ANSWER=' | tail -1)" -[[ "$out" == "ANSWER=42" ]] || { echo "FAIL: 1: generated source not linked: $out"; exit 1; } -echo "ok: 1" - -# ── 2. without the feature, nothing is built ──────────────────────────────── -cd "$TMP" -mkdir -p plain/src -cat > plain/mcpp.toml <<'EOF' -[package] -name = "plain" -version = "0.1.0" - -[dependencies] -toolpkg = { path = "../toolpkg" } -EOF -echo 'int main() {}' > plain/src/main.cpp -cd plain -"$MCPP" build > b2.log 2>&1 || { cat b2.log; echo "FAIL: 2: build failed"; exit 1; } -if grep -q "host tool" b2.log; then - cat b2.log; echo "FAIL: 2: a tool was built for a consumer that did not enable the feature"; exit 1 -fi -echo "ok: 2" - -# ── 3. a tools entry that names no bin target is refused ──────────────────── -cd "$TMP" -sed 's/tools = \["codegen"\]/tools = ["nosuch"]/' toolpkg/mcpp.toml > toolpkg/mcpp.toml.new -mv toolpkg/mcpp.toml.new toolpkg/mcpp.toml -cd app && rm -rf target -if "$MCPP" build > b3.log 2>&1; then - cat b3.log; echo "FAIL: 3: an unknown tool name was accepted"; exit 1 -fi -grep -q "nosuch" b3.log && grep -q "codegen" b3.log || { - cat b3.log; echo "FAIL: 3: the refusal does not name the entry and the bin targets"; exit 1; } -echo "ok: 3" - -echo "PASS: 800_a_feature_provides_its_host_tools" diff --git a/tests/e2e/802_a_host_build_applies_its_host_row.sh b/tests/e2e/802_a_host_build_applies_its_host_row.sh deleted file mode 100755 index 4b3fc1e41..000000000 --- a/tests/e2e/802_a_host_build_applies_its_host_row.sh +++ /dev/null @@ -1,89 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc elf -# 802_a_host_build_applies_its_host_row.sh — mcpp#704. -# -# A build without `--target` targets the host, and `[target.]` -# describes that target as it describes any other. The row used to be read -# only when a target was named, so its `cxx_runtime` had no effect on a plain -# `mcpp build`, while `--target ` honoured it. A program linking Qt's -# libraries (which require libstdc++.so.6 and carry RUNPATH $ORIGIN) then -# embedded its own libstdc++ and failed at start. -# -# Criteria: -# 1. without the row, a plain build is self-contained (no NEEDED -# libstdc++.so.6), the engine default; -# 2. with `[target.] cxx_runtime = "toolchain-coupled"`, a plain build -# needs libstdc++.so.6, exactly as `--target ` does; -# 3. the row is found under another spelling of the host triple. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -host="$(uname -m)-linux-gnu" - -write_manifest() { # $1 = the row's selector, empty for no row - cat > mcpp.toml <> mcpp.toml - fi -} - -mkdir -p src -cat > src/main.cpp <<'EOF' -#include -#include -int main() { std::string s = "RT_OK"; std::printf("%s\n", s.c_str()); } -EOF - -needs_libstdcxx() { - local bin - bin="$(find target -path '*/bin/cxxrt' -type f | head -1)" - [[ -n "$bin" ]] || { echo "FAIL: no program built"; exit 1; } - readelf -d "$bin" | grep -q 'NEEDED.*libstdc++\.so' -} - -# ── 1 ── -write_manifest "" -"$MCPP" build > b1.log 2>&1 || { cat b1.log; echo "FAIL: 1: build failed"; exit 1; } -if needs_libstdcxx; then - echo "FAIL: 1: the default build is not self-contained"; exit 1 -fi -echo "ok: 1" - -# ── 2 ── -rm -rf target -write_manifest "$host" -"$MCPP" build > b2.log 2>&1 || { cat b2.log; echo "FAIL: 2: build failed"; exit 1; } -needs_libstdcxx || { - cat b2.log; echo "FAIL: 2: [target.$host] cxx_runtime was not applied to a host build"; exit 1; } -[[ "$("$MCPP" run 2>&1 | tail -1)" == "RT_OK" ]] || { echo "FAIL: 2: the program does not run"; exit 1; } -echo "ok: 2" - -# ── 3 ── -rm -rf target -write_manifest "$(uname -m)-unknown-linux-gnu" -"$MCPP" build > b3.log 2>&1 || { cat b3.log; echo "FAIL: 3: build failed"; exit 1; } -needs_libstdcxx || { - cat b3.log; echo "FAIL: 3: the row was not matched under another spelling"; exit 1; } -echo "ok: 3" - -echo "PASS: 802_a_host_build_applies_its_host_row" diff --git a/tests/e2e/823_mcpp_core_and_mcpp_name_one_interface.sh b/tests/e2e/823_mcpp_core_and_mcpp_name_one_interface.sh deleted file mode 100755 index 98f51bd52..000000000 --- a/tests/e2e/823_mcpp_core_and_mcpp_name_one_interface.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env bash -# requires: unix-shell -# 823_mcpp_core_and_mcpp_name_one_interface.sh -- #734 E8. -# -# The engine's build-program interface is named `mcpp.core`, the layer name the -# specification uses (SPEC-007), and `mcpp` is its permanent equivalent: the -# engine embeds `mcpp` and a second unit whose body is `export import mcpp;`. -# A build program may use either spelling, or both. -# -# C1 `import mcpp.core;` builds and the program's directive takes effect; -# C2 `import mcpp;` behaves the same; -# C3 a program importing both compiles and behaves the same. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } -MCPP="${MCPP:-mcpp}" - -leg() { # $1 = label, $2 = the import lines - rm -rf "$TMP/$1"; mkdir -p "$TMP/$1/src"; cd "$TMP/$1" - printf '[package]\nname = "core823"\nversion = "0.1.0"\n' > mcpp.toml - printf '#include \nint main() { std::printf("%%d\\n", CORE823); return 0; }\n' > src/main.cpp - printf '%s\nint main() {\n mcpp::cxxflag("-DCORE823=823");\n return 0;\n}\n' "$2" > build.mcpp - "$MCPP" build > build.log 2>&1 || fail "$1: the build failed" build.log build.mcpp - "$MCPP" run > run.log 2>&1 || fail "$1: the program did not run" run.log - grep -qx "823" run.log || fail "$1: the directive did not take effect" run.log -} - -leg C1 'import mcpp.core;' -leg C2 'import mcpp;' -leg C3 'import mcpp.core; -import mcpp;' - -echo "OK" diff --git a/tests/e2e/824_a_build_program_reads_the_resolved_toolchain.sh b/tests/e2e/824_a_build_program_reads_the_resolved_toolchain.sh deleted file mode 100755 index b2345901d..000000000 --- a/tests/e2e/824_a_build_program_reads_the_resolved_toolchain.sh +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env bash -# requires: unix-shell -# 824_a_build_program_reads_the_resolved_toolchain.sh -- #734 E2 (protocol 14). -# -# A build program reads the resolved toolchain as facts: the row's tools, the -# target ABI's native tools, the environment the engine runs them with, a -# path-free identity, the ninja mcpp runs, and the program's C++ runtime -# contract. A plugin driving a foreign build system translates them; mcpp -# interprets nothing. On the MSVC ABI the Windows rows of CI read the toolset's -# `cl`, `link` and SDK tools and a non-empty environment; on this row: -# -# B1 `tool("cxx")` is the resolved compiler and `tool("cc")` its C driver, -# both existing files; `tool("ld")` is the driver; -# B2 `abi_tool(role)` equals `tool(role)` off the MSVC ABI; -# B3 `tool_env()`, `msvc_instance_dir()` and `msvc_crt_linkage()` are empty; -# B4 `toolset_identity()` names the family and version, and no path; -# B5 `ninja_program()` runs; -# B6 `cxx_runtime()` follows the manifest: the default, then a stated -# `host-coupled`. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } -MCPP="${MCPP:-mcpp}" - -mkdir -p p/src -cd p -printf 'int main() { return 0; }\n' > src/main.cpp -cat > build.mcpp <<'EOF' -import std; -import mcpp.core; -int main() { - std::ofstream out(std::string(mcpp::out_dir()) + "/info.txt"); - for (const char* r : {"cc", "cxx", "ld", "ar", "rc", "as", "mt"}) - out << "tool." << r << "=" << mcpp::tool(r) << "\n" - << "abi." << r << "=" << mcpp::abi_tool(r) << "\n"; - out << "env=" << mcpp::tool_env() << "\n" - << "identity=" << mcpp::toolset_identity() << "\n" - << "instance=" << mcpp::msvc_instance_dir() << "\n" - << "ninja=" << mcpp::ninja_program() << "\n" - << "runtime=" << mcpp::cxx_runtime() << "\n" - << "crt=" << mcpp::msvc_crt_linkage() << "\n"; - return 0; -} -EOF -printf '[package]\nname = "info824"\nversion = "0.1.0"\n' > mcpp.toml -"$MCPP" build > b1.log 2>&1 || fail "the build failed" b1.log -INFO=$(find target -name info.txt | head -1) -test -s "$INFO" || fail "the build program wrote no info.txt" b1.log -val() { grep "^$1=" "$INFO" | head -1 | cut -d= -f2-; } - -# B1 -for r in cxx cc; do - [ -x "$(val tool.$r)" ] || fail "B1: tool($r) is not an executable file" "$INFO" -done -[ "$(val tool.ld)" = "$(val tool.cxx)" ] || fail "B1: tool(ld) is not the driver" "$INFO" -# B2 -for r in cc cxx ld ar rc as mt; do - [ "$(val abi.$r)" = "$(val tool.$r)" ] || fail "B2: abi_tool($r) differs from tool($r) off the MSVC ABI" "$INFO" -done -# B3 -for k in env instance crt; do - [ -z "$(val $k)" ] || fail "B3: $k is not empty off the MSVC ABI" "$INFO" -done -# B4 -id=$(val identity) -case "$id" in clang\ [0-9]*|gcc\ [0-9]*) ;; *) fail "B4: identity '$id' is not ' '" "$INFO" ;; esac -case "$id" in */*) fail "B4: identity '$id' holds a path" "$INFO" ;; esac -# B5 -"$(val ninja)" --version > /dev/null 2>&1 || fail "B5: ninja_program() does not run" "$INFO" -# B6 -[ "$(val runtime)" = "self-contained" ] || fail "B6: the default contract is '$(val runtime)'" "$INFO" -printf '[package]\nname = "info824"\nversion = "0.1.0"\n\n[build]\ncxx_runtime = "host-coupled"\n' > mcpp.toml -"$MCPP" build > b2.log 2>&1 || fail "the second build failed" b2.log -INFO=$(find target -name info.txt -newer b1.log | head -1) -[ -n "$INFO" ] || INFO=$(find target -name info.txt | head -1) -[ "$(val runtime)" = "host-coupled" ] || fail "B6: the stated contract is '$(val runtime)'" "$INFO" - -echo "OK" diff --git a/tests/e2e/826_plugin_diagnostics_features_and_names.sh b/tests/e2e/826_plugin_diagnostics_features_and_names.sh deleted file mode 100755 index 87e663925..000000000 --- a/tests/e2e/826_plugin_diagnostics_features_and_names.sh +++ /dev/null @@ -1,91 +0,0 @@ -#!/usr/bin/env bash -# requires: unix-shell -# 826_plugin_diagnostics_features_and_names.sh -- #734 E11, E7 and E10. -# -# A third-party build plugin, `acme.gen`, offers `mcpp.acme.gen` from its lib -# root and two modules behind features. A consumer's build program uses it. -# -# D1 a structured diagnostic is rendered in the engine's form, with its -# impact and hint lines (E11); -# D2 after a source changes, the next build is a cache hit that does not -# run the program, and it reports the diagnostic again (E11); -# N1 importing a module that sits behind a feature the edge did not enable -# fails naming the package and the feature (E7); -# R1 `mcpp.acme.*` from namespace `acme` draws no naming warning (E10); -# R2 `mcpp.rules.*` from namespace `acme` is warned about, and the warning -# names the accepted form `mcpp.acme.*` (E10). -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } -MCPP="${MCPP:-mcpp}" - -mkdir -p gen/src app/src -cat > gen/mcpp.toml <<'EOF' -[package] -namespace = "acme" -name = "gen" -version = "0.1.0" - -[lib] -path = "src/gen.cppm" - -[features.extra] -sources = ["src/extra.cppm"] - -[features.bad] -sources = ["src/bad.cppm"] -EOF -cat > gen/src/gen.cppm <<'EOF' -export module mcpp.acme.gen; -import mcpp.core; -export namespace acme { -inline void tell() { - mcpp::report({.severity = "warning", - .message = "the generator found no schema", - .impact = "no bindings are generated", - .hint = "add schema/*.proto"}); -} -} -EOF -printf 'export module mcpp.acme.gen.extra;\nexport namespace acme { inline int extra() { return 1; } }\n' > gen/src/extra.cppm -printf 'export module mcpp.rules.bad;\nexport namespace acme { inline int bad() { return 2; } }\n' > gen/src/bad.cppm - -cd app -printf 'int main() { return 0; }\n' > src/main.cpp -edge() { printf '[package]\nname = "app826"\nversion = "0.1.0"\n\n[build-dependencies]\n"acme.gen" = { path = "../gen", host-module = true%s }\n' "$1" > mcpp.toml; } - -# D1, R1 -edge '' -printf 'import mcpp.core;\nimport mcpp.acme.gen;\nint main() { acme::tell(); return 0; }\n' > build.mcpp -"$MCPP" build > d1.log 2>&1 || fail "D1: the build failed" d1.log -grep -q "the generator found no schema" d1.log || fail "D1: the message is missing" d1.log -grep -q "impact: no bindings are generated" d1.log || fail "D1: the impact line is missing" d1.log -grep -q "help: add schema/\*.proto" d1.log || fail "D1: the help line is missing" d1.log -grep -q "mcpp.acme.gen'.*mcpp\.\|claims an origin" d1.log && fail "R1: an own-namespace module was warned about" d1.log - -# D2. `touch` first, as e2e 139 does: an unmodified build takes the project -# fast path and reaches no build program, so it would measure neither path. -touch src/main.cpp -# A reused program is stated under --verbose only (build output design revision 3, §7.1). -"$MCPP" build -v > d2.log 2>&1 || fail "D2: the second build failed" d2.log -grep -qE "^ *build\.mcpp .* cached" d2.log || fail "D2: the second build ran the program; the replay is not measured" d2.log -grep -q "impact: no bindings are generated" d2.log || fail "D2: the cached run did not report the diagnostic" d2.log - -# N1 -printf 'import mcpp.core;\nimport mcpp.acme.gen.extra;\nint main() { return acme::extra() == 1 ? 0 : 1; }\n' > build.mcpp -if "$MCPP" build > n1.log 2>&1; then fail "N1: a module behind a disabled feature was importable" n1.log; fi -grep -q "provided by: acme.gen, feature \"extra\" (not enabled)" n1.log || fail "N1: the error does not name the feature" n1.log -edge ', features = ["extra"]' -"$MCPP" build > n1b.log 2>&1 || fail "N1: enabling the named feature did not fix the build" n1b.log - -# R2 -edge ', features = ["bad"]' -printf 'import mcpp.core;\nimport mcpp.rules.bad;\nint main() { return acme::bad() == 2 ? 0 : 1; }\n' > build.mcpp -"$MCPP" build > r2.log 2>&1 || fail "R2: the build failed" r2.log -grep -q "uses 'mcpp.rules'" r2.log || fail "R2: a reserved segment was not warned about" r2.log -grep -q "'mcpp.acme.\*'" r2.log || fail "R2: the warning does not name the accepted form" r2.log - -echo "OK" diff --git a/tests/e2e/831_a_path_dependency_host_module_outside_src.sh b/tests/e2e/831_a_path_dependency_host_module_outside_src.sh deleted file mode 100755 index 6672623c6..000000000 --- a/tests/e2e/831_a_path_dependency_host_module_outside_src.sh +++ /dev/null @@ -1,86 +0,0 @@ -#!/usr/bin/env bash -# requires: unix-shell -# 831_a_path_dependency_host_module_outside_src.sh -- #734. -# -# A path dependency's host module is compiled into the consumer's build -# program, which no edge of the consumer's build.ninja names. The project fast -# path therefore has to see an edit to it itself. It swept only the -# dependency's `src/`, so a feature unit elsewhere -- mcpp-plugins keeps its -# members in `deps/`, `rules/`, `dist/` and `tools/` -- was replayed as "no -# work" after an edit. -# -# S1 an edit to `rules/hm.cppm` of the dependency re-runs the build program, -# which reads the new value; -# S2 a newer file inside a package nested in the dependency (a test fixture -# with its own mcpp.toml) does not decline the fast path. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } -MCPP="${MCPP:-mcpp}" - -mkdir -p plug/src plug/rules plug/tests/fixture/src app/src -cat > plug/mcpp.toml <<'EOF' -[package] -namespace = "probe" -name = "plug831" -version = "0.1.0" - -[build] -sources = ["src/plug831.cppm"] - -[features.hm] -sources = ["rules/hm.cppm"] -EOF -printf 'export module probe.plug831;\nexport int plug_root() { return 0; }\n' > plug/src/plug831.cppm -printf 'export module probe.hm831;\nexport inline int value() { return 1; }\n' > plug/rules/hm.cppm -printf '[package]\nname = "fixture831"\nversion = "0.1.0"\n' > plug/tests/fixture/mcpp.toml -printf 'int main() { return 0; }\n' > plug/tests/fixture/src/main.cpp - -cat > app/mcpp.toml <<'EOF' -[package] -name = "app831" -version = "0.1.0" - -[build-dependencies.probe] -plug831 = { path = "../plug", features = ["hm"], host-module = true } -EOF -cat > app/build.mcpp <<'EOF' -import std; -import mcpp; -import probe.hm831; -int main() { - if (value() != 1) { std::println("host module value {}", value()); return 1; } - return 0; -} -EOF -printf 'int main() { return 0; }\n' > app/src/main.cpp -cd app - -"$MCPP" build > b1.log 2>&1 || fail "the first build failed" b1.log -"$MCPP" build -v > b1b.log 2>&1 || fail "the second build failed" b1b.log - -# S1 -sleep 1.1 -printf 'export module probe.hm831;\nexport inline int value() { return 2; }\n' > ../plug/rules/hm.cppm -if "$MCPP" build > s1.log 2>&1; then - fail "S1: the build after an edit to the dependency's rules/hm.cppm succeeded without re-running the build program" s1.log -fi -grep -q "host module value 2" s1.log || fail "S1: the build program did not read the edited host module" s1.log - -# S2 -printf 'export module probe.hm831;\nexport inline int value() { return 1; }\n' > ../plug/rules/hm.cppm -"$MCPP" build > s2a.log 2>&1 || fail "S2: the build after restoring the host module failed" s2a.log -sleep 1.1 -touch ../plug/tests/fixture/src/main.cpp -"$MCPP" build -v > s2.log 2>&1 || fail "S2: the build failed" s2.log -if grep -q "declined" s2.log; then - fail "S2: the fast path declined after a file of a package nested in the dependency changed" s2.log -fi -if grep -q "Resolving toolchain" s2.log; then - fail "S2: the build was planned instead of replayed" s2.log -fi - -echo "OK" diff --git a/tests/e2e/859_a_host_module_entry_is_reused_while_its_inputs_agree.sh b/tests/e2e/859_a_host_module_entry_is_reused_while_its_inputs_agree.sh deleted file mode 100755 index f6338889a..000000000 --- a/tests/e2e/859_a_host_module_entry_is_reused_while_its_inputs_agree.sh +++ /dev/null @@ -1,128 +0,0 @@ -#!/usr/bin/env bash -# requires: -# 859 -- a build program's library is compiled again exactly when what it is -# compiled from changed, and the program never runs on a stale compile. -# -# #748 (B1), mcpp#797 (R6). A library of a path dependency can change without -# its name and version changing, so a program's identity holds the content -# digest of that package's tree: an included header beside the interface is -# part of what was compiled. Hashing the interface file alone left an edited -# header unseen, and the program was neither compiled nor run again (R6). -# -# Criteria: -# A. A second run of a stale program (its declared input changed, its -# graph did not) compiles nothing. -# B. An edit to the interface compiles the library and the program again. -# C. An edit to a header the interface includes, and not to the interface -# itself, does too, and the program reads the new value. -# D. A record that disagrees with the identity the program would have -# (here, edited in place) is a miss: the program is compiled again and -# the record replaced. -set -e -source "$(dirname "$0")/_host_path.sh" - -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } -MCPP="${MCPP:-mcpp}" - -export MCPP_HOME="$TMP/mcpp-home" -source "$(dirname "$0")/_inherit_toolchain.sh" - -mkdir -p "$TMP/app/src" "$TMP/rules/src" -cd "$TMP/app" - -cat > "$TMP/rules/mcpp.toml" <<'EOF' -[package] -name = "rule859" -version = "0.1.0" - -[targets.rule859] -kind = "lib" -EOF -printf '#define T859_VALUE 1\n' > "$TMP/rules/src/value.h" -cat > "$TMP/rules/src/rule859.cppm" <<'EOF' -module; -#include "value.h" -export module t859.rules; -import mcpp; -export int rule_value() { return T859_VALUE; } -EOF -cat > mcpp.toml <<'EOF' -[package] -name = "app859" -version = "0.1.0" - -[build-dependencies] -rule859 = { path = "../rules", host-module = true } - -[targets.app859] -kind = "bin" -main = "src/main.cpp" -EOF -printf 'int main() { return 0; }\n' > src/main.cpp -cat > build.mcpp <<'EOF' -import mcpp; -import t859.rules; -int main() { - mcpp::rerun_if_changed("marker.txt"); - // Said on every run, so the log states which value the BMI carried. - mcpp::warning(rule_value() == 1 ? "T859 value one" - : rule_value() == 2 ? "T859 value two" - : "T859 value other"); - return 0; -} -EOF -echo 0 > marker.txt - -HOST=target/.build-mcpp/host -lib_object() { find $HOST -name 'rule859.m.o' | head -1; } -stamp() { stat -c %Y.%s "$(lib_object)" 2>/dev/null || stat -f %m.%z "$(lib_object)"; } -compiled() { grep -cE "build-program: app859: identity [0-9a-f]+ -> [0-9a-f]+, compiled" "$1" || true; } -# A stale program: its declared input changed, the sources of its imports did not. -rerun() { echo "$1" > marker.txt; MCPP_VERBOSE=1 "$MCPP" build > "$2" 2>&1 || fail "the build failed ($2)" "$2"; } - -MCPP_VERBOSE=1 "$MCPP" build > first.log 2>&1 || fail "the first build failed" first.log -[ -n "$(lib_object)" ] || fail "the first build compiled no library object" first.log -grep -q "T859 value one" first.log || fail "the program did not read the library's value" first.log -s0=$(stamp) - -# A -sleep 1 -rerun 1 a.log -[ "$(compiled a.log)" = 0 ] || fail "A: a stale program was compiled again" a.log -[ "$(stamp)" = "$s0" ] || fail "A: the library was compiled again" a.log -grep -q "T859 value one" a.log || fail "A: the program gave another value" a.log - -# B -sleep 1 -cat > "$TMP/rules/src/rule859.cppm" <<'EOF' -module; -#include "value.h" -export module t859.rules; -import mcpp; -export int rule_value() { return T859_VALUE + 0; } -EOF -rerun 2 b.log -[ "$(compiled b.log)" = 1 ] || fail "B: an edit to the interface did not compile the program again" b.log -[ "$(stamp)" != "$s0" ] || fail "B: an edit to the interface did not compile the library" b.log -s1=$(stamp) - -# C -sleep 1 -printf '#define T859_VALUE 2\n' > "$TMP/rules/src/value.h" -rerun 3 c.log -[ "$(compiled c.log)" = 1 ] || fail "C: an edit to an included header did not compile the program again" c.log -[ "$(stamp)" != "$s1" ] || fail "C: an edit to an included header did not compile the library" c.log -grep -q "T859 value two" c.log || fail "C: the program took the compile made before the header changed" c.log - -# D -rec=$(ls $HOST/*/programs.json 2>/dev/null | head -1) -[ -n "$rec" ] || fail "D: no build programs' record under $HOST" c.log -sed 's/"identity": "[0-9a-f]*"/"identity": "0000000000000000"/' "$rec" > "$rec.edited" && mv "$rec.edited" "$rec" -rerun 4 d.log -[ "$(compiled d.log)" = 1 ] || fail "D: a record that disagrees was used without compiling" d.log -grep -q '"identity": "0000000000000000"' "$rec" && fail "D: the record that disagreed was not replaced" "$rec" -grep -q "T859 value two" d.log || fail "D: the replaced compile gave another value" d.log - -echo "PASS: 859_a_host_module_entry_is_reused_while_its_inputs_agree" diff --git a/tests/e2e/873_payload_overrides.sh b/tests/e2e/873_payload_overrides.sh deleted file mode 100755 index 17940c5a8..000000000 --- a/tests/e2e/873_payload_overrides.sh +++ /dev/null @@ -1,125 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc unix-shell -# `[xlings.overrides]`, `MCPP_XLINGS_OVERRIDE__` and config.toml -# state where a declared payload comes from (mcpp#755): an overridden payload -# is not provisioned, the build program receives the override, and the build -# says so on a `Using` line and in its `Finished` line. -# -# THE ASSERTION IS ON WHAT mcpp ASKS FOR. The payload is a name no index -# carries, and every build runs with `MCPP_NO_AUTO_INSTALL=1`: a build that -# still wanted it is refused naming it, so "built" means "not asked for". The -# baseline is the control: the same project without an override is refused. -set -e - -MCPP="${MCPP:-mcpp}" -work="$(mktemp -d)" -# A path written INTO a manifest goes through host_path (see _host_path.sh). -source "$(dirname "$0")/_host_path.sh" -work_HOST="$(host_path "$work")" -trap 'rm -rf "$work"' EXIT -export NO_COLOR=1 - -# A plugin that declares the payload behind a feature, the way a rule package -# declares its tool, and a consumer whose build program reads it. -mkdir -p "$work/plug/src" "$work/plug/members" "$work/app/src" -cat > "$work/plug/mcpp.toml" <<'TOML' -[package] -name = "plug" -namespace = "e2e" -version = "0.1.0" - -[build] -sources = ["src/plug.cppm"] - -[features.tools-x] -sources = ["members/x.cppm"] - -[feature-xlings.tools-x] -"xim:mcpp-e2e-absent-tool" = ">=2.0" - -[targets.plug] -kind = "lib" -TOML -echo 'export module e2e.plug;' > "$work/plug/src/plug.cppm" -cat > "$work/plug/members/x.cppm" <<'CPP' -export module e2e.tools.x; -export namespace e2e::x { inline int v = 1; } -CPP -cat > "$work/app/mcpp.toml" < "$work/app/build.mcpp" <<'CPP' -import std; -import mcpp; -import e2e.tools.x; -int main() { - std::string s = std::string("dir=") + mcpp::xpkg_dir("xim", "mcpp-e2e-absent-tool") - + " source=" + mcpp::xpkg_source("xim", "mcpp-e2e-absent-tool") - + " program=" + mcpp::xpkg_program("xim", "mcpp-e2e-absent-tool"); - mcpp::warning(s.c_str()); - return 0; -} -CPP -echo 'int main() { return 0; }' > "$work/app/src/main.cpp" -mkdir -p "$work/opt/bin" -printf '#!/bin/sh\necho tool\n' > "$work/opt/bin/absent-tool"; chmod +x "$work/opt/bin/absent-tool" - -cd "$work/app" -probe() { rm -rf target; MCPP_NO_AUTO_INSTALL=1 "$MCPP" "$@" 2>&1 || true; } -fail() { echo "FAIL: $*"; echo "----"; echo "$out"; exit 1; } - -# Control: declared and not overridden, so it is asked for. -out="$(probe build)" -grep -q "not provisioned" <<<"$out" || fail "the baseline did not ask for the payload" -grep -q "mcpp-e2e-absent-tool" <<<"$out" || fail "the refusal does not name the payload" - -# The environment variable. -out="$(MCPP_XLINGS_OVERRIDE_XIM_MCPP_E2E_ABSENT_TOOL="$work/opt/bin/absent-tool" probe build)" -grep -q "Finished" <<<"$out" || fail "an env override still needed the payload" -grep -q "Using xim:mcpp-e2e-absent-tool ← $work_HOST/opt/bin/absent-tool \[custom · env MCPP_XLINGS_OVERRIDE_XIM_MCPP_E2E_ABSENT_TOOL\]" <<<"$out" \ - || fail "no Using line naming the env override" -grep -q "dir=$work_HOST/opt source=override program=$work_HOST/opt/bin/absent-tool" <<<"$out" \ - || fail "the build program did not receive the override (root, source, program)" -grep -q "Finished .* · custom: xim:mcpp-e2e-absent-tool" <<<"$out" || fail "Finished does not summarise the source" - -# The manifest, with a version that satisfies the plugin's requirement. -cp mcpp.toml.base mcpp.toml -printf '\n[xlings.overrides]\n"xim:mcpp-e2e-absent-tool" = { program = "%s", version = "2.1" }\n' \ - "$work/opt/bin/absent-tool" >> mcpp.toml -out="$(probe build)" -grep -q "\[custom · mcpp.toml:[0-9]*\]" <<<"$out" || fail "no Using line naming mcpp.toml" -# --managed-only refuses it, naming it. -out="$(probe --managed-only build)" -grep -q "managed-only" <<<"$out" && grep -q "mcpp-e2e-absent-tool" <<<"$out" \ - || fail "--managed-only did not refuse the override" - -# A stated version below the requirement is refused, naming both sides. -cp mcpp.toml.base mcpp.toml -printf '\n[xlings.overrides]\n"xim:mcpp-e2e-absent-tool" = { program = "%s", version = "1.0" }\n' \ - "$work/opt/bin/absent-tool" >> mcpp.toml -out="$(probe build)" -grep -q ">=2.0" <<<"$out" && grep -q "e2e:plug" <<<"$out" || fail "a version below the requirement was accepted" - -# A path that does not exist is refused, naming it. -cp mcpp.toml.base mcpp.toml -printf '\n[xlings.overrides]\n"xim:mcpp-e2e-absent-tool" = "%s"\n' "$work/nope" >> mcpp.toml -out="$(probe build)" -grep -q "does not exist" <<<"$out" || fail "a missing override path was accepted" - -# A dependency may not state where a payload comes from. -cp mcpp.toml.base mcpp.toml -printf '\n[xlings.overrides]\n"xim:mcpp-e2e-absent-tool" = "/bin/sh"\n' >> "$work/plug/mcpp.toml" -out="$(probe build)" -grep -q "is a dependency of this build" <<<"$out" || fail "a dependency's override was accepted" - -echo "PASS: overrides skip provisioning, reach the build program, and are reported" diff --git a/tests/e2e/874_payload_on_request.sh b/tests/e2e/874_payload_on_request.sh deleted file mode 100755 index 1f4472641..000000000 --- a/tests/e2e/874_payload_on_request.sh +++ /dev/null @@ -1,95 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc unix-shell -# `provision = "on-request"` (mcpp#755): a payload installed when a build -# program asks for it with `xpkg_request`, not before the program runs. -# -# Every build runs with `MCPP_NO_AUTO_INSTALL=1`, so "built" means "nothing was -# asked for", and a refusal names the requester. The pair is the criterion: the -# same project builds while the program does not ask, and is refused naming -# the program when it does. -set -e - -MCPP="${MCPP:-mcpp}" -work="$(mktemp -d)" -# A path written INTO a manifest goes through host_path (see _host_path.sh). -source "$(dirname "$0")/_host_path.sh" -work_HOST="$(host_path "$work")" -trap 'rm -rf "$work"' EXIT -export NO_COLOR=1 - -mkdir -p "$work/plug/src" "$work/plug/members" "$work/app/src" -cat > "$work/plug/mcpp.toml" <<'TOML' -[package] -name = "plug" -namespace = "e2e" -version = "0.1.0" - -[build] -sources = ["src/plug.cppm"] - -[features.tools-x] -sources = ["members/x.cppm"] - -[feature-xlings.tools-x] -"xim:mcpp-e2e-absent-tool" = { version = "1.0.0", provision = "on-request" } - -[targets.plug] -kind = "lib" -TOML -echo 'export module e2e.plug;' > "$work/plug/src/plug.cppm" -cat > "$work/plug/members/x.cppm" <<'CPP' -export module e2e.tools.x; -export namespace e2e::x { inline int v = 1; } -CPP -cat > "$work/app/mcpp.toml" < "$work/app/build.mcpp" <<'CPP' -import std; -import mcpp; -import e2e.tools.x; -int main() { - std::string s = std::string("source=") + mcpp::xpkg_source("xim", "mcpp-e2e-absent-tool"); - if (const char* ask = std::getenv("E2E_ASK"); ask && *ask) { - if (std::string_view(ask) == "undeclared") { - std::printf("mcpp:xpkg-request=xim:mcpp-e2e-undeclared\n"); - return 0; - } - mcpp::xpkg_request("xim", "mcpp-e2e-absent-tool"); - if (mcpp::xpkg_pending()) return 0; - } - mcpp::warning(s.c_str()); - return 0; -} -CPP -echo 'int main() { return 0; }' > "$work/app/src/main.cpp" - -cd "$work/app" -probe() { rm -rf target; MCPP_NO_AUTO_INSTALL=1 "$MCPP" "$@" 2>&1 || true; } -fail() { echo "FAIL: $*"; echo "----"; echo "$out"; exit 1; } - -out="$(probe build)" -grep -q "Finished" <<<"$out" || fail "a payload nobody asked for was required" -grep -q "source=pending" <<<"$out" || fail "the build program was not told the payload is pending" - -out="$(E2E_ASK=1 probe build)" -grep -q "payloads requested by the build program of \`app\`" <<<"$out" \ - || fail "a request did not reach the installer naming the requester" -grep -q "mcpp-e2e-absent-tool@1.0.0" <<<"$out" || fail "the refusal does not name the payload" - -out="$(E2E_ASK=undeclared probe build)" -grep -q "no manifest of this build declares" <<<"$out" || fail "a request for an undeclared payload was accepted" - -out="$(E2E_ASK=1 MCPP_NO_AUTO_INSTALL=1 "$MCPP" emit build-database --format json 2>/dev/null || true)" -grep -q "MCPP_BUILD_DATABASE_PAYLOAD_DEFERRED" <<<"$out" || fail "planning did not defer the request" - -echo "PASS: on-request payloads wait for a request, and a request names its requester" diff --git a/tests/e2e/877_why_sources.sh b/tests/e2e/877_why_sources.sh deleted file mode 100755 index 09a1c0bdd..000000000 --- a/tests/e2e/877_why_sources.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc unix-shell python3 -# `mcpp why sources|tool|payload` and `--format json` (mcpp#755): the decision -# record of a prepare, as the build reported it. -set -e - -MCPP="${MCPP:-mcpp}" -work="$(mktemp -d)" -trap 'rm -rf "$work"' EXIT -export NO_COLOR=1 -mkdir -p "$work/app/src" "$work/opt/bin" -printf '#!/bin/sh\n' > "$work/opt/bin/tool"; chmod +x "$work/opt/bin/tool" -cat > "$work/app/mcpp.toml" < "$work/app/src/main.cpp" -cd "$work/app" -fail() { echo "FAIL: $*"; echo "----"; echo "$out"; exit 1; } - -out="$(MCPP_NO_AUTO_INSTALL=1 "$MCPP" why payload mcpp-e2e 2>&1)" || fail "why payload failed" -grep -q "payload:xim:mcpp-e2e-absent-tool $work/opt/bin/tool" <<<"$out" || fail "the payload is not listed" -grep -q "custom · mcpp.toml:[0-9]*" <<<"$out" || fail "the origin is not listed" - -out="$(MCPP_NO_AUTO_INSTALL=1 "$MCPP" why sources --format json 2>/dev/null)" || fail "why sources --format json failed" -python3 - "$out" <<'PY' || exit 1 -import json, sys -env = json.loads(sys.argv[1]) -assert env["kind"] == "mcpp.why.sources", env["kind"] -subjects = {s["subject"]: s for s in env["data"]["sources"]} -p = subjects["payload:xim:mcpp-e2e-absent-tool"] -assert p["class"] == "custom" and p["origin"]["kind"] == "manifest", p -assert "toolchain.build" in subjects, list(subjects) -PY - -out="$("$MCPP" why nonsense 2>&1 || true)" -grep -q "is not a topic of \`mcpp why\`" <<<"$out" || fail "an unknown topic was accepted" - -echo "PASS: mcpp why reports the decision record" diff --git a/tests/e2e/89_build_mcpp.sh b/tests/e2e/89_build_mcpp.sh deleted file mode 100755 index dcf2209b7..000000000 --- a/tests/e2e/89_build_mcpp.sh +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env bash -# 89_build_mcpp.sh — L3 `build.mcpp`: a project-local native imperative build -# program (Zig build.zig / Cargo build.rs model, in C++). mcpp compiles it with -# the host toolchain and runs it before the main build; its stdout `mcpp:` -# directives augment the build (here: a cxxflag define + a generated source). A -# declared-input cache re-runs it only when its source / inputs / env change. -# See .agents/docs/2026-06-30-l3-build-mcpp-implementation-design.md. -# -# requires: gcc -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -mkdir -p app/src -cd app - -cat > mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" -EOF - -# main.cpp hard-asserts the cxxflag define reached this TU, and links a function -# that only exists in the generated source the build program writes. -cat > src/main.cpp <<'EOF' -#ifndef FROM_BUILD_MCPP -#error "build.mcpp cxxflag did not reach the translation unit" -#endif -int generated_value(); -int main() { return generated_value() == 42 ? 0 : 1; } -EOF - -# The build program: writes a generated source, emits a define, declares an env input. -cat > build.mcpp <<'EOF' -#include -#include -int main() { - std::ofstream("src/generated.cpp") << "int generated_value() { return 42; }\n"; - std::puts("mcpp:cxxflag=-DFROM_BUILD_MCPP=1"); - std::puts("mcpp:generated=src/generated.cpp"); - std::puts("mcpp:rerun-if-env-changed=MCPP_TEST_TOGGLE"); - return 0; -} -EOF - -# ── Build 1: first run — compiles+runs build.mcpp, applies directives ────── -"$MCPP" build > b1.log 2>&1 || { cat b1.log; echo "FAIL: build 1 errored"; exit 1; } -grep -q "build.mcpp" b1.log || { cat b1.log; echo "FAIL: build.mcpp not invoked"; exit 1; } -[ -f src/generated.cpp ] || { echo "FAIL: generated source not written"; exit 1; } -[ -f target/.build-mcpp/build.mcpp.cache ] || { echo "FAIL: cache not written under target/"; exit 1; } - -# The binary returns 0 only if both the define AND the generated source took effect. -"$MCPP" run > r1.log 2>&1 || { cat r1.log; echo "FAIL: run returned non-zero (define/generated source missing)"; exit 1; } - -# ── Build 2: touch a source so prepare runs again, but build.mcpp inputs are -# unchanged — the declared-input cache short-circuits the re-run. (A no-change -# rebuild is skipped wholesale by the top-level up-to-date check, so we touch a -# source to actually exercise the prepare path.) -touch src/main.cpp -# A reused program is stated under --verbose only (build output design revision 3, §7.1). -"$MCPP" build -v > b2.log 2>&1 || { cat b2.log; echo "FAIL: build 2 errored"; exit 1; } -grep -qi "build.mcpp.*cached" b2.log || { cat b2.log; echo "FAIL: build.mcpp did not short-circuit (expected cached)"; exit 1; } - -# ── Build 3: a declared env input changed — forces a re-run ──────────────── -touch src/main.cpp -MCPP_TEST_TOGGLE=1 "$MCPP" build > b3.log 2>&1 || { cat b3.log; echo "FAIL: build 3 errored"; exit 1; } -grep -qE "^ *build\.mcpp .* ran [0-9]" b3.log || { cat b3.log; echo "FAIL: changed env did not force build.mcpp re-run"; exit 1; } - -# ── CWD independence: build.mcpp must run with cwd = project root, so its -# relative file writes (the generated source) land in the project even when -# mcpp is invoked from a SUBDIRECTORY (e.g. workspace -p, or `cd src && mcpp`). -cd "$TMP" -mkdir -p sub/src -cat > sub/mcpp.toml <<'EOF' -[package] -name = "sub" -version = "0.1.0" -EOF -cat > sub/src/main.cpp <<'EOF' -#ifndef FROM_BUILD_MCPP -#error "define missing" -#endif -int gen(); -int main() { return gen() == 7 ? 0 : 1; } -EOF -cat > sub/build.mcpp <<'EOF' -#include -#include -int main() { - std::ofstream("src/gen.cpp") << "int gen(){return 7;}\n"; - std::puts("mcpp:generated=src/gen.cpp"); - std::puts("mcpp:cxxflag=-DFROM_BUILD_MCPP=1"); - return 0; -} -EOF -# Invoke from the nested src/ dir — mcpp walks up to the project; build.mcpp must -# still write src/gen.cpp into the project root, not into the cwd. -( cd sub/src && "$MCPP" build > "$TMP/sub.log" 2>&1 ) || { cat "$TMP/sub.log"; echo "FAIL: build from subdir errored (cwd not project root?)"; exit 1; } -[ -f sub/src/gen.cpp ] || { echo "FAIL: generated source not written to project root"; exit 1; } - -echo "OK" diff --git a/tests/e2e/902_a_build_dependency_library_is_compiled_as_its_package.sh b/tests/e2e/902_a_build_dependency_library_is_compiled_as_its_package.sh deleted file mode 100755 index af9f49c01..000000000 --- a/tests/e2e/902_a_build_dependency_library_is_compiled_as_its_package.sh +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env bash -# requires: -# 902 -- a library in [build-dependencies] is usable from build.mcpp as the -# package it is (mcpp#797, R1). -# -# The shape is a module wrapper over a header library (toml++, nlohmann.json, -# fmt in mcpp-index): the interface unit's global module fragment includes the -# library's own header through the package's `include_dirs`. Before the build -# programs' graph, the interface was compiled alone with the build program's -# flags, and the include failed (`fatal error: hl/hl.hpp: No such file`). -# -# A a plain `[build-dependencies]` entry, with no `host-module`, gives the -# build program the library: `` resolves, the package's -# `defines` apply, and the value reaches the target through a directive; -# B `[lib] path` names an interface root outside `src/`, and the module is -# imported under the name it declares; -# C the library is not part of the target (no object of it in the target's -# plan), since only [build-dependencies] names it. -set -e -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } -export MCPP_HOME="$TMP/mcpp-home" -source "$(dirname "$0")/_inherit_toolchain.sh" - -mkdir -p "$TMP/hl/include/hl" "$TMP/hl/modules" "$TMP/app/src" -cat > "$TMP/hl/include/hl/hl.hpp" <<'EOF' -#pragma once -namespace hl { inline int answer() { return HL_BASE + 2; } } -EOF -cat > "$TMP/hl/modules/hl.cppm" <<'EOF' -module; -#include -export module acme.hl; -export namespace hl { using hl::answer; } -EOF -cat > "$TMP/hl/mcpp.toml" <<'EOF' -[package] -namespace = "acme" -name = "hl" -version = "0.1.0" -[lib] -path = "modules/hl.cppm" -[build] -sources = ["modules/hl.cppm"] -include_dirs = ["include"] -defines = ["HL_BASE=40"] -[targets.hl] -kind = "lib" -EOF -cat > "$TMP/app/mcpp.toml" <<'EOF' -[package] -name = "app" -version = "0.1.0" -[build-dependencies.acme] -hl = { path = "../hl" } -[targets.app] -kind = "bin" -main = "src/main.cpp" -EOF -cat > "$TMP/app/src/main.cpp" <<'EOF' -#include -int main() { std::printf("V=%d\n", V); } -EOF -cat > "$TMP/app/build.mcpp" <<'EOF' -import std; -import mcpp; -import acme.hl; -int main() { mcpp::cxxflag(std::format("-DV={}", hl::answer()).c_str()); } -EOF - -cd "$TMP/app" -"$MCPP" build > b.log 2>&1 || fail "A: a header-backed module library in [build-dependencies] did not build" b.log -out="$("$MCPP" run 2>/dev/null | tr -d '\r' | grep '^V=' | tail -1)" -[ "$out" = "V=42" ] || fail "A/B: the library's value did not reach the target: $out" b.log -grep -q "host module" b.log && fail "A: the old host-module path was reported" b.log -# C: the target's own plan compiles no unit of the library. -if find target -path '*/obj/*' -name 'hl.m.o*' -not -path '*/.build-mcpp/*' | grep -q .; then - fail "C: the build dependency was compiled into the target's plan" -fi -echo "PASS: 902_a_build_dependency_library_is_compiled_as_its_package" diff --git a/tests/e2e/903_one_package_in_both_tables_has_two_instances.sh b/tests/e2e/903_one_package_in_both_tables_has_two_instances.sh deleted file mode 100755 index 7d31a502b..000000000 --- a/tests/e2e/903_one_package_in_both_tables_has_two_instances.sh +++ /dev/null @@ -1,138 +0,0 @@ -#!/usr/bin/env bash -# requires: elf python3 -# 903 -- one package in [dependencies] and [build-dependencies] has two -# instances, each correct (mcpp#797, R2, R3; design §3.5, M1-M3, M5-M7, M9, M10). -# -# The two declarations used to merge into one edge that carried the -# build-program request, and the target-reachability test read that request as -# "build time only": the package's sources were cleared and the target's -# `import` failed with `failed to read compiled module`. -# -# M1 same consumer, both tables: the target and build.mcpp both import it; -# M2 `host-module = true` written beside it changes nothing; -# M3 a feature requested only by the build dependency reaches the -# build-program instance and not the target instance; -# M5 a workspace: one member links it, another's build program uses it; -# M6 the target reaches it through another library, build.mcpp directly; -# M7 a dependency's own build.mcpp uses it while the root links it; -# M9 the build database has a target set and a `:host` set for it, and -# compile_commands.json describes the target units only; -# M10 an edit to its header recompiles both instances and reruns the program. -set -e -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } -export MCPP_HOME="$TMP/mcpp-home" -source "$(dirname "$0")/_inherit_toolchain.sh" - -mkdir -p "$TMP/hl/include/hl" "$TMP/hl/src" -cat > "$TMP/hl/include/hl/hl.hpp" <<'EOF' -#pragma once -namespace hl { -inline int answer() { return 42; } -inline int x_on() { -#ifdef HL_X - return 1; -#else - return 0; -#endif -} -} -EOF -cat > "$TMP/hl/src/hl.cppm" <<'EOF' -module; -#include -export module hl; -export namespace hl { using hl::answer; using hl::x_on; } -EOF -cat > "$TMP/hl/mcpp.toml" <<'EOF' -[package] -name = "hl" -version = "0.1.0" -[build] -sources = ["src/hl.cppm"] -include_dirs = ["include"] -[features] -x = { defines = ["HL_X"] } -[targets.hl] -kind = "lib" -EOF -program='import std; -import mcpp; -import hl; -int main() { - mcpp::cxxflag(std::format("-DV={}", hl::answer()).c_str()); - mcpp::cxxflag(std::format("-DX_IN_PROGRAM={}", hl::x_on()).c_str()); -}' -target_main='#include -import hl; -int main() { std::printf("V=%d target=%d xprog=%d xtarget=%d\n", V, hl::answer(), X_IN_PROGRAM, hl::x_on()); }' - -app() { # $1 dir, $2 extra manifest - mkdir -p "$1/src" - printf '[package]\nname = "%s"\nversion = "0.1.0"\n%s\n[targets.%s]\nkind = "bin"\nmain = "src/main.cpp"\n' \ - "$(basename "$1")" "$2" "$(basename "$1")" > "$1/mcpp.toml" - printf '%s\n' "$target_main" > "$1/src/main.cpp" - printf '%s\n' "$program" > "$1/build.mcpp" -} -run_line() { "$MCPP" run 2>/dev/null | grep '^V=' | tail -1; } - -# M1, M3 -app "$TMP/m1" $'[dependencies]\nhl = { path = "../hl" }\n[build-dependencies]\nhl = { path = "../hl", features = ["x"] }' -cd "$TMP/m1" -"$MCPP" build > b.log 2>&1 || fail "M1: one package in both tables did not build" b.log -[ "$(run_line)" = "V=42 target=42 xprog=1 xtarget=0" ] || fail "M1/M3: $(run_line)" b.log - -# M2 -sed -i 's/features = \["x"\] }/features = ["x"], host-module = true }/' mcpp.toml -"$MCPP" build > b2.log 2>&1 || fail "M2: host-module = true beside the entry broke the build" b2.log -grep -q "deprecated" b2.log && fail "M2: stating the default was reported" b2.log -[ "$(run_line)" = "V=42 target=42 xprog=1 xtarget=0" ] || fail "M2: $(run_line)" b2.log - -# M9 -"$MCPP" emit build-database --format json > db.json 2>/dev/null || fail "M9: emit build-database failed" -python3 - db.json <<'PY' || fail "M9: the build database does not describe both instances" db.json -import json, sys -doc = json.load(open(sys.argv[1]))["data"]["database"] -sets = {s["name"]: s for s in doc["sets"]} -assert "hl" in sets or any(n.endswith(".hl") for n in sets), sets.keys() -host = [n for n in sets if n.endswith("hl:host")] -assert host, sets.keys() -assert sets[host[0]]["ide"]["configuration"] == "build-program" -assert any(n.endswith(":build") for n in sets), sets.keys() -PY -"$MCPP" emit build-database --spec compile-commands > cc.json 2>/dev/null -grep -q 'build.mcpp' cc.json && fail "M9: compile_commands describes build.mcpp" cc.json - -# M10 -sleep 1 -sed -i 's/return 42;/return 43;/' "$TMP/hl/include/hl/hl.hpp" -"$MCPP" build > b3.log 2>&1 || fail "M10: the build after a header edit failed" b3.log -[ "$(run_line)" = "V=43 target=43 xprog=1 xtarget=0" ] || fail "M10: an instance kept the old header: $(run_line)" b3.log -sed -i 's/return 43;/return 42;/' "$TMP/hl/include/hl/hl.hpp" - -# M5 -mkdir -p "$TMP/ws" -printf '[workspace]\nmembers = ["a", "b"]\n' > "$TMP/ws/mcpp.toml" -app "$TMP/ws/a" $'[dependencies]\nhl = { path = "../../hl" }' -printf '#include \nimport hl;\nint main() { std::printf("V=%%d\\n", hl::answer()); }\n' > "$TMP/ws/a/src/main.cpp" -rm "$TMP/ws/a/build.mcpp" -app "$TMP/ws/b" $'[build-dependencies]\nhl = { path = "../../hl" }' -printf '#include \nint main() { std::printf("V=%%d\\n", V); }\n' > "$TMP/ws/b/src/main.cpp" -cd "$TMP/ws" -"$MCPP" build --workspace > w.log 2>&1 || fail "M5: the workspace did not build" w.log -[ "$("$MCPP" run -p a 2>/dev/null | grep '^V=' | tail -1)" = "V=42" ] || fail "M5: member a" w.log -[ "$("$MCPP" run -p b 2>/dev/null | grep '^V=' | tail -1)" = "V=42" ] || fail "M5: member b" w.log - -# M6, M7 -mkdir -p "$TMP/mid/src" -printf '[package]\nname = "mid"\nversion = "0.1.0"\n[dependencies]\nhl = { path = "../hl" }\n[build-dependencies]\nhl = { path = "../hl" }\n[targets.mid]\nkind = "lib"\n' > "$TMP/mid/mcpp.toml" -printf 'export module mid;\nimport hl;\nexport int via_mid() { return hl::answer() + MID; }\n' > "$TMP/mid/src/mid.cppm" -printf 'import std;\nimport mcpp;\nimport hl;\nint main() { mcpp::cxxflag(std::format("-DMID={}", hl::answer() - 42).c_str()); }\n' > "$TMP/mid/build.mcpp" -app "$TMP/m6" $'[dependencies]\nmid = { path = "../mid" }\n[build-dependencies]\nhl = { path = "../hl" }' -printf '#include \nimport mid;\nint main() { std::printf("V=%%d mid=%%d\\n", V, via_mid()); }\n' > "$TMP/m6/src/main.cpp" -cd "$TMP/m6" -"$MCPP" build > b.log 2>&1 || fail "M6/M7: a library reached through another one, used by two programs, did not build" b.log -[ "$(run_line)" = "V=42 mid=42" ] || fail "M6/M7: $(run_line)" b.log - -echo "PASS: 903_one_package_in_both_tables_has_two_instances" diff --git a/tests/e2e/904_a_build_dependency_feature_stays_out_of_the_target.sh b/tests/e2e/904_a_build_dependency_feature_stays_out_of_the_target.sh deleted file mode 100755 index 0eced0d7d..000000000 --- a/tests/e2e/904_a_build_dependency_feature_stays_out_of_the_target.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env bash -# requires: elf -# 904 -- features are activated per instance (mcpp#797, R3). -# -# A feature one package requests of a library in its [build-dependencies] is -# a request of the library's build-program instance. It used to be unioned -# with every other request of the package, so it reached the target instance: -# the target compile of the library carried the feature's define. -# -# A the target instance of `hl` does not have feature `x`, which only -# `mid`'s [build-dependencies] requests; -# B the build-program instance has it: mid's build program sees `x`. -set -e -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } -export MCPP_HOME="$TMP/mcpp-home" -source "$(dirname "$0")/_inherit_toolchain.sh" - -mkdir -p "$TMP/hl/src" "$TMP/mid/src" "$TMP/app/src" -cat > "$TMP/hl/src/hl.cppm" <<'EOF' -export module hl; -export namespace hl { -inline int x_on() { -#ifdef HL_X - return 1; -#else - return 0; -#endif -} -} -EOF -printf '[package]\nname = "hl"\nversion = "0.1.0"\n[features]\nx = { defines = ["HL_X"] }\n[targets.hl]\nkind = "lib"\n' > "$TMP/hl/mcpp.toml" -printf '[package]\nname = "mid"\nversion = "0.1.0"\n[build-dependencies]\nhl = { path = "../hl", features = ["x"] }\n[targets.mid]\nkind = "lib"\n' > "$TMP/mid/mcpp.toml" -printf 'export module mid;\nexport int mid_x() { return MID_X; }\n' > "$TMP/mid/src/mid.cppm" -printf 'import std;\nimport mcpp;\nimport hl;\nint main() { mcpp::cxxflag(std::format("-DMID_X={}", hl::x_on()).c_str()); }\n' > "$TMP/mid/build.mcpp" -printf '[package]\nname = "app"\nversion = "0.1.0"\n[dependencies]\nhl = { path = "../hl" }\nmid = { path = "../mid" }\n[targets.app]\nkind = "bin"\nmain = "src/main.cpp"\n' > "$TMP/app/mcpp.toml" -printf '#include \nimport hl;\nimport mid;\nint main() { std::printf("target=%%d program=%%d\\n", hl::x_on(), mid_x()); }\n' > "$TMP/app/src/main.cpp" - -cd "$TMP/app" -"$MCPP" build > b.log 2>&1 || fail "the build failed" b.log -out="$("$MCPP" run 2>/dev/null | grep '^target=' | tail -1)" -[ "$out" = "target=0 program=1" ] || fail "A/B: $out (want target=0 program=1)" b.log -echo "PASS: 904_a_build_dependency_feature_stays_out_of_the_target" diff --git a/tests/e2e/905_a_build_program_links_its_libraries_whole.sh b/tests/e2e/905_a_build_program_links_its_libraries_whole.sh deleted file mode 100755 index 0209ea443..000000000 --- a/tests/e2e/905_a_build_program_links_its_libraries_whole.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env bash -# requires: elf -# 905 -- a library a build program uses is compiled and linked whole -# (mcpp#797, #797 Q4/Q5): its implementation units, its C sources and its own -# dependencies reach the build program. The single-unit path compiled one -# object per interface unit and nothing else, so a library whose functions are -# defined out of line could not be used at all. -set -e -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } -export MCPP_HOME="$TMP/mcpp-home" -source "$(dirname "$0")/_inherit_toolchain.sh" - -mkdir -p "$TMP/base/src" "$TMP/lib/src" "$TMP/app/src" -printf 'export module acme.base;\nexport int base_two();\n' > "$TMP/base/src/base.cppm" -printf 'module acme.base;\nint base_two() { return 2; }\n' > "$TMP/base/src/base_impl.cpp" -printf '[package]\nname = "base"\nversion = "0.1.0"\n[targets.base]\nkind = "lib"\n' > "$TMP/base/mcpp.toml" -printf 'export module lib;\nexport int lib_value();\n' > "$TMP/lib/src/lib.cppm" -cat > "$TMP/lib/src/lib_impl.cpp" <<'EOF' -module lib; -import acme.base; -extern "C" int c_forty(); -int lib_value() { return c_forty() + base_two(); } -EOF -printf 'int c_forty(void) { return 40; }\n' > "$TMP/lib/src/forty.c" -printf '[package]\nname = "lib"\nversion = "0.1.0"\n[dependencies]\nbase = { path = "../base" }\n[targets.lib]\nkind = "lib"\n' > "$TMP/lib/mcpp.toml" -printf '[package]\nname = "app"\nversion = "0.1.0"\n[build-dependencies]\nlib = { path = "../lib" }\n[targets.app]\nkind = "bin"\nmain = "src/main.cpp"\n' > "$TMP/app/mcpp.toml" -printf '#include \nint main() { std::printf("V=%%d\\n", V); }\n' > "$TMP/app/src/main.cpp" -printf 'import std;\nimport mcpp;\nimport lib;\nint main() { mcpp::cxxflag(std::format("-DV={}", lib_value()).c_str()); }\n' > "$TMP/app/build.mcpp" - -cd "$TMP/app" -"$MCPP" build > b.log 2>&1 || fail "a library with implementation units, C sources and a dependency did not link into the build program" b.log -[ "$("$MCPP" run 2>/dev/null | grep '^V=' | tail -1)" = "V=42" ] || fail "the program computed another value" b.log -echo "PASS: 905_a_build_program_links_its_libraries_whole" diff --git a/tests/e2e/906_a_build_program_is_compiled_when_its_graph_changes.sh b/tests/e2e/906_a_build_program_is_compiled_when_its_graph_changes.sh deleted file mode 100755 index af2554a61..000000000 --- a/tests/e2e/906_a_build_program_is_compiled_when_its_graph_changes.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env bash -# requires: elf -# 906 -- a build program is compiled again exactly when its identity moves, and -# a build whose programs stand does not plan their graph (mcpp#797, R6). -# -# A an edit to a header a path library includes reruns the program with the -# new value (the identity used to hash the interface file only); -# B a build in which nothing changed states that the identity is current -# and plans no build programs' graph; -# C the identity and the reason are in the verbose log. -set -e -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } -export MCPP_HOME="$TMP/mcpp-home" -source "$(dirname "$0")/_inherit_toolchain.sh" - -mkdir -p "$TMP/hl/src" "$TMP/app/src" -printf '#define HL_VALUE 42\n' > "$TMP/hl/src/value.h" -printf 'module;\n#include "value.h"\nexport module hl;\nexport int hl_value() { return HL_VALUE; }\n' > "$TMP/hl/src/hl.cppm" -printf '[package]\nname = "hl"\nversion = "0.1.0"\n[targets.hl]\nkind = "lib"\n' > "$TMP/hl/mcpp.toml" -printf '[package]\nname = "app"\nversion = "0.1.0"\n[build-dependencies]\nhl = { path = "../hl" }\n[targets.app]\nkind = "bin"\nmain = "src/main.cpp"\n' > "$TMP/app/mcpp.toml" -printf '#include \nint main() { std::printf("V=%%d\\n", V); }\n' > "$TMP/app/src/main.cpp" -printf 'import std;\nimport mcpp;\nimport hl;\nint main() { mcpp::cxxflag(std::format("-DV={}", hl_value()).c_str()); }\n' > "$TMP/app/build.mcpp" - -cd "$TMP/app" -"$MCPP" build > b1.log 2>&1 || fail "the first build failed" b1.log -[ "$("$MCPP" run 2>/dev/null | grep '^V=' | tail -1)" = "V=42" ] || fail "first value" b1.log - -# B, C: nothing changed. A source the fast path sweeps is touched so that -# planning runs; the program's identity stands. -sleep 1 -touch src/main.cpp -"$MCPP" build --verbose > b2.log 2>&1 || fail "B: the second build failed" b2.log -grep -qE "build-program: app: identity [0-9a-f]+ current" b2.log || fail "C: the log does not state the identity stood" b2.log -grep -q "host-graph: build programs in" b2.log && fail "B: the build programs' graph was planned although nothing changed" b2.log - -# A -sleep 1 -printf '#define HL_VALUE 43\n' > "$TMP/hl/src/value.h" -"$MCPP" build --verbose > b3.log 2>&1 || fail "A: the build after the header edit failed" b3.log -grep -qE "build-program: app: identity [0-9a-f]+ -> [0-9a-f]+, compiled" b3.log || fail "C: the log does not state why the program was compiled" b3.log -[ "$("$MCPP" run 2>/dev/null | grep '^V=' | tail -1)" = "V=43" ] || fail "A: the program ran on the old header" b3.log -echo "PASS: 906_a_build_program_is_compiled_when_its_graph_changes" diff --git a/tests/e2e/908_what_a_build_dependency_asks_for.sh b/tests/e2e/908_what_a_build_dependency_asks_for.sh deleted file mode 100755 index 4dea30326..000000000 --- a/tests/e2e/908_what_a_build_dependency_asks_for.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env bash -# requires: elf -# 908 -- what a [build-dependencies] entry asks for (SPEC-004 §12 item 5). -# -# A with neither `tools` nor `host-module`, the library: build.mcpp imports it; -# B with only `tools`, the program and no library: an import of the library -# is refused by name, before the compiler, pointing at `host-module`; -# C `host-module = true` beside `tools` gives both; -# D `host-module = false` declines the library. -set -e -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } -export MCPP_HOME="$TMP/mcpp-home" -source "$(dirname "$0")/_inherit_toolchain.sh" - -mkdir -p "$TMP/gen/src" "$TMP/gen/tools" "$TMP/app/src" -printf 'export module acme.gen;\nexport int gen_value() { return 42; }\n' > "$TMP/gen/src/gen.cppm" -printf '#include \nint main() { std::puts("generated"); }\n' > "$TMP/gen/tools/gen_main.cpp" -cat > "$TMP/gen/mcpp.toml" <<'EOF' -[package] -namespace = "acme" -name = "gen" -version = "0.1.0" -[build] -sources = ["src/gen.cppm"] -[targets.gen] -kind = "lib" -[targets.gentool] -kind = "bin" -main = "tools/gen_main.cpp" -EOF -printf '#include \nint main() { std::printf("V=%%d\\n", V); }\n' > "$TMP/app/src/main.cpp" -printf 'import std;\nimport mcpp;\nimport acme.gen;\nint main() { mcpp::cxxflag(std::format("-DV={}", gen_value()).c_str()); }\n' > "$TMP/app/build.mcpp" -manifest() { printf '[package]\nname = "app"\nversion = "0.1.0"\n[build-dependencies.acme]\ngen = { path = "../gen"%s }\n[targets.app]\nkind = "bin"\nmain = "src/main.cpp"\n' "$1" > "$TMP/app/mcpp.toml"; } -cd "$TMP/app" - -manifest "" -"$MCPP" build > a.log 2>&1 || fail "A: the default entry did not give the library" a.log - -manifest ', tools = ["gentool"]' -"$MCPP" build > b.log 2>&1 && fail "B: a tools-only entry gave the library" b.log -grep -q "imports 'acme.gen'" b.log || fail "B: the refusal does not name the module" b.log -grep -q "host-module = true" b.log || fail "B: the refusal does not name the key" b.log -grep -qi "failed to read compiled module" b.log && fail "B: the compiler was reached" b.log - -manifest ', tools = ["gentool"], host-module = true' -"$MCPP" build > c.log 2>&1 || fail "C: tools and host-module = true did not give both" c.log - -manifest ', host-module = false' -"$MCPP" build > d.log 2>&1 && fail "D: host-module = false still gave the library" d.log -grep -q "imports 'acme.gen'" d.log || fail "D: the refusal does not name the module" d.log -echo "PASS: 908_what_a_build_dependency_asks_for" diff --git a/tests/e2e/910_a_build_plugin_is_a_package_of_the_build_program.sh b/tests/e2e/910_a_build_plugin_is_a_package_of_the_build_program.sh deleted file mode 100755 index b8b3371ea..000000000 --- a/tests/e2e/910_a_build_plugin_is_a_package_of_the_build_program.sh +++ /dev/null @@ -1,114 +0,0 @@ -#!/usr/bin/env bash -# requires: elf -# 910 -- build plugins (SPEC-007 §10, mcpp#797). A unit that imports -# `mcpp.core` (or `mcpp`) is a build-plugin unit; nothing else declares it. -# -# A a plugin built on another plugin and on an ordinary library, each named -# in its [dependencies], serves a consumer's build program (S1, S2); -# B P3: the plugin builds as the root of a build, in the build-program -# configuration, and its tests run; the engine package's cache entry is -# filled there, its `mcpp.core` interface built though the plugin imports -# `mcpp` alone; -# C P3: `--target` for another machine is refused, naming the plugin; -# D P1: a plugin reached through [dependencies] of a target is refused, -# naming the chain and [build-dependencies]; -# E P4: a program target importing mcpp is refused; -# F P5: `mcpp pack` refuses a plugin; -# G a library a plugin depends on is importable by the plugin's consumers' -# build programs when the plugin hands it on (`reexport = true` on its -# [dependencies] edge), and refused, naming `reexport`, when it does not. -set -e -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } -export MCPP_HOME="$TMP/mcpp-home" -source "$(dirname "$0")/_inherit_toolchain.sh" - -lib() { # $1 = name, $2 = dependencies table body - mkdir -p "$TMP/$1/src" - printf '[package]\nnamespace = "acme"\nname = "%s"\nversion = "0.1.0"\n[dependencies.acme]\n%s\n[targets.%s]\nkind = "lib"\n' \ - "$1" "$2" "$1" > "$TMP/$1/mcpp.toml" -} -lib util "" -printf 'export module acme.util;\nexport int util_base() { return 40; }\n' > "$TMP/util/src/util.cppm" -lib core-rule "" -cat > "$TMP/core-rule/src/core-rule.cppm" <<'EOF' -export module acme.core_rule; -import std; -import mcpp; -export namespace acme::core_rule { -inline void define(std::string_view key, int value) { mcpp::define(std::format("{}={}", key, value).c_str()); } -} -EOF -lib rule 'core-rule = { path = "../core-rule" } -util = { path = "../util" }' -cat > "$TMP/rule/src/rule.cppm" <<'EOF' -export module acme.rule; -import mcpp; -import acme.core_rule; -import acme.util; -export namespace acme::rule { -inline int value() { return util_base() + 2; } -inline void go() { acme::core_rule::define("V", value()); } -} -EOF - -# A -mkdir -p "$TMP/app/src" -printf '[package]\nname = "app"\nversion = "0.1.0"\n[build-dependencies.acme]\nrule = { path = "../rule" }\n[targets.app]\nkind = "bin"\nmain = "src/main.cpp"\n' > "$TMP/app/mcpp.toml" -printf '#include \nint main() { std::printf("V=%%d\\n", V); }\n' > "$TMP/app/src/main.cpp" -printf 'import acme.rule;\nint main() { acme::rule::go(); }\n' > "$TMP/app/build.mcpp" -cd "$TMP/app" -"$MCPP" build > a.log 2>&1 || fail "A: a plugin built on a plugin and a library did not serve the build program" a.log -[ "$("$MCPP" run 2>/dev/null | grep '^V=' | tail -1)" = "V=42" ] || fail "A: value" a.log -if find target -path '*/obj/*' -name '*.m.o' -not -path '*/.build-mcpp/*' | grep -E 'rule|util' | grep -q .; then - fail "A: a package of the build program reached the target" -fi - -# B -mkdir -p "$TMP/rule/tests" -printf 'import mcpp;\nimport acme.rule;\nint main() { return acme::rule::value() == 42 ? 0 : 1; }\n' > "$TMP/rule/tests/value.cpp" -cd "$TMP/rule" -"$MCPP" build > b1.log 2>&1 || fail "B: the plugin did not build as the root of a build" b1.log -grep -q "build-program" b1.log || fail "B: the build does not state its configuration" b1.log -! grep -q "populate failed" b1.log || fail "B: the engine package's cache entry was not filled" b1.log -ls "$MCPP_HOME"/build-cache/v1/pkg/_engine/*/*/bmi/mcpp.core.* > /dev/null 2>&1 \ - || fail "B: the engine package's entry lacks mcpp.core" b1.log -"$MCPP" test > b2.log 2>&1 || fail "B: the plugin's tests did not run" b2.log -grep -q "1 passed" b2.log || fail "B: the test did not pass" b2.log - -# C -"$MCPP" build --target aarch64-linux-gnu > c.log 2>&1 && fail "C: --target for another machine was accepted" c.log -grep -q "acme.rule' is a build plugin" c.log && grep -q "cannot apply" c.log \ - || fail "C: the refusal does not name the plugin" c.log - -# D -mkdir -p "$TMP/app2/src" -printf '[package]\nname = "app2"\nversion = "0.1.0"\n[dependencies.acme]\nrule = { path = "../rule" }\n[targets.app2]\nkind = "bin"\nmain = "src/main.cpp"\n' > "$TMP/app2/mcpp.toml" -printf 'int main() { return 0; }\n' > "$TMP/app2/src/main.cpp" -cd "$TMP/app2" -"$MCPP" build > d.log 2>&1 && fail "D: a plugin in [dependencies] of a target was accepted" d.log -grep -q "is a build plugin" d.log && grep -q "app2 → acme.rule" d.log && grep -q "\[build-dependencies\]" d.log \ - || fail "D: the refusal does not name the chain and the table" d.log - -# E -mkdir -p "$TMP/app3/src" -printf '[package]\nname = "app3"\nversion = "0.1.0"\n[targets.app3]\nkind = "bin"\nmain = "src/main.cpp"\n' > "$TMP/app3/mcpp.toml" -printf 'import mcpp;\nint main() { return 0; }\n' > "$TMP/app3/src/main.cpp" -cd "$TMP/app3" -"$MCPP" build > e.log 2>&1 && fail "E: a program importing mcpp was accepted" e.log -grep -q "exists only in a build" e.log || fail "E: the refusal does not say why" e.log - -# F -cd "$TMP/rule" -"$MCPP" pack > f.log 2>&1 && fail "F: mcpp pack accepted a plugin" f.log -grep -q "is a build plugin" f.log || fail "F: the refusal does not say why" f.log -# G -cd "$TMP/app" -printf 'import acme.rule;\nimport acme.util;\nint main() { if (util_base() == 40) acme::rule::go(); }\n' > build.mcpp -"$MCPP" build > g1.log 2>&1 && fail "G: a library the plugin does not hand on was importable" g1.log -grep -q "reexport = true" g1.log || fail "G: the refusal does not name reexport" g1.log -sed -i 's|util = { path = "../util" }|util = { path = "../util", reexport = true }|' "$TMP/rule/mcpp.toml" -"$MCPP" build > g2.log 2>&1 || fail "G: a library the plugin hands on was refused" g2.log -[ "$("$MCPP" run 2>/dev/null | grep '^V=' | tail -1)" = "V=42" ] || fail "G: value" g2.log -echo "PASS: 910_a_build_plugin_is_a_package_of_the_build_program" diff --git a/tests/e2e/92_build_mcpp_import.sh b/tests/e2e/92_build_mcpp_import.sh deleted file mode 100755 index 5143f00b5..000000000 --- a/tests/e2e/92_build_mcpp_import.sh +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env bash -# 92_build_mcpp_import.sh — the bundled `import mcpp;` typed build library. A -# build.mcpp written modules-first (no #include, no `import std;`) calls the typed -# API (mcpp::cxxflag / define / generated / link_lib …), which emits the same -# `mcpp:` wire protocol the engine parses. mcpp compiles the bundled module on the -# fly and links it. See .agents/docs/2026-06-30-l3-build-mcpp-implementation-design.md. -# -# requires: gcc -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -mkdir -p app/src -cd app -cat > mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" -EOF - -# Modules-first build program: import mcpp;, no headers, no import std. -cat > build.mcpp <<'EOF' -import mcpp; -int main() { - mcpp::generated("src/gen.cpp"); // a generated source to link - mcpp::cxxflag("-DVIA_MODULE=1"); // a compile flag - mcpp::define("MODULE_DEFINE"); // cfg= → -DMODULE_DEFINE - mcpp::rerun_if_changed("build.mcpp"); - return 0; -} -EOF -# build.mcpp uses generated() to declare src/gen.cpp; write it (the program could -# also generate it — here we just commit it so the test stays focused on import). -cat > src/gen.cpp <<'EOF' -int gen_value() { return 99; } -EOF -cat > src/main.cpp <<'EOF' -#if !defined(VIA_MODULE) || !defined(MODULE_DEFINE) -#error "import mcpp typed directives did not reach the build" -#endif -int gen_value(); -int main() { return gen_value() == 99 ? 0 : 1; } -EOF - -"$MCPP" build > b.log 2>&1 || { cat b.log; echo "FAIL: import mcpp build errored"; exit 1; } -grep -q "build.mcpp" b.log || { cat b.log; echo "FAIL: build.mcpp not invoked"; exit 1; } -# The bundled module is compiled once and kept by key: in the global cache, where -# every project of the machine finds it, or, when the cache mode asks for no -# global entry, in the build programs' graph under target/ (#748, #797). -CACHE_ROOT="$("$MCPP" cache dir | head -1)" -find "$CACHE_ROOT/pkg/_engine" target/.build-mcpp/host \( -name 'mcpp.m.o' -o -name 'mcpp.m.obj' \) 2>/dev/null | grep -q . \ - || { echo "FAIL: bundled mcpp module not compiled"; exit 1; } -# The binary returns 0 only if both the module-emitted define AND the generated -# source took effect. -"$MCPP" run > r.log 2>&1 || { cat r.log; echo "FAIL: run non-zero (module directives/generated source missing)"; exit 1; } - -echo "OK" diff --git a/tests/e2e/958_a_build_program_runs_again_when_its_output_is_gone.sh b/tests/e2e/958_a_build_program_runs_again_when_its_output_is_gone.sh deleted file mode 100755 index fb14b4339..000000000 --- a/tests/e2e/958_a_build_program_runs_again_when_its_output_is_gone.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 958 -- a build program's cached run is replayed only while what it wrote is -# there. The program writes a header below `MCPP_OUT_DIR` and names the -# directory with `mcpp::include_dir`; once that directory is deleted, a replay -# would hand the compile an include directory with nothing in it. The program -# runs again instead (measured 2026-10-28 on openkal-image's build program: a -# deleted output directory, then generated includes missing). -# -# Criteria: -# A. A second build replays the program (cached) and compiles nothing. -# B. With the program's output deleted, the build runs the program again and -# the unit finds its header. -set -e - -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } - -mkdir -p "$TMP/app/src" -cd "$TMP/app" -cat > build.mcpp <<'CPP' -#include -#include -#include -import mcpp; -int main() { - const std::string inc = std::string(mcpp::out_dir()) + "/inc"; - std::filesystem::create_directories(inc); - std::ofstream(inc + "/generated.h") << "#define GENERATED 7\n"; - mcpp::include_dir(inc.c_str()); - return 0; -} -CPP -printf '#include \n#include \nint main() { std::printf("generated %%d\\n", GENERATED); return 0; }\n' > src/main.cpp -printf '[package]\nname = "regen"\nversion = "0.1.0"\n' > mcpp.toml - -"$MCPP" run > a1.log 2>&1 || fail "A: the first build" a1.log -grep -q "generated 7" a1.log || fail "A: the header did not arrive" a1.log -"$MCPP" build -v > a2.log 2>&1 || fail "A: the second build" a2.log -grep -q "Compiling regen" a2.log && fail "A: the second build compiled" a2.log -echo "ok: a second build replays the program" - -# B -out=$(find target -type f -name generated.h | head -1) -[ -n "$out" ] || fail "B: the generated header is not under target/" -rm -rf "$(dirname "$out")" -rm -rf target/*/*/bin target/*/*/obj -"$MCPP" run -v > b.log 2>&1 || fail "B: the build after the output was deleted" b.log -grep -q "generated 7" b.log || fail "B: the program did not run again" b.log -echo "ok: the program runs again when its output is gone" - -echo "PASS: 958 a build program runs again when its output is gone" diff --git a/tests/e2e/997_a_members_dev_dependencies_stay_out_of_a_host_tool.sh b/tests/e2e/997_a_members_dev_dependencies_stay_out_of_a_host_tool.sh deleted file mode 100755 index 04b4b332a..000000000 --- a/tests/e2e/997_a_members_dev_dependencies_stay_out_of_a_host_tool.sh +++ /dev/null @@ -1,124 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 997 -- a host tool has one store key whatever the command and the member -# (mcpp#874). The key's dependency closure was read from the graph the build -# had walked: empty where only a build dependency's `tools` reached the tool -# package (`mcpp test --workspace`), complete where it was a member being -# built (`mcpp build`), and holding the dev-dependencies of a member whose -# tests were built. The tool's path changed with the key, every action that -# ran it ran again, and its outputs rebuilt every importer. The closure is now -# the tool package's own manifests, as its build resolves them. -# -# Criteria: -# A. `mcpp test --workspace`, then `mcpp build`: the tool has one store entry, -# and it names `lib` (the tool's path dependency) and not `testing` -# (`lib`'s dev-dependency). -# B. A second `mcpp test --workspace` builds no host tool. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" -export MCPP_HOME="$TMP/mcpphome" -mkdir -p "$MCPP_HOME" -[ -d "$HOME/.mcpp/registry" ] && ln -s "$HOME/.mcpp/registry" "$MCPP_HOME/registry" -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } - -mkdir -p ws/testing/src ws/lib/src ws/lib/tests ws/toolpkg/src ws/app/src ws/app/tests -cd ws -cat > mcpp.toml <<'EOF' -[workspace] -members = ["testing", "lib", "toolpkg", "app"] -EOF -cat > testing/mcpp.toml <<'EOF' -[package] -name = "testing" -version = "0.1.0" - -[targets.testing] -kind = "lib" -EOF -echo 'int testing_ok() { return 0; }' > testing/src/testing.cpp -cat > lib/mcpp.toml <<'EOF' -[package] -name = "lib" -version = "0.1.0" - -[targets.lib] -kind = "lib" - -[dev-dependencies] -testing = { path = "../testing" } -EOF -echo 'int lib_value() { return 41; }' > lib/src/lib.cpp -cat > lib/tests/smoke.cpp <<'EOF' -int testing_ok(); -int lib_value(); -int main() { return testing_ok() + (lib_value() == 41 ? 0 : 1); } -EOF -cat > toolpkg/mcpp.toml <<'EOF' -[package] -name = "toolpkg" -version = "0.1.0" - -[dependencies] -lib = { path = "../lib" } - -[targets.gen] -kind = "bin" -main = "src/gen.cpp" -EOF -cat > toolpkg/src/gen.cpp <<'EOF' -#include -int lib_value(); -int main(int argc, char** argv) { - if (argc < 2) return 2; - FILE* f = std::fopen(argv[1], "w"); - if (!f) return 3; - std::fprintf(f, "int generated_answer() { return %d; }\n", lib_value() + 1); - return std::fclose(f); -} -EOF -cat > app/mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" - -[build-dependencies] -toolpkg = { path = "../toolpkg", tools = ["gen"] } -EOF -cat > app/src/main.cpp <<'EOF' -#include -int generated_answer(); -int main() { std::printf("ANSWER=%d\n", generated_answer()); } -EOF -cat > app/tests/answer.cpp <<'EOF' -int generated_answer(); -int main() { return generated_answer() == 42 ? 0 : 1; } -EOF -cat > app/build.mcpp <<'EOF' -#include -#include -import mcpp; -int main() { - const std::string out = std::string(mcpp::out_dir()) + "/gen.cpp"; - const std::string cmd = std::string("\"") + mcpp::dep_bin("toolpkg", "gen") + "\" \"" + out + "\""; - if (std::system(cmd.c_str()) != 0) return 1; - mcpp::generated(out.c_str()); -} -EOF - -# A -"$MCPP" test --workspace > t1.log 2>&1 || fail "the first test run failed" t1.log -"$MCPP" build > b1.log 2>&1 || fail "the build failed" b1.log -keys=$(find "$MCPP_HOME/build-cache/v1/tool" -name entry.json -path "*toolpkg@*" | wc -l) -entries=$(find "$MCPP_HOME/build-cache/v1/tool" -name entry.json -path "*toolpkg@*") -[ "$keys" -eq 1 ] || fail "the tool has $keys store entries, expected 1" $entries -grep -q '"lib@' $entries || fail "the key does not name the tool's dependency lib" $entries -grep -q '"testing@' $entries && fail "the key names lib's dev-dependency testing" $entries - -# B -"$MCPP" test --workspace > t2.log 2>&1 || fail "the second test run failed" t2.log -grep -q "Building.*host tool" t2.log && fail "the second test run built a host tool again" t2.log - -echo "OK" diff --git a/tests/e2e/MIGRATION.tsv b/tests/e2e/MIGRATION.tsv index 14396c66c..08bef097c 100644 --- a/tests/e2e/MIGRATION.tsv +++ b/tests/e2e/MIGRATION.tsv @@ -62,7 +62,7 @@ script subsystem class destination note 55_dependency_shared_artifact.sh runtime merge e2e/runtime 56_transitive_shared_artifact.sh runtime merge e2e/runtime 57_static_dep_shared_artifact.sh runtime merge e2e/runtime -58_preinstall_mcpp_deps_for_hooks.sh build-program merge e2e/build-program +58_preinstall_mcpp_deps_for_hooks.sh build-program merge e2e/build-program::deps/58 preinstall mcpp deps for hooks migrated (batch 2); script removed 59_cpp_standard_config.sh toolchain merge e2e/toolchain 60_stale_xpkg_cache_reinstall.sh index merge e2e/index 61_dependency_visibility.sh deps merge e2e/deps @@ -96,10 +96,10 @@ script subsystem class destination note 86_target_cfg_dependencies.sh features merge e2e/features 87_build_default_profile.sh build merge e2e/build 88_xlings_environment.sh self merge e2e/self -89_build_mcpp.sh build-program merge e2e/build-program +89_build_mcpp.sh build-program merge e2e/build-program::protocol/89 build mcpp migrated (batch 2); script removed 90_workspace_test.sh test merge e2e/test::workspace/90 workspace test migrated (batch 1); script removed 91_target_bare_alias.sh target merge e2e/target -92_build_mcpp_import.sh build-program merge e2e/build-program +92_build_mcpp_import.sh build-program merge e2e/build-program::protocol/92 build mcpp import migrated (batch 2); script removed 93_xpkg_parse.sh index sink unit/test_xpkg_parse answers a parse/selection rule without a build: descriptor grammar, strict keys 94_macos_test_stdlib.sh test merge e2e/test::frameworks/94 macos test stdlib migrated (batch 1); script removed; skips without macos, as the script was excluded 95_msvc_system_toolchain.sh windows merge e2e/windows @@ -118,9 +118,9 @@ script subsystem class destination note 107_generated_files_toml.sh actions merge e2e/actions 108_cfg_conditional_sources.sh features merge e2e/features 109_per_glob_flags.sh build merge e2e/build -110_build_mcpp_env_contract.sh build-program merge e2e/build-program -111_dep_build_mcpp.sh build-program merge e2e/build-program -112_build_mcpp_cross.sh build-program merge e2e/build-program +110_build_mcpp_env_contract.sh build-program merge e2e/build-program::protocol/110 build mcpp env contract migrated (batch 2); script removed +111_dep_build_mcpp.sh build-program merge e2e/build-program::deps/111 dep build mcpp migrated (batch 2); script removed +112_build_mcpp_cross.sh build-program merge e2e/build-program::cross/112 build mcpp cross migrated (batch 2); script removed 113_scanner_mcpp_dir_prune.sh modules merge e2e/modules 114_run_scan_scope.sh run merge e2e/run 115_manifest_syntax_aot_brace.sh build merge e2e/build @@ -133,7 +133,7 @@ script subsystem class destination note 122_run_member.sh workspace merge e2e/workspace 123_same_named_main_across_dep.sh build merge e2e/build 124_gen_source_out_of_root_object_path.sh actions merge e2e/actions -125_build_mcpp_dep_dir.sh build-program merge e2e/build-program +125_build_mcpp_dep_dir.sh build-program merge e2e/build-program::deps/125 build mcpp dep dir migrated (batch 2); script removed 126_default_features_opt_out.sh features merge e2e/features 127_transitive_default_features_optout.sh features merge e2e/features 128_feature_forwarding.sh features merge e2e/features @@ -146,12 +146,12 @@ script subsystem class destination note 136_aarch64_zero_libc_target.sh baremetal merge e2e/baremetal 137_x86_64_zero_libc_target.sh baremetal merge e2e/baremetal 138_feature_sources_gate_vs_provider.sh features merge e2e/features -139_build_program_advisory.sh build-program merge e2e/build-program +139_build_program_advisory.sh build-program merge e2e/build-program::protocol/139 build program advisory migrated (batch 2); script removed 141_include_dirs_after.sh build merge e2e/build 142_generated_files_incremental.sh actions merge e2e/actions -143_build_mcpp_source_directive.sh build-program merge e2e/build-program -144_build_mcpp_include_dir.sh build-program merge e2e/build-program -145_root_build_mcpp_dep_dirs.sh build-program merge e2e/build-program +143_build_mcpp_source_directive.sh build-program merge e2e/build-program::protocol/143 build mcpp source directive migrated (batch 2); script removed +144_build_mcpp_include_dir.sh build-program merge e2e/build-program::protocol/144 build mcpp include dir migrated (batch 2); script removed +145_root_build_mcpp_dep_dirs.sh build-program merge e2e/build-program::deps/145 root build mcpp dep dirs migrated (batch 2); script removed 146_feature_flags.sh features merge e2e/features 147_per_os_features_xpkg.sh index merge e2e/index 148_wide_include_list.sh build merge e2e/build @@ -174,7 +174,7 @@ script subsystem class destination note 165_bare_name_cross_namespace_wire_address.sh deps merge e2e/deps 166_run_env_no_private_glibc.sh run merge e2e/run 167_build_defines_module_scan.sh modules merge e2e/modules -168_build_mcpp_musl_host_static.sh build-program merge e2e/build-program +168_build_mcpp_musl_host_static.sh build-program merge e2e/build-program::cross/168 build mcpp musl host static migrated (batch 2); script kept: .github/workflows/ci-aarch64-fresh-install.yml runs it by path 169_semver_project_index.sh index merge e2e/index 170_bmi_staging_no_cascade.sh modules merge e2e/modules 171_bmi_staging_locked_dest.sh modules merge e2e/modules @@ -188,21 +188,21 @@ script subsystem class destination note 178_test_observability.sh test merge e2e/test::streams/178 test observability migrated (batch 1); script removed 179_spaced_paths.sh build merge e2e/build 180_msvc_build_mcpp.sh windows merge e2e/windows -181_build_mcpp_import_std.sh build-program merge e2e/build-program +181_build_mcpp_import_std.sh build-program merge e2e/build-program::protocol/181 build mcpp import std migrated (batch 2); script removed 182_windows_no_msvc_fallback.sh windows merge e2e/windows 183_cross_no_relink.sh windows merge e2e/windows 183_cxx_runtime_contract.sh runtime merge e2e/runtime 184_build_cache_object_layout.sh cache merge e2e/cache 185_index_floor_degrades.sh index merge e2e/index -186_build_mcpp_protocol_and_bound.sh build-program merge e2e/build-program -187_dep_host_tool.sh build-program merge e2e/build-program +186_build_mcpp_protocol_and_bound.sh build-program merge e2e/build-program::protocol/186 build mcpp protocol and bound migrated (batch 2); script removed +187_dep_host_tool.sh build-program merge e2e/build-program::hosttools/187 dep host tool migrated (batch 2); script removed 188_build_actions.sh actions merge e2e/actions -189_host_module_rules.sh build-program merge e2e/build-program +189_host_module_rules.sh build-program merge e2e/build-program::hosttools/189 host module rules migrated (batch 2); script removed 190_link_rspfile_newlines.sh build merge e2e/build 191_link_scale.sh build merge e2e/build 192_install_path_namespace.sh index merge e2e/index -193_provision_reexport.sh build-program merge e2e/build-program -194_build_mcpp_glob_input.sh build-program merge e2e/build-program +193_provision_reexport.sh build-program merge e2e/build-program::hosttools/193 provision reexport migrated (batch 2); script removed +194_build_mcpp_glob_input.sh build-program merge e2e/build-program::protocol/194 build mcpp glob input migrated (batch 2); script removed 195_target_cfg_feature_deps.sh features merge e2e/features 196_version_identity_and_lock.sh deps merge e2e/deps 197_windows_resources.sh windows merge e2e/windows @@ -300,10 +300,10 @@ script subsystem class destination note 306_dependency_linkage_axis.sh runtime merge e2e/runtime 307_symbol_provision.sh runtime merge e2e/runtime 308_dependency_required_features.sh features merge e2e/features -309_host_module_identity.sh build-program merge e2e/build-program -310_build_dependencies.sh build-program merge e2e/build-program -311_rule_build_dependencies.sh build-program merge e2e/build-program -312_build_rules_example.sh build-program merge e2e/build-program +309_host_module_identity.sh build-program merge e2e/build-program::hosttools/309 host module identity migrated (batch 2); script removed +310_build_dependencies.sh build-program merge e2e/build-program::deps/310 build dependencies migrated (batch 2); script removed +311_rule_build_dependencies.sh build-program merge e2e/build-program::deps/311 rule build dependencies migrated (batch 2); script removed +312_build_rules_example.sh build-program merge e2e/build-program::hosttools/312 build rules example migrated (batch 2); script removed 313_check_stamp_on_every_platform.sh actions merge e2e/actions 314_dependency_action_generated_header.sh actions merge e2e/actions 315_blocking_check_gates_compilation.sh actions merge e2e/actions @@ -322,18 +322,18 @@ script subsystem class destination note 331_xlings_workspace_one_table.sh index merge e2e/index 332_cortex_m_builds_and_boots.sh baremetal keep keep emulator: boots the Cortex-M image in qemu-system-arm 333_named_runners_locked_and_sbom.sh run merge e2e/run -334_dependency_declared_tool_is_found.sh build-program merge e2e/build-program +334_dependency_declared_tool_is_found.sh build-program merge e2e/build-program::deps/334 dependency declared tool is found migrated (batch 2); script removed 335_xlings_tool_tiers.sh index merge e2e/index 336_armv7a_builds_and_boots.sh baremetal keep keep emulator: boots the armv7-a image in qemu-system-arm 337_run_takes_features_and_profile.sh run merge e2e/run 338_cortex_m_picolibc_sysroot.sh baremetal keep keep emulator: boots the Cortex-M picolibc image in qemu-system-arm 339_run_reports_the_program_status.sh run merge e2e/run 340_no_orphan_survives_a_killed_mcpp.sh cli merge e2e/cli -600_accel_variant_selection.sh build-program merge e2e/build-program +600_accel_variant_selection.sh build-program merge e2e/build-program::accel/600 accel variant selection migrated (batch 2); script removed 601_exclusive_capability.sh features merge e2e/features 603_version_floor.sh deps merge e2e/deps 604_offline_skips_sandbox_bootstrap.sh self merge e2e/self -605_probe_channel_and_accel_reach_the_build_program.sh build-program merge e2e/build-program +605_probe_channel_and_accel_reach_the_build_program.sh build-program merge e2e/build-program::accel/605 probe channel and accel reach the build program migrated (batch 2); script removed 606_constrained_source_globs_narrow_to_the_build.sh actions merge e2e/actions 607_chained_actions_form_the_device_link.sh actions merge e2e/actions 608_object_actions_reach_a_static_library.sh actions merge e2e/actions @@ -341,28 +341,28 @@ script subsystem class destination note 609_shader_sources_are_device_sources.sh actions merge e2e/actions 610_feature_controlled_rule_collection.sh actions merge e2e/actions 611_variant_switch_is_not_replayed.sh actions merge e2e/actions -612_build_program_file_input_fast_path.sh build-program merge e2e/build-program +612_build_program_file_input_fast_path.sh build-program merge e2e/build-program::protocol/612 build program file input fast path migrated (batch 2); script removed 613_sycl_sources_are_device_sources.sh actions merge e2e/actions 614_feature_xlings_reaches_xpkg_dir.sh index merge e2e/index 615_feature_deps_shared_library_is_linked.sh features merge e2e/features 616_examples_and_their_index_agree.sh repo sink unit/test_repo_consistency answers a parse/selection rule without a build: examples vs their index -617_build_program_knows_the_cxx_stdlib.sh build-program merge e2e/build-program -618_dependency_feature_tools_reach_its_build_program.sh build-program merge e2e/build-program +617_build_program_knows_the_cxx_stdlib.sh build-program merge e2e/build-program::protocol/617 build program knows the cxx stdlib migrated (batch 2); script removed +618_dependency_feature_tools_reach_its_build_program.sh build-program merge e2e/build-program::deps/618 dependency feature tools reach its build program migrated (batch 2); script removed 619_a_tool_may_not_be_conditioned_on_a_layer.sh actions merge e2e/actions 620_link_flag_reaches_the_link_line.sh build merge e2e/build 621_exports_narrows_the_symbol_surface.sh runtime merge e2e/runtime -622_rule_declared_payload_reaches_the_consumers_build_program.sh build-program merge e2e/build-program +622_rule_declared_payload_reaches_the_consumers_build_program.sh build-program merge e2e/build-program::deps/622 rule declared payload reaches the consumers build program migrated (batch 2); script removed 623_device_sources_must_reach_an_action.sh actions merge e2e/actions -624_a_missing_host_module_names_the_key.sh build-program merge e2e/build-program -625_a_payload_can_be_gated_on_the_accelerator.sh build-program merge e2e/build-program -626_naming_a_subset_of_backends_is_not_a_mismatch.sh build-program merge e2e/build-program +624_a_missing_host_module_names_the_key.sh build-program merge e2e/build-program::hosttools/624 a missing host module names the key migrated (batch 2); script removed +625_a_payload_can_be_gated_on_the_accelerator.sh build-program merge e2e/build-program::accel/625 a payload can be gated on the accelerator migrated (batch 2); script removed +626_naming_a_subset_of_backends_is_not_a_mismatch.sh build-program merge e2e/build-program::accel/626 naming a subset of backends is not a mismatch migrated (batch 2); script removed 627_one_package_one_version.sh deps merge e2e/deps 628_a_pin_below_a_stated_floor_is_refused.sh deps merge e2e/deps 629_a_version_range_is_answered_by_xpkg_dir.sh deps merge e2e/deps 630_a_rule_brings_its_own_environment.sh actions merge e2e/actions 631_action_depfile_tracks_a_runtime_discovered_include.sh actions merge e2e/actions -632_a_build_program_is_told_its_package_name.sh build-program merge e2e/build-program -633_host_modules_are_ordered_by_what_they_import.sh build-program merge e2e/build-program +632_a_build_program_is_told_its_package_name.sh build-program merge e2e/build-program::protocol/632 a build program is told its package name migrated (batch 2); script removed +633_host_modules_are_ordered_by_what_they_import.sh build-program merge e2e/build-program::hosttools/633 host modules are ordered by what they import migrated (batch 2); script removed 634_a_private_module_fragment_is_not_a_partition.sh modules merge e2e/modules 635_a_byte_order_mark_does_not_hide_a_module.sh modules merge e2e/modules 636_a_module_extension_file_need_not_provide_a_module.sh modules merge e2e/modules @@ -380,7 +380,7 @@ script subsystem class destination note 648_an_install_hook_sees_the_build_target.sh actions merge e2e/actions 649_pack_carries_runtime_deploy_files.sh pack merge e2e/pack 650_wasm_row_names_its_launcher.sh web merge e2e/web -651_a_build_program_deploys_what_it_generated.sh build-program merge e2e/build-program +651_a_build_program_deploys_what_it_generated.sh build-program merge e2e/build-program::protocol/651 a build program deploys what it generated migrated (batch 2); script removed 652_an_application_is_a_kind.sh pack merge e2e/pack 652b_an_application_on_android_is_a_shared_library.sh android merge e2e/android 653_exceptions_is_the_second_abi_member.sh target merge e2e/target @@ -394,7 +394,7 @@ script subsystem class destination note 662_pack_stages_declared_files_before_the_closure.sh pack merge e2e/pack 663_a_graph_libcxx_over_the_payloads_c_library.sh toolchain merge e2e/toolchain 664_a_universal_apk_is_two_legs_in_one_tree.sh android merge e2e/android -665_a_host_tool_follows_its_source.sh build-program merge e2e/build-program +665_a_host_tool_follows_its_source.sh build-program merge e2e/build-program::hosttools/665 a host tool follows its source migrated (batch 2); script removed 666_a_macho_program_reaches_a_dispatched_format_with_its_tree.sh macos merge e2e/macos 667_an_android_pack_stages_its_closure.sh android merge e2e/android 668_a_macho_program_is_staged_with_its_closure.sh macos merge e2e/macos @@ -411,7 +411,7 @@ script subsystem class destination note 680_a_dependency_floor_on_the_android_api_level.sh android merge e2e/android 681_index_repos_reach_an_existing_home.sh self merge e2e/self 682_why_deps_prints_the_resolved_graph.sh deps merge e2e/deps -683_a_build_program_reads_the_payload_pkg_config_view.sh build-program merge e2e/build-program +683_a_build_program_reads_the_payload_pkg_config_view.sh build-program merge e2e/build-program::protocol/683 a build program reads the payload pkg config view migrated (batch 2); script removed 684_a_runner_supplied_twice_is_refused.sh run merge e2e/run 685_a_nested_test_loads_a_graph_shared_library.sh runtime merge e2e/runtime 686_an_activated_xlings_subos_does_not_redirect_the_registry.sh self merge e2e/self @@ -420,9 +420,9 @@ script subsystem class destination note 689_pack_takes_features.sh pack merge e2e/pack 690_a_shared_library_over_a_graph_cxx_runtime.sh runtime merge e2e/runtime 692_a_package_states_its_default_link_form.sh runtime merge e2e/runtime -693_the_root_build_program_reads_dependency_link_forms.sh build-program merge e2e/build-program +693_the_root_build_program_reads_dependency_link_forms.sh build-program merge e2e/build-program::deps/693 the root build program reads dependency link forms migrated (batch 2); script removed 696_a_cxx_layer_provider_keeps_its_own_standard.sh toolchain merge e2e/toolchain -698_a_tool_source_under_its_dependency_has_a_bounded_address.sh build-program merge e2e/build-program +698_a_tool_source_under_its_dependency_has_a_bounded_address.sh build-program merge e2e/build-program::hosttools/698 a tool source under its dependency has a bounded address migrated (batch 2); script removed 700_a_program_over_a_cxx_shared_library_has_one_cxx_runtime.sh runtime merge e2e/runtime 701_symbol_provision_does_not_report_what_the_build_shares_by_construction.sh runtime merge e2e/runtime 702_a_static_package_under_a_shared_library_is_linked_into_it.sh runtime merge e2e/runtime @@ -431,10 +431,10 @@ script subsystem class destination note 705_cxx_identity_across_pe_images.sh windows keep keep a measurement leg (prints a table, asserts nothing) 710_a_forward_reaches_every_dependency_table.sh features merge e2e/features 711_a_feature_deps_restatement_names_one_source.sh features merge e2e/features -712_a_tool_package_contributes_nothing_to_its_consumer.sh build-program merge e2e/build-program +712_a_tool_package_contributes_nothing_to_its_consumer.sh build-program merge e2e/build-program::hosttools/712 a tool package contributes nothing to its consumer migrated (batch 2); script removed 713_a_git_dependency_selects_a_repository_member.sh deps merge e2e/deps 714_features_names_a_dependency_feature.sh features merge e2e/features -720_the_root_build_program_reads_the_resolved_graph.sh build-program merge e2e/build-program +720_the_root_build_program_reads_the_resolved_graph.sh build-program merge e2e/build-program::deps/720 the root build program reads the resolved graph migrated (batch 2); script removed 721_an_android_row_links_on_a_macos_host.sh macos merge e2e/macos 722_pack_strips_what_the_graph_built.sh pack merge e2e/pack 723_pack_reports_what_it_produced.sh pack merge e2e/pack @@ -461,11 +461,11 @@ script subsystem class destination note 772_a_published_member_is_self_contained.sh workspace merge e2e/workspace 773_commands_outside_the_build_read_the_effective_manifest.sh workspace merge e2e/workspace 774_an_index_member_inherits_its_archive_workspace.sh workspace merge e2e/workspace -775_a_host_tool_merges_its_conditional_sections_once.sh build-program merge e2e/build-program +775_a_host_tool_merges_its_conditional_sections_once.sh build-program merge e2e/build-program::hosttools/775 a host tool merges its conditional sections once migrated (batch 2); script removed 776_a_path_with_no_utf8_spelling_is_named.sh build merge e2e/build 777_c_standard_applies_to_the_package_that_declares_it.sh build merge e2e/build 778_a_graph_link_searches_no_host_directory.sh target merge e2e/target -779_a_build_program_declares_a_runtime_search_dir.sh build-program merge e2e/build-program +779_a_build_program_declares_a_runtime_search_dir.sh build-program merge e2e/build-program::deps/779 a build program declares a runtime search dir migrated (batch 2); script removed 780_a_passing_check_moves_its_stamp_past_a_changed_input.sh actions merge e2e/actions 781_deleted_root_cdb_is_restored.sh cdb merge e2e/cdb 782_foreign_root_cdb_entries_are_replaced.sh cdb merge e2e/cdb @@ -486,11 +486,11 @@ script subsystem class destination note 797_a_windows_program_finds_its_dlls_beside_it.sh windows keep keep emulator: runs the PE program under wine 798_a_rule_applies_through_a_source_it_claims.sh actions merge e2e/actions 799_an_action_runs_with_its_env_and_cwd.sh actions merge e2e/actions -800_a_feature_provides_its_host_tools.sh build-program merge e2e/build-program +800_a_feature_provides_its_host_tools.sh build-program merge e2e/build-program::hosttools/800 a feature provides its host tools migrated (batch 2); script removed 801_a_dependency_program_is_shipped_with_the_consumer.sh pack merge e2e/pack -802_a_host_build_applies_its_host_row.sh build-program merge e2e/build-program +802_a_host_build_applies_its_host_row.sh build-program merge e2e/build-program::hosttools/802 a host build applies its host row migrated (batch 2); script removed 803_a_recorded_payload_that_is_gone_is_refused_offline.sh index merge e2e/index -804_a_path_host_tool_builds_with_its_chosen_toolchain.sh build-program merge e2e/build-program +804_a_path_host_tool_builds_with_its_chosen_toolchain.sh build-program merge e2e/build-program::hosttools/804 a path host tool builds with its chosen toolchain migrated (batch 2); script kept: .github/workflows/ci-aarch64-fresh-install.yml runs it by path 805_rooted_workspace_reaches_its_own_path_dependency.sh workspace merge e2e/workspace 806_dash_p_resolves_the_package_first.sh workspace merge e2e/workspace 807_a_lib_root_imports_its_own_package.sh modules merge e2e/modules @@ -509,14 +509,14 @@ script subsystem class destination note 820_a_windows_program_takes_its_runtime_from_one_resolver.sh windows merge e2e/windows 821_an_edge_states_its_advice_on_both_paths.sh deps merge e2e/deps 822_a_package_states_the_oldest_mcpp_it_supports.sh deps merge e2e/deps -823_mcpp_core_and_mcpp_name_one_interface.sh build-program merge e2e/build-program -824_a_build_program_reads_the_resolved_toolchain.sh build-program merge e2e/build-program +823_mcpp_core_and_mcpp_name_one_interface.sh build-program merge e2e/build-program::protocol/823 mcpp core and mcpp name one interface migrated (batch 2); script removed +824_a_build_program_reads_the_resolved_toolchain.sh build-program merge e2e/build-program::protocol/824 a build program reads the resolved toolchain migrated (batch 2); script removed 825_a_build_program_reads_the_msvc_toolset.sh windows merge e2e/windows -826_plugin_diagnostics_features_and_names.sh build-program merge e2e/build-program +826_plugin_diagnostics_features_and_names.sh build-program merge e2e/build-program::hosttools/826 plugin diagnostics features and names migrated (batch 2); script removed 827_many_placements_run_as_one_process.sh actions merge e2e/actions 828_a_library_states_its_interface.sh pack merge e2e/pack 829_pack_takes_a_member.sh pack merge e2e/pack -831_a_path_dependency_host_module_outside_src.sh build-program merge e2e/build-program +831_a_path_dependency_host_module_outside_src.sh build-program merge e2e/build-program::hosttools/831 a path dependency host module outside src migrated (batch 2); script removed 832_the_fast_path_resumes_after_an_edit.sh cache merge e2e/cache 833_a_workspace_is_one_graph_per_configuration.sh workspace merge e2e/workspace 834_a_workspace_plans_once_and_replays_in_one_check.sh workspace merge e2e/workspace @@ -544,7 +544,7 @@ script subsystem class destination note 856_a_test_over_several_members_continues_past_a_failing_one.sh test merge e2e/test::workspace/856 a test over several members continues past a failing one migrated (batch 1); script removed 857_a_host_module_several_members_import_is_compiled_once.sh modules merge e2e/modules 858_the_engines_module_is_compiled_once_per_home.sh modules merge e2e/modules -859_a_host_module_entry_is_reused_while_its_inputs_agree.sh build-program merge e2e/build-program +859_a_host_module_entry_is_reused_while_its_inputs_agree.sh build-program merge e2e/build-program::hosttools/859 a host module entry is reused while its inputs agree migrated (batch 2); script removed 860_a_workspaces_programs_compile_at_the_same_time.sh workspace merge e2e/workspace 861_concurrent_compiles_plan_what_serial_compiles_plan.sh build merge e2e/build 862_run_q_writes_exactly_the_programs_stdout.sh run merge e2e/run @@ -558,11 +558,11 @@ script subsystem class destination note 870_a_pack_over_several_members_resolves_and_fails_per_member.sh pack merge e2e/pack 871_a_pack_states_its_build_and_every_drive_takes_the_job_count.sh pack merge e2e/pack 872_a_member_compiles_once_whatever_the_selection.sh workspace merge e2e/workspace -873_payload_overrides.sh build-program merge e2e/build-program -874_payload_on_request.sh build-program merge e2e/build-program +873_payload_overrides.sh build-program merge e2e/build-program::deps/873 payload overrides migrated (batch 2); script removed +874_payload_on_request.sh build-program merge e2e/build-program::deps/874 payload on request migrated (batch 2); script removed 875_toolchain_by_path.sh toolchain merge e2e/toolchain 876_toolchain_phase.sh toolchain merge e2e/toolchain -877_why_sources.sh build-program merge e2e/build-program +877_why_sources.sh build-program merge e2e/build-program::deps/877 why sources migrated (batch 2); script removed 878_an_upgraded_engine_does_not_replay_the_old_graph.sh cache merge e2e/cache 879_a_providers_module_extension_reaches_the_fast_path.sh cache merge e2e/cache 880_a_members_executable_keeps_its_own_implementation.sh workspace merge e2e/workspace @@ -587,15 +587,15 @@ script subsystem class destination note 899_toolchain_flag_replacing_a_declared_toolchain_says_so.sh toolchain merge e2e/toolchain 900_publish_writes_the_workspace_layer_into_the_member.sh workspace merge e2e/workspace 901_the_toolchain_library_path_stays_with_the_tools.sh toolchain keep keep container: a host whose /bin/sh is not the toolchain's (termux-style loader) -902_a_build_dependency_library_is_compiled_as_its_package.sh build-program merge e2e/build-program -903_one_package_in_both_tables_has_two_instances.sh build-program merge e2e/build-program -904_a_build_dependency_feature_stays_out_of_the_target.sh build-program merge e2e/build-program -905_a_build_program_links_its_libraries_whole.sh build-program merge e2e/build-program -906_a_build_program_is_compiled_when_its_graph_changes.sh build-program merge e2e/build-program +902_a_build_dependency_library_is_compiled_as_its_package.sh build-program merge e2e/build-program::deps/902 a build dependency library is compiled as its package migrated (batch 2); script removed +903_one_package_in_both_tables_has_two_instances.sh build-program merge e2e/build-program::graph/903 one package in both tables has two instances migrated (batch 2); script removed +904_a_build_dependency_feature_stays_out_of_the_target.sh build-program merge e2e/build-program::graph/904 a build dependency feature stays out of the target migrated (batch 2); script removed +905_a_build_program_links_its_libraries_whole.sh build-program merge e2e/build-program::graph/905 a build program links its libraries whole migrated (batch 2); script removed +906_a_build_program_is_compiled_when_its_graph_changes.sh build-program merge e2e/build-program::graph/906 a build program is compiled when its graph changes migrated (batch 2); script removed 907_the_library_interface_is_stated_once.sh build merge e2e/build -908_what_a_build_dependency_asks_for.sh build-program merge e2e/build-program +908_what_a_build_dependency_asks_for.sh build-program merge e2e/build-program::graph/908 what a build dependency asks for migrated (batch 2); script removed 909_deprecated_readings_are_registered_and_kept.sh build merge e2e/build -910_a_build_plugin_is_a_package_of_the_build_program.sh build-program merge e2e/build-program +910_a_build_plugin_is_a_package_of_the_build_program.sh build-program merge e2e/build-program::graph/910 a build plugin is a package of the build program migrated (batch 2); script removed 911_a_workspace_states_each_table_once.sh workspace merge e2e/workspace 912_two_configuration_groups_share_the_build_programs_graph.sh workspace merge e2e/workspace 913_the_build_database_describes_the_build_programs.sh cdb merge e2e/cdb @@ -643,7 +643,7 @@ script subsystem class destination note 955_the_openkal_set_is_one_tree_of_every_architecture.sh openkal merge e2e/openkal 956_a_single_file_runs_without_unpacking.sh openkal merge e2e/openkal 957_packages_merge_and_describe_themselves.sh openkal merge e2e/openkal -958_a_build_program_runs_again_when_its_output_is_gone.sh build-program merge e2e/build-program +958_a_build_program_runs_again_when_its_output_is_gone.sh build-program merge e2e/build-program::protocol/958 a build program runs again when its output is gone migrated (batch 2); script removed 959_a_package_is_keyed_by_every_module_it_imports.sh cache merge e2e/cache 960_an_image_of_another_architecture_runs_through_its_execution_host.sh openkal merge e2e/openkal 961_a_single_file_carries_the_image_in_a_program_of_its_architecture.sh openkal merge e2e/openkal @@ -667,7 +667,7 @@ script subsystem class destination note 994_the_contract_names_the_target_side.sh openkal merge e2e/openkal 995_the_windows_launcher_builds_over_any_platform_release.sh openkal merge e2e/openkal 996_an_ok_image_waits_for_a_prepare_action.sh openkal merge e2e/openkal -997_a_members_dev_dependencies_stay_out_of_a_host_tool.sh build-program merge e2e/build-program +997_a_members_dev_dependencies_stay_out_of_a_host_tool.sh build-program merge e2e/build-program::hosttools/997 a members dev dependencies stay out of a host tool migrated (batch 2); script removed 998_a_build_keeps_its_bytes_once.sh cache merge e2e/cache 999_debug_information_is_stored_once.sh cache merge e2e/cache 1000_a_configuration_keeps_one_directory.sh cache merge e2e/cache @@ -681,7 +681,7 @@ script subsystem class destination note 1008_a_provided_format_is_told_the_compression.sh pack merge e2e/pack 1009_cache_gc_asks_the_runner.sh openkal merge e2e/openkal 1010_the_std_module_takes_the_platforms_flags.sh toolchain merge e2e/toolchain -1011_accelerator_requests_are_complete_and_canonical.sh build-program merge e2e/build-program +1011_accelerator_requests_are_complete_and_canonical.sh build-program merge e2e/build-program::accel/1011 accelerator requests are complete and canonical migrated (batch 2); script removed 1012_capabilities_bind_in_each_consumer_context.sh features merge e2e/features 1013_optional_action_progress_preserves_outputs.sh actions merge e2e/actions 1014_host_only_layers_do_not_select_the_target_abi.sh target merge e2e/target @@ -706,7 +706,7 @@ script subsystem class destination note 1033_an_ok_image_runs_on_the_system_that_built_it.sh openkal merge e2e/openkal 1034_a_workspace_members_test_finds_its_programs.sh test merge e2e/test::workspace/1034 a workspace members test finds its programs migrated (batch 1); script removed 1035_a_test_program_reports_its_cases.sh test merge e2e/test::protocol/1035 a test program reports its cases migrated (batch 1); script removed -1036_a_context_states_the_engine_values_in_process.sh build-program merge e2e/build-program +1036_a_context_states_the_engine_values_in_process.sh build-program merge e2e/build-program::protocol/1036 a context states the engine values in process migrated (batch 2); script removed 1037_a_skeleton_tests_with_mtest_and_a_test_needs_no_main.sh test merge e2e/test::skeleton/1037 a skeleton tests with mtest and a test needs no main migrated (batch 1); script removed 1038_test_programs_of_a_directory_and_the_tests_a_change_affects.sh test merge e2e/test::affected/1038 test programs of a directory and the tests a change affects migrated (batch 1); script removed 1039_two_builds_that_find_the_std_module_missing_compile_it_once.sh modules merge e2e/modules @@ -721,7 +721,7 @@ script subsystem class destination note 1048_a_git_reference_follows_the_default_branch.sh deps merge e2e/deps 1049_a_platform_skeleton_is_made_and_its_statement_read_as_the_engine_reads_it.sh openkal merge e2e/openkal 1050_a_test_run_is_kept_in_the_mtest_store.sh test merge e2e/test::archive/1050 a test run is kept in the mtest store migrated (batch 1); script removed -1051_a_cross_targets_toolchain_section_is_not_the_build_programs.sh build-program merge e2e/build-program +1051_a_cross_targets_toolchain_section_is_not_the_build_programs.sh build-program merge e2e/build-program::cross/1051 a cross targets toolchain section is not the build programs migrated (batch 2); script removed 1052_a_framework_that_uses_the_package_under_test_is_built_against_it.sh test merge e2e/test::frameworks/1052 a framework that uses the package under test is built against it migrated (batch 1); script removed 1053_a_mach_o_images_c_library_is_up_before_its_initialisers.sh macos merge e2e/macos 1054_programs_that_share_an_entry_keep_their_own_flags.sh build merge e2e/build diff --git a/tests/e2e/build-program/accel.cpp b/tests/e2e/build-program/accel.cpp new file mode 100644 index 000000000..b368dcd60 --- /dev/null +++ b/tests/e2e/build-program/accel.cpp @@ -0,0 +1,213 @@ +// The accelerator dimension and the probe channel: which prebuilt artifact a build may take, what +// a build program is told of the accelerator and how its stated facts and floors are compared +// (formerly tests/e2e/600, 605). +import std; +import mcpplibs.mtest; +import mcpp.test.support; + +using namespace mcpplibs::mtest; +namespace s = mcpp::test::support; +namespace fs = std::filesystem; + +namespace { + +// `mcpp new ` with only src/main.cpp (`int main`) and `manifest`. +s::project scaffold_plain(std::string_view name, const std::string& manifest) { + fs::remove_all(temp_dir() / "new" / name); + auto p = s::project::scaffold(name); + p.write("mcpp.toml", manifest); + p.write("src/main.cpp", "int main() { return 0; }\n"); + for (const auto& m : s::glob(p.at("src"), "*.cppm")) fs::remove(m); + return p; +} + +E2E accel{"accel", [] { + "600 accel variant selection"_test[tag{"requires:elf"}] = [] { + // gpuonly publishes one artifact, for sm_90f: an sm_86 build is refused naming the + // dimension. gpukit publishes a CPU-only artifact first: the same build takes it (the + // order that keeps an mcpp predating the field working). + s::needs({"elf"}); + const auto root = temp_dir() / "new"; + constexpr std::string_view triple = "x86_64-linux-gnu"; + auto artifact = [&](std::string_view name, bool device) { + return std::format("[[runtime.artifacts]]\nrole = \"static-library\"\npath = \"lib/lib{}.a\"\nprovenance = \"mcpp-pack/1\"\nabi = \"{}\"\n{}", + name, triple, device ? "accel = \"cuda12.8+{sm_90f}\"\n" : ""); + }; + auto make_pkg = [&](std::string_view name, const std::string& tables) { + s::project pkg{root / name, {}}; + pkg.write(std::format("lib/lib{}.a", name), ""); + fs::create_directories(pkg.at("include")); + pkg.write("mcpp.toml", std::format("[package]\nname = \"{}\"\nversion = \"0.1.0\"\n[language]\nstandard = \"c++23\"\n", name) + tables); + }; + make_pkg("gpuonly", artifact("gpuonly", true)); + make_pkg("gpukit", artifact("gpukit", false) + "\n" + artifact("gpukit", true)); + auto consumer = [&](std::string_view dep) { + return scaffold_plain("app", std::format("[package]\nname = \"app\"\nversion = \"0.1.0\"\n[language]\nstandard = \"c++23\"\n[dependencies]\n{0} = {{ path = \"../{0}\" }}\n", dep)); + }; + + // 1. A device-only package refuses another architecture, naming both and the remedy that + // can work (pinning [toolchain] cannot change the GPU architecture). + auto app = consumer("gpuonly"); + auto r1 = app.mcpp({"build", "--accel", "cuda12.8+{sm_86}"}); + const auto out = s::text(r1); + expect(r1.started && r1.status != 0) << "sm_86 accepted an sm_90f-only artifact"; + expect(e2e::contains(out, "accel")) << "refusal does not name the accel dimension"; + expect(e2e::contains(out, "sm_90f")) << "refusal does not say what the artifact has"; + expect(e2e::contains(out, "sm_86")) << "refusal does not say what this build asked for"; + expect(e2e::contains(out, "--no-accel")) << "refusal offers no remedy for the device axis"; + expect(e2e::contains(out, "accel=cuda12.8+{sm_90f}")) << "the published listing hides the device dimension"; + // 2. The architecture it was built for, and 3. no accelerator at all. + expect(e2e::succeeded(app.mcpp({"build", "--accel", "cuda12.8+{sm_90}"}))) << "sm_90 refused by an sm_90f artifact"; + expect(e2e::succeeded(app.mcpp({"build", "--no-accel"}))) << "--no-accel refused"; + // 4. CPU first: an unmatched device build falls back. + app = consumer("gpukit"); + expect(e2e::succeeded(app.mcpp({"build", "--accel", "cuda12.8+{sm_86}"}))) << "CPU-only artifact listed first did not accept sm_86"; + }; + + "605 probe channel and accel reach the build program"_test[tag{"requires:gcc"}] = [] { + // The package measures (mcpp::fact), states what it needs (mcpp::floor), and the engine + // compares after the root's build program has run: an unmet floor is refused naming both + // values, under the reason `version-floor-unmet`. The same program reads MCPP_ACCEL, and + // `cfg(accelerator = ...)` is fed from the same value. No vendor is named: `widget`. + s::needs({"gcc"}); + auto p = scaffold_plain("probe", ""); + p.write("src/main.cpp", "#ifndef WIDGET_ON\n#error \"cfg(accelerator = \\\"widget\\\") did not match, so WIDGET_ON is missing\"\n#endif\nint main() { return 0; }\n"); + auto write_manifest = [&](std::string_view floor) { + p.write("mcpp.toml", R"cpp([package] +name = "probe" +version = "0.1.0" + +[language] +standard = "c++23" + +[build] +accel = "widget9+{w1,w2}" + +[target.'cfg(accelerator = "widget")'.build] +defines = ["WIDGET_ON"] + +[targets.probe] +kind = "bin" +main = "src/main.cpp" +)cpp"); + p.write("build.mcpp", std::format(R"cpp(import std; +import mcpp; +int main() {{ + mcpp::warning((std::string("accel=[") + mcpp::accel() + "]").c_str()); + mcpp::fact("widget.driver", "1.2"); + mcpp::floor("{}"); + return 0; +}} +)cpp", floor)); + }; + // One: the floor is above the fact. + write_manifest("widget.driver >= 2.0"); + auto refused = p.mcpp({"build"}); + expect(refused.started && refused.status != 0) << "a floor above the stated fact was accepted"; + expect(e2e::contains(s::text(refused), "widget.driver")) << "refusal does not name the quantity"; + expect(e2e::contains(s::text(refused), "2.0")) << "refusal does not say what was needed"; + expect(e2e::contains(s::text(refused), "1.2")) << "refusal does not say what is there"; + // `why toolchain` runs the same prepare, build program included. + auto why = s::json(p.mcpp({"why", "toolchain", "--format", "json"}).out); + const auto reason = why.contains("data") && why["data"].is_object() ? why["data"].value("reason", std::string("-")) : std::string("-"); + expect(eq(reason, std::string("version-floor-unmet"))); + // Two: a met floor builds; three: the program saw the resolved accel, canonically spelt. + write_manifest("widget.driver >= 1.0"); + fs::last_write_time(p.at("src/main.cpp"), fs::file_time_type::clock::now()); + auto ok = p.mcpp({"build"}); + expect(fatal(e2e::succeeded(ok))) << "a met floor was refused"; + expect(e2e::contains(s::text(ok), "accel=[widget9+{w1,w2}]")) << "MCPP_ACCEL did not carry the resolved accel"; + // Four: --no-accel empties the layer (main.cpp's #error fires) and the variable (not the + // display spelling `(none)`). + auto none = p.mcpp({"build", "--no-accel"}); + expect(none.started && none.status != 0) << "cfg(accelerator) matched under --no-accel"; + expect(e2e::contains(s::text(none), "WIDGET_ON is missing")) << "the build failed for another reason than the layer"; + expect(e2e::contains(s::text(none), "accel=[]")) << "MCPP_ACCEL is not empty under --no-accel"; + }; + + "625 a payload can be gated on the accelerator"_test[tag{"requires:elf"}][tag{"requires:gcc"}] = [] { + // `[target.'cfg(accelerator = ...)'.xlings]`: the accel is an input read before + // provisioning, so a payload gated on it is installed for a device build and not for a + // CPU-only one. Both legs; the second is the point. A fresh home and shaderc, as in 618. + s::needs({"elf", "gcc"}); + s::project p{{"src/main.cpp", "int main() { return 0; }\n"}, + {"mcpp.toml", "[package]\nname = \"gated\"\nversion = \"0.1.0\"\naccelerators = [\"vulkan\"]\n\n[targets.gated]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n\n" + "[target.'cfg(accelerator = \"vulkan\")'.xlings.workspace]\n\"xim:shaderc\" = \"2026.3\"\n"}}; + const s::options fresh{.where = s::home::fresh}; + const auto store = s::case_home() / "registry/data/xpkgs/xim-x-shaderc"; + expect(fatal(e2e::succeeded(p.mcpp({"build"}, fresh)))) << "the CPU-only build was refused"; + expect(!fs::is_directory(store)) << "a CPU-only build installed the device payload"; + expect(fatal(e2e::succeeded(p.mcpp({"build", "--accel", "vulkan1.2"}, fresh)))) << "the device build was refused"; + expect(fs::is_directory(store)) << "the device build did not install the payload its predicate names"; + }; + + "626 naming a subset of backends is not a mismatch"_test[tag{"requires:elf"}][tag{"requires:gcc"}] = [] { + // A constrained glob whose backend the build never named is left out, not refused; one + // whose backend is named and architecture uncovered still is; a backend the package never + // declared is refused, naming the misspelling. + s::needs({"elf", "gcc"}); + auto mk = [](std::string_view accel) { + fs::remove_all(temp_dir() / "p"); + s::project p{temp_dir() / "p", {{"src/main.cpp", "int main() { return 0; }\n"}, + {"src/kernels/k.cu", "extern \"C\" __global__ void k() {}\n"}, + {"build.mcpp", "import std;\nimport mcpp;\nint main() { return 0; }\n"}}}; + p.write("mcpp.toml", std::format("[package]\nname = \"subset\"\nversion = \"0.1.0\"\naccelerators = [\"cuda\"]\n\n[build]\nsources = [\n \"src/*.cpp\",\n {{ glob = \"src/kernels/*.cu\", accel = \"{}\" }},\n]\n\n[targets.subset]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n", accel)); + return p; + }; + // 1. The backend is named: the glob comes through (the device-source audit sees it). + auto p = mk("cuda12.9+{sm_89}"); + auto r1 = p.mcpp({"build", "--accel", "cuda12.9+{sm_89}"}); + expect(r1.started && r1.status != 0) << "the glob did not reach the build"; + expect(e2e::contains(s::text(r1), "no action compiles")) << "expected the device-source audit"; + // 2. A different backend: left out, as --no-accel leaves it. + p = mk("cuda12.9+{sm_89}"); + expect(e2e::succeeded(p.mcpp({"build", "--accel", "vulkan1.2"}))) << "naming a different backend was refused"; + // 3. The same backend, an architecture it does not cover: refused. + p = mk("cuda12.9+{sm_89}"); + auto r3 = p.mcpp({"build", "--accel", "cuda12.9+{sm_80}"}); + expect(r3.started && r3.status != 0) << "an uncovered architecture built"; + expect(e2e::contains(s::text(r3), "does not cover")) << "expected the accel mismatch refusal"; + // 4. A backend [package] accelerators does not list: refused, naming it. + p = mk("cude12.9+{sm_89}"); + auto r4 = p.mcpp({"build", "--accel", "cuda12.9+{sm_89}"}); + expect(r4.started && r4.status != 0) << "an undeclared backend built"; + expect(e2e::contains(s::text(r4), "does not declare")) << "expected the undeclared-backend refusal"; + expect(e2e::contains(s::text(r4), "cude")) << "the refusal does not name the misspelling"; + }; + + "1011 accelerator requests are complete and canonical"_test[tag{"requires:gcc"}][tag{"requires:unix-shell"}] = [] { + // Invalid accelerator syntax does not become a successful CPU build, an implementation + // cannot occur twice with order-dependent coverage, and equivalent requests are one + // variant. (The script required python3 and did not use it.) + s::needs({"gcc", "unix-shell"}); + const s::options home{.where = s::home::inherited}; + const std::string manifest = "[package]\nname = \"accel-request\"\nversion = \"0.1.0\"\n\n[targets.accel-request]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n"; + s::project p{{"mcpp.toml", manifest}, {"src/main.cpp", "int main(){return 0;}\n"}}; + for (auto request : {"sycl+{sm_89", "sycl+{sm_89}garbage", "sycl+{sm_89,,sm_90}", "sycl+{sm_89},sycl+{sm_90}", "sycl+{sm_90},sycl+{sm_89}"}) { + auto r = p.mcpp({"build", "--accel", request}, home); + expect(r.started && r.status != 0) << "accepted malformed or repeated request: " << request; + expect(e2e::contains(s::text(r), "invalid accelerator request at byte")) << "missing syntax location"; + expect(e2e::contains(s::text(r), "--accel")) << "missing request source"; + } + p.write("mcpp.toml", manifest + "\n[build]\naccel = \"sycl+{sm_89\"\n"); + auto m = p.mcpp({"build"}, home); + expect(m.started && m.status != 0) << "accepted malformed manifest request"; + expect(e2e::contains(s::text(m), "[build] accel")) << "missing manifest field"; + p.write("mcpp.toml", manifest + "\n[build]\nsources = [{ glob = \"src/main.cpp\", accel = \"sycl+{sm_90\" }]\n"); + auto c = p.mcpp({"build", "--no-accel"}, home); + expect(c.started && c.status != 0) << "ignored malformed source constraint"; + expect(e2e::contains(s::text(c), "sources entry 'src/main.cpp'")) << "missing source constraint"; + expect(e2e::contains(s::text(c), "invalid accelerator request")) << "missing constraint syntax error"; + p.write("mcpp.toml", manifest + "\n[build]\nsources = [{ glob = \"src/main.cpp\", accel = \"sycl+{sm_90}\" }]\n"); + expect(fatal(e2e::succeeded(p.mcpp({"build", "--accel", "sycl+{sm_90,sm_89},newmodel2+{opaque}"}, home)))) << "valid open-vocabulary request did not build"; + const auto before = s::glob(p.at("target"), "*/*/build.ninja"); + expect(fatal(!before.empty())) << "no build graph was generated"; + auto again = p.mcpp({"build", "--accel", "newmodel2+{opaque},sycl+{sm_89,sm_90,sm_89}"}, home); + expect(fatal(e2e::succeeded(again))) << "equivalent request did not build"; + expect(eq(s::glob(p.at("target"), "*/*/build.ninja"), before)) << "equivalent sets allocated different variants"; + expect(s::lacks(s::text(again), "fingerprint changed")) << "equivalent sets changed the fingerprint"; + expect(e2e::succeeded(p.mcpp({"run", "--accel", "sycl+{sm_89,sm_90},newmodel2+{opaque}"}, home))) << "the canonical variant did not run"; + }; +}}; + +} // namespace diff --git a/tests/e2e/build-program/cross.cpp b/tests/e2e/build-program/cross.cpp new file mode 100644 index 000000000..1a7152d18 --- /dev/null +++ b/tests/e2e/build-program/cross.cpp @@ -0,0 +1,122 @@ +// A build program under a cross target: it is compiled for and run on the host, and told the +// target (formerly tests/e2e/112). +import std; +import mcpplibs.mtest; +import mcpp.test.support; + +using namespace mcpplibs::mtest; +namespace s = mcpp::test::support; +namespace fs = std::filesystem; + +namespace { + +E2E cross{"cross", [] { + "112 build mcpp cross"_test[tag{"requires:gcc"}][tag{"requires:mingw-cross"}] = [] { + // Under `--target x86_64-windows-gnu` build.mcpp compiles and runs on the host and sees + // MCPP_TARGET = the cross triple. + s::needs({"gcc", "mingw-cross"}); + auto p = s::project::scaffold("crossbp"); + p.write("build.mcpp", R"cpp(#include +#include +#include +static const char* env_or(const char* n) { const char* v = std::getenv(n); return v ? v : ""; } +int main() { + std::ofstream f("src/cross_gen.cpp"); + f << "extern \"C\" const char* bp_target() { return \"" << env_or("MCPP_TARGET") << "\"; }\n"; + if (!f) return 1; + std::printf("mcpp:generated=src/cross_gen.cpp\n"); + return 0; +} +)cpp"); + p.write("src/main.cpp", "import std;\nextern \"C\" const char* bp_target();\nint main() {\n" + " std::println(\"bp target = {}\", bp_target());\n return 0;\n}\n"); + p.write("mcpp.toml", "[package]\nname = \"crossbp\"\nversion = \"0.1.0\"\n"); + auto b = p.mcpp({"build", "--target", "x86_64-windows-gnu"}); + expect(fatal(e2e::succeeded(b))) << "cross build failed"; + expect(!s::contains_nocase(s::text(b), "skipped under a cross")) << "build.mcpp still skipped under cross"; + expect(e2e::contains(p.read("src/cross_gen.cpp"), "x86_64-windows-gnu")) << "MCPP_TARGET was not the cross triple"; + auto exe = s::find_file(p.at("target"), "crossbp.exe"); + expect(fatal(exe.has_value())) << "no PE artifact produced"; + // Under wine, when the machine has it, the loop is whole. + if (s::has("wine")) { + auto w = e2e::run({.argv = {"wine", exe->string()}, .env = {{"WINEDEBUG", "-all"}}}); + auto out = s::last_line(w.out); + if (out.ends_with('\r')) out.pop_back(); + expect(eq(out, std::string("bp target = x86_64-windows-gnu"))) << "unexpected wine output"; + } + + // The std module staged for a build.mcpp is the host's: a helper built against the + // target's could not run here, and the failure would be silent until it ran. + p.write("build.mcpp", R"cpp(import std; +int main() { + std::ofstream f("src/cross_gen.cpp"); + f << "extern \"C\" const char* bp_target() { return \"" + << (std::getenv("MCPP_TARGET") ? std::getenv("MCPP_TARGET") : "") + << "\"; }\n"; + if (!f) return 1; + std::println("mcpp:generated=src/cross_gen.cpp"); + return 0; +} +)cpp"); + p.remove("src/cross_gen.cpp"); + auto b2 = p.mcpp({"build", "--target", "x86_64-windows-gnu"}); + expect(fatal(e2e::succeeded(b2))) << "cross build with import std in build.mcpp failed"; + // The helper ran on the host: the file it was asked to write is there. + expect(fatal(p.exists("src/cross_gen.cpp"))) << "import-std build.mcpp did not run on the host"; + expect(e2e::contains(p.read("src/cross_gen.cpp"), "x86_64-windows-gnu")) << "MCPP_TARGET wrong under import std"; + }; + + "168 build mcpp musl host static"_test[tag{"requires:elf"}] = [] { + // Under a musl host toolchain the build program, which the system executes directly, is + // static: a dynamic musl program names /lib/ld-musl-.so.1, which no payload installs, + // and dies as an unexplained 127 (#295). The newest installed musl payload is the one a + // user gets; none installed is a skip (this case never installs ~200 MB). + s::needs({"elf"}); + auto musl = s::newest_payload("xim-x-musl-gcc", "bin/*-linux-musl-g++"); + if (!musl) + skip("no musl-gcc payload installed — not testing the musl host helper"); + auto p = s::project::scaffold("app"); + p.write("mcpp.toml", p.read("mcpp.toml") + std::format("\n[toolchain]\ndefault = \"gcc@{}-musl\"\n", *musl)); + p.write("build.mcpp", R"cpp(#include +int main() { + FILE* marker = std::fopen("helper-ran", "w"); + if (!marker) return 2; + std::fputs("ok\n", marker); + std::fclose(marker); + std::puts("mcpp:rerun-if-changed=build.mcpp"); + return 0; +} +)cpp"); + expect(fatal(e2e::succeeded(p.mcpp({"build"})))) << "musl build.mcpp helper did not run on this host"; + expect(p.exists("helper-ran")) << "build.mcpp did not run"; + const auto helper = p.at("target/.build-mcpp/build.mcpp.bin"); + expect(fatal(fs::is_regular_file(helper))) << "helper binary missing"; + // `file`'s "statically linked" and readelf's interpreter, read from the header. + const auto h = s::read_header(helper); + expect(eq(h.kind, s::format::elf)); + expect(h.needed.empty()) << "musl host helper is not statically linked"; + expect(!h.elf_interpreter) << "musl host helper contains PT_INTERP: " << h.interpreter; + }; + + "1051 a cross targets toolchain section is not the build programs"_test[tag{"requires:elf"}][tag{"requires:llvm"}][tag{"requires:gcc"}][tag{"requires:musl"}] = [] { + // `[target.] toolchain` names the target's compiler, not the build program's: under + // --target x86_64-linux-musl the program is compiled by [toolchain] (llvm) and runs on + // this machine, the target by the section's gcc (on Windows the program was compiled by + // the musl cross gcc and could not start). + s::needs({"elf", "llvm", "gcc", "musl"}); + if (!s::has("x86_64-host")) + skip("an x86_64 host"); + s::project p{{"mcpp.toml", std::format("[package]\nname = \"crossprog\"\nversion = \"0.1.0\"\n\n[toolchain]\ndefault = \"llvm@{}\"\n\n[target.x86_64-linux-musl]\ntoolchain = \"gcc@16.1.0\"\n", s::llvm_version())}, + {"build.mcpp", "import std;\nimport mcpp;\nint main() {\n std::ofstream(std::string(mcpp::out_dir()) + \"/ran.txt\") << \"ran\\n\";\n}\n"}, + {"src/main.cpp", "int main() { return 0; }\n"}}; + auto r = p.mcpp({"build", "--target", "x86_64-linux-musl"}); + const auto out = s::text(r); + expect(fatal(e2e::succeeded(r))) << "the cross build failed"; + expect(out.contains("host toolchain for build.mcpp: llvm") || s::has_line(out, "Resolved host toolchain for build.mcpp: clang")) + << "the build program is not compiled by the [toolchain] compiler"; + expect(e2e::contains(out, "x86_64-linux-musl-g++")) << "the target is not compiled by the section's gcc"; + expect(s::find_file(p.at("target"), "ran.txt").has_value()) << "the build program did not run"; + }; +}}; + +} // namespace diff --git a/tests/e2e/build-program/deps.cpp b/tests/e2e/build-program/deps.cpp new file mode 100644 index 000000000..653315521 --- /dev/null +++ b/tests/e2e/build-program/deps.cpp @@ -0,0 +1,942 @@ +// The build program of a dependency: its scope, where its outputs live, what it is told of its own +// dependencies (formerly tests/e2e/111, 125). +import std; +import mcpplibs.mtest; +import mcpp.test.support; + +using namespace mcpplibs::mtest; +namespace s = mcpp::test::support; +namespace fs = std::filesystem; + +namespace { + +// The regular files under `root` named `name` that their owner may execute. +std::vector executables(const fs::path& root, std::string_view name) { + std::vector out; + for (const auto& f : s::find_files(root, ".*")) + if (f.filename() == name && (fs::status(f).permissions() & fs::perms::owner_exec) != fs::perms::none) + out.push_back(f); + return out; +} + +// `strings -a ; nm -C ` holds `symbol`: the bytes hold it, or the demangled table does. +bool has_symbol(const fs::path& bin, std::string_view symbol) { + if (s::read_file(bin).contains(symbol)) + return true; + if (auto nm = s::on_path("nm")) + return s::run(*nm, {"-C", bin.string()}).out.contains(symbol); + return false; +} + +std::string library(std::string_view name) { + return std::format("[package]\nname = \"{0}\"\nversion = \"0.1.0\"\n\n[targets.{0}]\nkind = \"lib\"\n", name); +} + +E2E deps{"deps", [] { + "111 dep build mcpp"_test[tag{"requires:gcc"}] = [] { + // A dependency's build.mcpp runs with Cargo's scope: its define reaches only the + // dependency's own units, its generated source (in MCPP_OUT_DIR) compiles into it, and + // what it writes lives in the consuming project; the dependency's root is never written. + s::needs({"gcc"}); + const auto root = temp_dir(); + s::project dep{root / "deplib", {{"mcpp.toml", "[package]\nname = \"deplib\"\nversion = \"0.1.0\"\n\n[targets.deplib]\nkind = \"lib\"\n"}, + {"build.mcpp", R"cpp(#include +#include +#include +#include +int main() { + const char* out = std::getenv("MCPP_OUT_DIR"); + if (!out) { std::fprintf(stderr, "MCPP_OUT_DIR unset\n"); return 1; } + std::ofstream f(std::string(out) + "/gen_dep.cpp"); + f << "extern \"C\" int dep_gen_answer() { return 77; }\n"; + if (!f) return 1; + std::printf("mcpp:generated=gen_dep.cpp\n"); + std::printf("mcpp:cfg=DEP_HAS_BP\n"); + return 0; +} +)cpp"}, + {"src/dep.cpp", R"cpp(#ifndef DEP_HAS_BP +#error "dep build.mcpp define did not reach the dep's own TU" +#endif +extern "C" int dep_gen_answer(); +extern "C" int dep_answer() { return dep_gen_answer(); } +)cpp"}}}; + s::project app{root / "app", {{"mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies.deplib]\npath = \"../deplib\"\n"}, + {"src/main.cpp", R"cpp(#ifdef DEP_HAS_BP +#error "dep build.mcpp define leaked into the consumer TU" +#endif +import std; +extern "C" int dep_answer(); +int main() { + std::println("dep = {}", dep_answer()); + return 0; +} +)cpp"}}}; + const auto before = s::listing(dep.dir()); + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "build failed"; + expect(eq(s::last_line(app.mcpp({"run"}).out), std::string("dep = 77"))); + expect(eq(s::listing(dep.dir()), before)) << "dependency root was written to"; + // The program lives in .build-mcpp/deps, its output in this configuration's directory. + bool found = false; + for (const auto& f : s::find_files(app.at("target/.build-mcpp/out"), R"(gen_dep\.cpp)")) + found = found || f.generic_string().ends_with("deplib/gen_dep.cpp"); + expect(found) << "dep OUT_DIR generated file not in project tree"; + }; + + "125 build mcpp dep dir"_test[tag{"requires:elf"}][tag{"requires:gcc"}] = [] { + // mcpp#241: each resolved dependency's directory is MCPP_DEP__DIR + // (mcpp::dep_dir("")): consumer -> pkgp (a build.mcpp) -> datad. + s::needs({"elf", "gcc"}); + const auto root = temp_dir(); + s::project datad{root / "datad", {{"src/d.cpp", "int datad_touch() { return 1; }\n"}, + {"mcpp.toml", "[package]\nname = \"datad\"\nversion = \"0.1.0\"\n\n[modules]\nsources = [\"src/**/*.cpp\"]\n\n[targets.datad]\nkind = \"lib\"\n"}}}; + s::project pkgp{root / "pkgp", {{"src/lib.cpp", "extern const char* dep_datad_dir();\nconst char* pkgp_datad_dir() { return dep_datad_dir(); }\n"}, + {"build.mcpp", R"cpp(#include +#include +import mcpp; +int main() { + const char* d = mcpp::dep_dir("datad"); + if (d == nullptr || d[0] == '\0') { + std::fprintf(stderr, "build.mcpp: MCPP_DEP_DATAD_DIR not set\n"); + return 1; + } + std::string out = std::string(mcpp::out_dir()) + "/depdir.cpp"; + std::FILE* f = std::fopen(out.c_str(), "w"); + std::fprintf(f, "extern const char* dep_datad_dir();\n"); + std::fprintf(f, "const char* dep_datad_dir() { return \"%s\"; }\n", d); + std::fclose(f); + mcpp::generated("depdir.cpp"); + return 0; +} +)cpp"}, + {"mcpp.toml", "[package]\nname = \"pkgp\"\nversion = \"0.1.0\"\n\n[modules]\nsources = [\"src/**/*.cpp\"]\n\n[dependencies]\ndatad = { path = \"../datad\" }\n\n[targets.pkgp]\nkind = \"lib\"\n"}}}; + s::project consumer{root / "consumer", {{"src/main.cpp", "import std;\nextern const char* pkgp_datad_dir();\nint main() {\n std::println(\"DEPDIR={}\", pkgp_datad_dir());\n return 0;\n}\n"}, + {"mcpp.toml", "[package]\nname = \"consumer\"\nversion = \"0.1.0\"\n\n[modules]\nsources = [\"src/**/*.cpp\"]\n\n[dependencies]\npkgp = { path = \"../pkgp\" }\n\n[targets.consumer]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n"}}}; + expect(fatal(e2e::succeeded(consumer.mcpp({"build"})))) << "build failed"; + std::string dir; + for (const auto& l : s::lines_with(s::text(consumer.mcpp({"run"})), "DEPDIR=")) + if (l.starts_with("DEPDIR=")) dir = l.substr(7); + expect(fatal(!dir.empty())) << "empty dep dir (contract var missing)"; + expect(fs::is_directory(dir)) << "dep dir does not exist: " << dir; + auto d = fs::path(dir); + if (d.filename().empty()) d = d.parent_path(); // basename ignores a trailing slash + expect(eq(d.filename().string(), std::string("datad"))) << "dep dir is not datad's root: " << dir; + }; + + "145 root build mcpp dep dirs"_test[tag{"requires:gcc"}] = [] { + // The root's build.mcpp runs after resolution and is given MCPP_DEP__DIR as a + // dependency's is: root -> datad. + s::needs({"gcc"}); + const auto root = temp_dir(); + s::project datad{root / "datad", {{"src/d.cpp", "int datad_touch() { return 1; }\n"}, + {"mcpp.toml", "[package]\nname = \"datad\"\nversion = \"0.1.0\"\n\n[modules]\nsources = [\"src/**/*.cpp\"]\n\n[targets.datad]\nkind = \"lib\"\n"}}}; + s::project rootp{root / "rootp", {{"build.mcpp", R"cpp(#include +#include +import mcpp; +int main() { + const char* d = mcpp::dep_dir("datad"); + if (d == nullptr || d[0] == '\0') { + std::fprintf(stderr, "build.mcpp: MCPP_DEP_DATAD_DIR not set for the ROOT\n"); + return 1; + } + std::string out = "src/depdir.cpp"; + std::FILE* f = std::fopen(out.c_str(), "w"); + std::fprintf(f, "const char* root_datad_dir() { return \"%s\"; }\n", d); + std::fclose(f); + mcpp::generated("src/depdir.cpp"); + return 0; +} +)cpp"}, + {"src/main.cpp", "import std;\nextern const char* root_datad_dir();\nint main() {\n std::println(\"DEPDIR={}\", root_datad_dir());\n return 0;\n}\n"}, + {"mcpp.toml", "[package]\nname = \"rootp\"\nversion = \"0.1.0\"\n\n[modules]\nsources = [\"src/**/*.cpp\"]\n\n[dependencies]\ndatad = { path = \"../datad\" }\n\n[targets.rootp]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n"}}}; + expect(fatal(e2e::succeeded(rootp.mcpp({"build"})))) << "build failed"; + std::string dir; + for (const auto& l : s::lines_with(s::text(rootp.mcpp({"run"})), "DEPDIR=")) + if (l.starts_with("DEPDIR=")) dir = l.substr(7); + expect(fatal(!dir.empty())) << "empty dep dir (root contract var missing)"; + expect(fs::is_directory(dir)) << "dep dir does not exist: " << dir; + auto d = fs::path(dir); + if (d.filename().empty()) d = d.parent_path(); + expect(eq(d.filename().string(), std::string("datad"))) << "dep dir is not datad's root: " << dir; + }; + + "310 build dependencies"_test[tag{"requires:gcc"}] = [] { + // `[build-dependencies]`: what serves the build and what reaches the target are two + // questions. A rule's own [dependencies] and a build dependency (with what lies behind + // them) stay out of the consumer's binary; a package the project depends on directly + // still reaches it, also when named in both tables. The reading is the artifact. + s::needs({"gcc"}); + const auto root = temp_dir(); + for (std::string n : {"behindrule", "buildside", "bothsides"}) { + s::project lib{root / n, {}}; + lib.write("mcpp.toml", library(n)); + lib.write("src/" + n + ".cppm", std::format("export module {0};\nexport int {0}_symbol() {{ return 1; }}\n", n)); + } + s::project rules{root / "rules", {{"mcpp.toml", library("ruleswithdeps") + "\n[dependencies]\nbehindrule = { path = \"../behindrule\" }\n"}, + {"src/ruleswithdeps.cppm", "export module ruleswithdeps;\nimport std;\nimport mcpp;\nexport namespace rules { inline void go() { mcpp::cxxflag(\"-DRULE_RAN=1\"); } }\n"}}}; + const std::string head = "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies]\nruleswithdeps = { path = \"../rules\", host-module = true }\nbothsides = { path = \"../bothsides\" }\n\n[build-dependencies]\n"; + s::project app{root / "app", {{"mcpp.toml", head + "buildside = { path = \"../buildside\" }\n"}, + {"src/main.cpp", "#include \n#ifndef RULE_RAN\n#error \"the rule never ran\"\n#endif\nimport bothsides;\nint main() { std::printf(\"APP=%d\\n\", bothsides_symbol()); }\n"}, + {"build.mcpp", "import mcpp;\nimport ruleswithdeps;\nint main() { rules::go(); }\n"}}}; + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "build failed"; + expect(eq(s::last_starting(s::text(app.mcpp({"run"})), "APP="), std::string("APP=1"))) << "the app did not run"; + fs::path bin = s::last_line(app.mcpp({"run", "--print-artifact"}).out); + if (bin.empty() || !fs::is_regular_file(bin)) { + auto bins = executables(app.at("target"), "app"); + expect(fatal(eq(bins.size(), std::size_t{1}))) << "expected exactly one app binary"; + bin = bins.front(); + } + // The direct dependency first, so that a failure to link anything cannot pass the rest. + expect(fatal(has_symbol(bin, "bothsides_symbol"))) << "a direct [dependencies] library did not reach the target"; + expect(!has_symbol(bin, "behindrule_symbol")) << "a build rule's own [dependencies] leaked into the consumer's binary"; + expect(!has_symbol(bin, "buildside_symbol")) << "a [build-dependencies] entry reached the target"; + + // One package in both tables: the ordinary declaration wins. + app.write("mcpp.toml", head + "bothsides = { path = \"../bothsides\" }\n"); + app.remove("target"); + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "dual-table build failed"; + auto bins = executables(app.at("target"), "app"); + expect(fatal(eq(bins.size(), std::size_t{1}))) << "expected exactly one app binary"; + expect(has_symbol(bins.front(), "bothsides_symbol")) << "naming a library in [build-dependencies] too removed it from the target"; + }; + + "311 rule build dependencies"_test[tag{"requires:gcc"}] = [] { + // A rule may depend on another rule in its own [build-dependencies]; the consumer may not + // import that one unless the edge says `reexport = true` (enforced on every platform, as + // GCC's BMIs are reachable by name whatever the flags), also on a warm cache; a cycle is + // reported as one, naming its packages. + s::needs({"gcc"}); + const auto root = temp_dir(); + s::project globbing{root / "globbing", {{"mcpp.toml", library("globbing")}, + {"src/globbing.cppm", "export module globbing;\nimport std;\nexport namespace globbing {\ninline int answer() { return 7; }\n}\n"}}}; + s::project tidy{root / "tidyrule", {{"src/tidyrule.cppm", R"cpp(export module tidyrule; +import std; +import mcpp; +import globbing; +export namespace tidyrule { +inline void go() { + mcpp::cxxflag(std::format("-DTIDY_ANSWER={}", globbing::answer()).c_str()); +} +} +)cpp"}}}; + auto outer = [&](std::string_view extra) { + tidy.write("mcpp.toml", library("tidyrule") + std::format("\n[build-dependencies]\nglobbing = {{ path = \"../globbing\", host-module = true{} }}\n", extra)); + }; + outer(""); + s::project app{root / "app", {{"mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies]\ntidyrule = { path = \"../tidyrule\", host-module = true }\n"}, + {"src/main.cpp", "#include \n#ifndef TIDY_ANSWER\n#error \"the outer rule never ran\"\n#endif\nint main() { std::printf(\"ANSWER=%d\\n\", TIDY_ANSWER); }\n"}, + {"build.mcpp", "import mcpp;\nimport tidyrule;\nint main() { tidyrule::go(); }\n"}}}; + // 1. The value travels through the inner rule: it was compiled and linked, not only named. + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "a rule with its own [build-dependencies] did not build"; + expect(eq(s::last_starting(s::text(app.mcpp({"run"})), "ANSWER="), std::string("ANSWER=7"))); + expect(eq(executables(app.at("target"), "app").size(), std::size_t{1})) << "expected one app binary"; + + // 2. The consumer may not import what it never declared. + app.write("build.mcpp", "import mcpp;\nimport tidyrule;\nimport globbing;\nint main() { tidyrule::go(); }\n"); + app.remove("target"); + auto b2 = app.mcpp({"build"}); + expect(b2.started && b2.status != 0) << "build.mcpp imported a module reachable only as another rule's prerequisite"; + expect(e2e::contains(s::text(b2), "globbing")) << "the diagnostic does not name the module"; + expect(e2e::contains(s::text(b2), "reexport")) << "the diagnostic does not say how to fix it"; + + // 3. reexport = true makes it the consumer's to import. + outer(", reexport = true"); + app.remove("target"); + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "a re-exported host module was still refused"; + expect(eq(s::last_starting(s::text(app.mcpp({"run"})), "ANSWER="), std::string("ANSWER=7"))); + + // 3b. Withdrawing it is caught with the build.mcpp cache and the fast path warm. + outer(""); + fs::last_write_time(app.at("src/main.cpp"), fs::file_time_type::clock::now()); + auto b3b = app.mcpp({"build"}); + expect(b3b.started && b3b.status != 0) << "withdrawing reexport was replayed from the build.mcpp cache"; + expect(e2e::contains(s::text(b3b), "globbing")) << "the refusal after cache invalidation does not name the module"; + + // 4. A cycle. + globbing.write("mcpp.toml", globbing.read("mcpp.toml") + "\n[build-dependencies]\ntidyrule = { path = \"../tidyrule\", host-module = true }\n"); + app.remove("target"); + auto b4 = app.mcpp({"build"}); + expect(b4.started && b4.status != 0) << "a rule import cycle was accepted"; + expect(s::contains_nocase(s::text(b4), "cycle")) << "the cycle was not reported as a cycle"; + expect(e2e::contains(s::text(b4), "globbing") && e2e::contains(s::text(b4), "tidyrule")) + << "the cycle diagnostic does not name the packages on it"; + }; + + "334 dependency declared tool is found"_test[tag{"requires:gcc"}][tag{"requires:unix-shell"}] = [] { + // A runner named by bare name (mcpp::runner) is looked up through every package of the + // graph, so a dependency (a board package) may declare it. With the program nowhere, the + // run is an error naming it — not a fallback to executing the artifact. The stand-in + // payload is never on PATH: a lookup through PATH would pass for the wrong reason. + s::needs({"gcc", "unix-shell"}); + const auto root = temp_dir(); + s::write_executable(root / "fakepkg/bin/demo-tool", "#!/bin/sh\necho \"DEMO-TOOL ran with $*\"\n"); + if (s::on_path("demo-tool")) + skip("a demo-tool already on PATH"); + s::project dep{root / "dep", {{"mcpp.toml", "[package]\nname = \"toolbox\"\nversion = \"0.1.0\"\n"}, + {"src/t.cppm", "export module toolbox;\n"}, + {"build.mcpp", "import mcpp;\nimport std;\nint main() {\n mcpp::runner(\"demo-tool\");\n return 0;\n}\n"}}}; + s::project app{root / "app", {{"mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies]\ntoolbox = { path = \"../dep\" }\n"}, + {"src/main.cpp", "int main() { return 0; }\n"}}}; + auto r = app.mcpp({"run"}); + const auto out = s::text(r); + expect(r.started && r.status != 0) << "run succeeded with an unresolvable runner — it fell back to executing the artifact"; + expect(e2e::contains(out, "demo-tool")) << "the diagnostic does not name the program that was not found"; + expect(e2e::contains(out, "not found")) << "the diagnostic does not say the program was not found"; + expect(s::lacks(out, "DEMO-TOOL ran")) << "the artifact was executed anyway"; + }; + + "618 dependency feature tools reach its build program"_test[tag{"requires:elf"}][tag{"requires:gcc"}] = [] { + // A dependency's [feature-xlings.] is installed before that dependency's build.mcpp + // runs (it was provisioned after every build program had run). Only a registry that has + // never seen the tool can tell, so the home is fresh, and the tool (shaderc) is one mcpp + // does not install for its own reasons. The program writes what it found to a file. + s::needs({"elf", "gcc"}); + const auto root = temp_dir(); + s::project dep{root / "dep", {{"src/lib.cpp", "int dep_touch() { return 1; }\n"}, + {"build.mcpp", R"cpp(#include +#include +import mcpp; +int main() { + const char* d = mcpp::xpkg_dir("xim", "shaderc"); + if (d == nullptr || d[0] == '\0') { + std::fprintf(stderr, + "a dependency's [feature-xlings] tool was not provisioned before " + "its build.mcpp ran\n"); + return 2; + } + std::string out = std::string(mcpp::manifest_dir()) + "/tool-seen.txt"; + std::FILE* f = std::fopen(out.c_str(), "w"); + if (f == nullptr) return 3; + std::fprintf(f, "%s\n", d); + std::fclose(f); + return 0; +} +)cpp"}, + {"mcpp.toml", "[package]\nname = \"featuretool\"\nversion = \"0.1.0\"\n\n[modules]\nsources = [\"src/**/*.cpp\"]\n\n[features]\ndefault = []\nusestool = []\n\n[feature-xlings.usestool]\n\"xim:shaderc\" = \"2026.3\"\n\n[targets.featuretool]\nkind = \"lib\"\n"}}}; + s::project app{root / "app", {{"src/main.cpp", "int main() { return 0; }\n"}, + {"mcpp.toml", "[package]\nname = \"consumer\"\nversion = \"0.1.0\"\n\n[dependencies]\nfeaturetool = { path = \"../dep\", features = [\"usestool\"] }\n\n[targets.consumer]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n"}}}; + expect(fatal(e2e::succeeded(app.mcpp({"build"}, {.where = s::home::fresh})))) << "the consumer did not build"; + expect(fatal(dep.exists("tool-seen.txt") && !dep.read("tool-seen.txt").empty())) + << "the dependency's build program left no record of finding its tool"; + expect(e2e::contains(dep.read("tool-seen.txt"), "xim-x-shaderc")) << "the recorded path does not name shaderc"; + }; + + "622 rule declared payload reaches the consumers build program"_test[tag{"requires:elf"}][tag{"requires:gcc"}] = [] { + // A rule package's [feature-xlings.] is reachable from the consumer's build program, + // where the rule's code runs (xpkg_dir read one manifest: "" while the payload sat on + // disk). The reading is the path the rule answers with; a fresh home, for 618's reason. + s::needs({"elf", "gcc"}); + const auto root = temp_dir(); + s::project rule{root / "rule", {{"src/rule.cppm", R"cpp(export module rule; +import std; +import mcpp; +export namespace testrule { +inline std::string tool_dir() { + const char* d = mcpp::xpkg_dir("xim", "shaderc"); + return d == nullptr ? std::string() : std::string(d); +} +} +)cpp"}, + {"mcpp.toml", "[package]\nname = \"rule\"\nversion = \"0.1.0\"\n\n[language]\nstandard = \"c++23\"\nmodules = true\nimport_std = true\n\n[build]\nsources = [\"src/rule.cppm\"]\n\n[features]\ndefault = []\nusestool = []\n\n[feature-xlings.usestool]\n\"xim:shaderc\" = \"2026.3\"\n\n[targets.rule]\nkind = \"lib\"\n"}}}; + s::project app{root / "app", {{"src/main.cpp", "int main() { return 0; }\n"}, + {"build.mcpp", R"cpp(#include +#include +import mcpp; +import rule; +int main() { + std::string out = std::string(mcpp::manifest_dir()) + "/rule-saw.txt"; + std::FILE* f = std::fopen(out.c_str(), "w"); + if (f == nullptr) return 3; + std::fprintf(f, "%s\n", testrule::tool_dir().c_str()); + std::fclose(f); + return 0; +} +)cpp"}, + {"mcpp.toml", "[package]\nname = \"consumer\"\nversion = \"0.1.0\"\n\n[language]\nstandard = \"c++23\"\nmodules = true\nimport_std = true\n\n[build-dependencies]\nrule = { path = \"../rule\", features = [\"usestool\"], host-module = true }\n\n[targets.consumer]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n"}}}; + expect(fatal(e2e::succeeded(app.mcpp({"build"}, {.where = s::home::fresh})))) << "the consumer did not build"; + expect(fatal(app.exists("rule-saw.txt") && !app.read("rule-saw.txt").empty())) << "the rule left no record"; + const auto answer = s::last_line(app.read("rule-saw.txt")); + expect(fatal(!answer.empty())) << "the rule got an empty answer -- its payload was installed and unreachable"; + expect(e2e::contains(answer, "xim-x-shaderc")) << "the answer does not name shaderc"; + }; + + "693 the root build program reads dependency link forms"_test[tag{"requires:elf"}] = [] { + // #642 E2: mcpp::dep_linkage(name) answers "static" or "shared" from the resolution that + // decides what is linked, to the root's program only. A: a shared-by-default package and a + // generated-only one asked shared; B: nothing changed, no re-run; C: the dependency's + // default turns static; D: the edge asks shared; E: a dependency's program reads "" (gen + // fails the build otherwise); F: `namespace` + `name` answer both spellings. + s::needs({"elf"}); + const auto root = temp_dir(); + s::project leaf{root / "leaf", {{"mcpp.toml", "[package]\nname = \"leaf\"\nversion = \"0.1.0\"\n[targets.leaf]\nkind = \"lib\"\n"}, + {"src/leaf.cpp", "int leaf_value() { return 1; }\n"}}}; + s::project fw{root / "fw", {{"src/fw.cpp", "int fw_value() { return 40; }\n"}}}; + auto write_fw = [&](std::string_view form) { + fw.write("mcpp.toml", std::format("[package]\nname = \"fw\"\nversion = \"0.1.0\"\n[targets.fw]\nkind = \"lib\"\nlinkage = \"{}\"\n", form)); + }; + write_fw("shared"); + s::project gen{root / "gen", {{"mcpp.toml", "[package]\nname = \"gen\"\nversion = \"0.1.0\"\n\n[modules]\nsources = []\n\n[dependencies]\nleaf = { path = \"../leaf\" }\n\n[targets.gen]\nkind = \"lib\"\n"}, + {"build.mcpp", R"cpp(#include +#include +import mcpp; +int main() { + const char* dir = mcpp::dep_dir("leaf"); + const char* form = mcpp::dep_linkage("leaf"); + if (dir == nullptr || dir[0] == '\0') { + std::fprintf(stderr, "gen: dep_dir(leaf) is empty\n"); + return 1; + } + if (form != nullptr && form[0] != '\0') { + std::fprintf(stderr, "gen: a dependency's program was offered dep_linkage(leaf)=%s\n", form); + return 1; + } + std::string out = std::string(mcpp::out_dir()) + "/gen.cpp"; + std::FILE* f = std::fopen(out.c_str(), "w"); + std::fprintf(f, "int leaf_value();\nint gen_value() { return leaf_value() + 1; }\n"); + std::fclose(f); + mcpp::generated("gen.cpp"); + return 0; +} +)cpp"}}}; + s::project app{root / "app", {{"build.mcpp", R"cpp(#include +#include +import mcpp; +int main() { + std::string line = std::string("fw=") + mcpp::dep_linkage("fw") + + " gen=" + mcpp::dep_linkage("gen") + "\n"; + std::string log = std::string(mcpp::manifest_dir()) + "/runs.log"; + std::FILE* f = std::fopen(log.c_str(), "a"); + std::fputs(line.c_str(), f); + std::fclose(f); + return 0; +} +)cpp"}, + {"src/main.cpp", "int fw_value();\nint gen_value();\nint main() { return fw_value() + gen_value() == 42 ? 0 : 1; }\n"}}}; + auto write_app = [&](std::string_view edge) { + app.write("mcpp.toml", std::format("[package]\nname = \"app\"\nversion = \"0.1.0\"\n[dependencies]\nfw = {{ path = \"../fw\"{} }}\ngen = {{ path = \"../gen\", linkage = \"shared\" }}\n", edge)); + }; + write_app(""); + auto runs = [&] { return s::last_line(app.read("runs.log")); }; + auto run_count = [&] { return s::count_lines(app.read("runs.log"), "="); }; + auto lib = [&](std::string_view name) { return s::find_file(app.at("target"), name).has_value(); }; + // A. + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "A: build failed"; + expect(eq(runs(), std::string("fw=shared gen=shared"))) << "A"; + expect(lib("libfw.so")) << "A: no libfw.so"; + expect(lib("libgen.so")) << "A: no libgen.so for a package whose sources are all generated"; + std::optional bin; + for (const auto& f : executables(app.at("target"), "app")) + if (!bin && f.generic_string().contains("/bin/")) bin = f; + expect(fatal(bin.has_value())); + expect(e2e::succeeded(s::run(*bin))) << "A: the program did not run"; + // B. + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "B: build failed"; + expect(eq(run_count(), std::size_t{1})) << "B: an unchanged build re-ran the root program"; + // C. + write_fw("static"); + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "C: build failed"; + expect(eq(run_count(), std::size_t{2})) << "C: a changed dependency form did not re-run the root program"; + expect(eq(runs(), std::string("fw=static gen=shared"))) << "C"; + app.remove("target"); + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "C: clean build failed"; + expect(!lib("libfw.so")) << "C: libfw.so was built while the program read static"; + // D. + write_app(", linkage = \"shared\""); + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "D: build failed"; + expect(eq(runs(), std::string("fw=shared gen=shared"))) << "D"; + expect(lib("libfw.so")) << "D: no libfw.so while the program read shared"; + // F. + s::project nsfw{root / "nsfw", {{"mcpp.toml", "[package]\nnamespace = \"ns\"\nname = \"nsfw\"\nversion = \"0.1.0\"\n[targets.nsfw]\nkind = \"lib\"\nlinkage = \"shared\"\n"}, + {"src/nsfw.cpp", "int nsfw_value() { return 42; }\n"}}}; + s::project nsapp{root / "nsapp", {{"mcpp.toml", "[package]\nname = \"nsapp\"\nversion = \"0.1.0\"\n[dependencies]\nns.nsfw = { path = \"../nsfw\" }\n"}, + {"src/main.cpp", "int nsfw_value();\nint main() { return nsfw_value() == 42 ? 0 : 1; }\n"}, + {"build.mcpp", R"cpp(#include +#include +import mcpp; +int main() { + std::string line = std::string("qualified=") + mcpp::dep_linkage("ns.nsfw") + + " bare=" + mcpp::dep_linkage("nsfw") + + " dir=" + (mcpp::dep_dir("ns.nsfw")[0] != '\0' ? "yes" : "no") + "\n"; + std::string log = std::string(mcpp::manifest_dir()) + "/runs.log"; + std::FILE* f = std::fopen(log.c_str(), "a"); + std::fputs(line.c_str(), f); + std::fclose(f); + return 0; +} +)cpp"}}}; + expect(fatal(e2e::succeeded(nsapp.mcpp({"build"})))) << "F: build failed"; + expect(eq(s::last_line(nsapp.read("runs.log")), std::string("qualified=shared bare=shared dir=yes"))) << "F"; + }; + + "720 the root build program reads the resolved graph"_test = [] { + // #647 E1: mcpp::graph_file() gives the root's program the resolved graph and each + // package's [package.metadata] (a transitive library has no name to ask dep_dir for). + // A: b before a before the root, b's directory, kind and metadata verbatim; B: nothing + // changed, no re-run; C: a metadata edit re-runs with the new value; D: a source edit does + // not; E: a dependency's program reads ""; F: an unknown [package] key warns, fails under + // --strict, and `metadata` is known. (The script read the document with python3.) + const auto root = temp_dir(); + s::project b{root / "b", {{"src/b.cpp", "int b_answer() { return 40; }\n"}}}; + auto write_b = [&](std::string_view resources) { + b.write("mcpp.toml", std::format("[package]\nname = \"b\"\nnamespace = \"spike\"\nversion = \"0.2.0\"\n\n[package.metadata.demo]\nresources = \"{}\"\nlanguages = [\"en\", \"zh\"]\n\n[targets.b]\nkind = \"lib\"\n", resources)); + }; + write_b("res"); + s::project a{root / "a", {{"mcpp.toml", "[package]\nname = \"a\"\nnamespace = \"spike\"\nversion = \"0.1.0\"\n\n[dependencies]\nspike.b = { path = \"../b\" }\n\n[targets.a]\nkind = \"lib\"\n"}, + {"src/a.cpp", "int b_answer();\nint a_answer() { return b_answer() + 1; }\n"}, + {"build.mcpp", R"cpp(#include +import mcpp; +int main() { + const char* g = mcpp::graph_file(); + if (g != nullptr && g[0] != '\0') { + std::fprintf(stderr, "a: a dependency's program was offered graph_file()=%s\n", g); + return 1; + } + return 0; +} +)cpp"}}}; + const std::string app_manifest = "[package]\nname = \"app\"\nversion = \"0.1.0\"\n[dependencies]\nspike.a = { path = \"../a\" }\n"; + s::project app{root / "app", {{"mcpp.toml", app_manifest}, + {"src/main.cpp", "int a_answer();\nint main() { return a_answer() == 41 ? 0 : 1; }\n"}, + {"build.mcpp", R"cpp(#include +#include +#include +#include +import mcpp; +int main() { + const char* g = mcpp::graph_file(); + if (g == nullptr || g[0] == '\0') { + std::fprintf(stderr, "app: graph_file() is empty\n"); + return 1; + } + std::ifstream in(g, std::ios::binary); + std::stringstream ss; + ss << in.rdbuf(); + const std::string dir = mcpp::manifest_dir(); + int n = 0; + for (;; ++n) { + std::ifstream probe(dir + "/graph-" + std::to_string(n) + ".json"); + if (!probe) break; + } + std::ofstream(dir + "/graph-" + std::to_string(n) + ".json", std::ios::binary) << ss.str(); + return 0; +} +)cpp"}}}; + auto runs = [&] { return s::glob(app.dir(), "graph-*.json").size(); }; + // A, E. + auto ra = app.mcpp({"build"}); + expect(fatal(e2e::succeeded(ra))) << "A: build failed"; + expect(fatal(eq(runs(), std::size_t{1}))) << "A: the root program's runs"; + const auto d = s::json_file(app.at("graph-0.json")); + std::vector names; + for (const auto& p : d["packages"]) names.push_back(p["package"]["canonical"].get()); + expect(fatal(eq(names, std::vector{"spike.b@0.2.0", "spike.a@0.1.0", "mcpplibs.app@0.1.0"}))); + const auto& gb = d["packages"][0]; + const auto& ga = d["packages"][1]; + const auto& gapp = d["packages"][2]; + expect(gapp["root"] == true && gb["root"] == false); + const fs::path bdir = gb["manifest_dir"].get(); + expect(bdir.is_absolute()) << bdir.string(); + expect(fs::equivalent(bdir, b.dir())) << bdir.string(); + expect(gb["targets"] == nlohmann::json::parse(R"([{"name": "b", "kind": "lib"}])")) << gb["targets"].dump(); + expect(gb["metadata"] == nlohmann::json::parse(R"({"demo": {"resources": "res", "languages": ["en", "zh"]}})")) << gb["metadata"].dump(); + expect(ga["metadata"] == nlohmann::json::object()) << ga["metadata"].dump(); + std::vector requesters; + for (const auto& r : gb["requested_by"]) requesters.push_back(r["requester"].get()); + expect(eq(requesters, std::vector{"spike.a@0.1.0"})); + expect(gb["features"].is_array()); + // B. + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "B: build failed"; + expect(eq(runs(), std::size_t{1})) << "B: an unchanged build re-ran the root program"; + // C. + write_b("assets"); + auto rc = app.mcpp({"build"}); + expect(fatal(e2e::succeeded(rc))) << "C: build failed"; + expect(fatal(eq(runs(), std::size_t{2}))) << "C: editing a dependency's metadata did not re-run the root program"; + expect(eq(s::json_file(app.at("graph-1.json"))["packages"][0]["metadata"]["demo"]["resources"].get(), std::string("assets"))); + // D. + b.write("src/b.cpp", "int b_answer() { return 39 + 1; }\n"); + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "D: build failed"; + expect(eq(runs(), std::size_t{2})) << "D: editing a dependency's source re-ran the root program"; + // F. + app.write("mcpp.toml", std::regex_replace(app_manifest, std::regex("version = \"0.1.0\"\n"), "version = \"0.1.0\"\nlicence = \"MIT\"\n", std::regex_constants::format_first_only)); + auto rf = app.mcpp({"build"}); + expect(fatal(e2e::succeeded(rf))) << "F: build failed on a warning"; + expect(e2e::contains(s::text(rf), "[package] has unsupported key 'licence'")) << "F: no warning for an unknown [package] key"; + auto rf2 = app.mcpp({"build", "--strict"}); + expect(rf2.started && rf2.status != 0) << "F: --strict accepted an unknown [package] key"; + expect(e2e::contains(s::text(rf2), "[package] has unsupported key 'licence'")) << "F: --strict failed for another reason"; + for (const auto* r : {&ra, &rc, &rf}) + expect(s::lacks(s::text(*r), "unsupported key 'metadata'")) << "F: metadata reported as unknown"; + }; + + "779 a build program declares a runtime search dir"_test[tag{"requires:pack"}][tag{"requires:gcc"}] = [] { + // mcpp::runtime_search_dir(dir), the build-program form of runtime_search_dirs (docs/04 + // §2.11, mcpp#701/#702): -Wl,-rpath in build.ninja, never -L; `mcpp run` finds the library + // through that RUNPATH alone; a build.mcpp cache hit keeps it; pack stages it into lib/; + // a dependency's declaration reaches its consumer's executable. + s::needs({"pack", "gcc"}); + const auto gcc = s::on_path("gcc"); + expect(fatal(gcc.has_value())) << "no gcc on PATH"; + const auto root = temp_dir(); + auto graph = [](const s::project& p) { + for (const auto& f : s::find_files(p.at("target"), R"(build\.ninja)")) + if (!f.generic_string().contains("/.build-mcpp/")) return s::read_file(f); + return std::string(); + }; + auto shared = [&](const fs::path& c, const fs::path& so) { + expect(fatal(e2e::succeeded(s::run(*gcc, {"-shared", "-fPIC", c.string(), "-o", so.string()})))); + }; + auto program = [](std::string_view lib, std::string_view dir) { + return std::format("import mcpp;\n#include \nint main() {{\n const std::string root = mcpp::manifest_dir();\n mcpp::link_lib(\"{0}\");\n mcpp::link_search((root + \"/{1}\").c_str());\n mcpp::runtime_search_dir((root + \"/{1}\").c_str());\n return 0;\n}}\n", lib, dir); + }; + s::project app{root / "app", {{"rtlib/plugin.c", "int runtime_plugin_answer(void) { return 42; }\n"}, + {"src/main.cpp", "extern \"C\" int runtime_plugin_answer();\nint main() { return runtime_plugin_answer() == 42 ? 0 : 1; }\n"}, + {"mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[toolchain]\nlinux = \"gcc@16.1.0\"\n\n[targets.app]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n"}, + {"build.mcpp", program("runtime_plugin", "rtlib")}}}; + shared(app.at("rtlib/plugin.c"), app.at("rtlib/libruntime_plugin.so")); + const auto rtdir = fs::canonical(app.at("rtlib")).string(); + // 1. RUNPATH, never -L. + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "build failed"; + expect(e2e::contains(graph(app), "-Wl,-rpath," + rtdir)) << "runtime-search-dir directive missing from RUNPATH intent"; + // 2. `mcpp run` finds the library through it. + expect(e2e::succeeded(app.mcpp({"run"}))) << "run failed to find the plugin through the directive's RUNPATH"; + // 3. A cache hit keeps it (before pack, which builds under another profile). + fs::last_write_time(app.at("src/main.cpp"), fs::file_time_type::clock::now()); + auto b2 = app.mcpp({"build", "-v"}); + expect(fatal(e2e::succeeded(b2))) << "second build failed"; + expect(s::has_line(s::text(b2), R"(^ *build\.mcpp .* cached)")) << "the second build re-ran build.mcpp; the replay path was not exercised"; + expect(e2e::contains(graph(app), "-Wl,-rpath," + rtdir)) << "the RUNPATH directive did not survive a build.mcpp cache hit"; + expect(e2e::succeeded(app.mcpp({"run"}))) << "run failed after the directive was replayed from a build.mcpp cache hit"; + // 4. pack stages it into lib/. + expect(fatal(e2e::succeeded(app.mcpp({"pack", "--pkg-format", "dir"})))) << "pack --pkg-format dir failed"; + auto staged = s::find_file(app.at("target/dist"), "libruntime_plugin.so"); + expect(fatal(staged.has_value())) << "the runtime-search-dir closure was not staged by mcpp pack"; + expect(staged->generic_string().ends_with("/lib/libruntime_plugin.so")) << "the staged library is not under the packed bundle's lib/"; + // 5. A dependency's declaration reaches the consumer. + s::project libdep{root / "libdep", {{"plugin/dep_plugin.c", "int dep_plugin_answer(void) { return 7; }\n"}, + {"src/lib.cpp", "extern \"C\" int dep_plugin_answer();\nint libdep_touch() { return dep_plugin_answer(); }\n"}, + {"mcpp.toml", "[package]\nname = \"libdep\"\nversion = \"0.1.0\"\n\n[modules]\nsources = [\"src/**/*.cpp\"]\n\n[targets.libdep]\nkind = \"lib\"\n"}, + {"build.mcpp", program("dep_plugin", "plugin")}}}; + shared(libdep.at("plugin/dep_plugin.c"), libdep.at("plugin/libdep_plugin.so")); + s::project consumer{root / "consumer", {{"src/main.cpp", "extern \"C\" int dep_plugin_answer();\nint main() { return dep_plugin_answer() == 7 ? 0 : 1; }\n"}, + {"mcpp.toml", "[package]\nname = \"consumer\"\nversion = \"0.1.0\"\n\n[toolchain]\nlinux = \"gcc@16.1.0\"\n\n[dependencies]\nlibdep = { path = \"../libdep\" }\n\n[targets.consumer]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n"}}}; + expect(fatal(e2e::succeeded(consumer.mcpp({"build"})))) << "consumer build failed"; + expect(e2e::contains(graph(consumer), "-Wl,-rpath," + fs::canonical(libdep.at("plugin")).string())) << "the dependency's runtime-search-dir did not reach the consumer's link"; + expect(e2e::succeeded(consumer.mcpp({"run"}))) << "consumer run failed to find the dependency's plugin"; + }; + + "873 payload overrides"_test[tag{"requires:gcc"}][tag{"requires:unix-shell"}] = [] { + // mcpp#755: [xlings.overrides], MCPP_XLINGS_OVERRIDE__ and config.toml state + // where a declared payload comes from: it is not provisioned, the build program receives + // the override, and the build says so on a `Using` line and in `Finished`. Every build + // runs with MCPP_NO_AUTO_INSTALL=1 and the payload is in no index, so "built" means "not + // asked for"; the baseline (no override, refused) is the control. + s::needs({"gcc", "unix-shell"}); + const auto work = temp_dir(); + const auto w = work.generic_string(); + s::project plug{work / "plug", {{"mcpp.toml", "[package]\nname = \"plug\"\nnamespace = \"e2e\"\nversion = \"0.1.0\"\n\n[build]\nsources = [\"src/plug.cppm\"]\n\n[features.tools-x]\nsources = [\"members/x.cppm\"]\n\n[feature-xlings.tools-x]\n\"xim:mcpp-e2e-absent-tool\" = \">=2.0\"\n\n[targets.plug]\nkind = \"lib\"\n"}, + {"src/plug.cppm", "export module e2e.plug;\n"}, + {"members/x.cppm", "export module e2e.tools.x;\nexport namespace e2e::x { inline int v = 1; }\n"}}}; + const std::string base = std::format("[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[build-dependencies.e2e]\nplug = {{ path = \"{}/plug\", features = [\"tools-x\"], host-module = true }}\n\n[targets.app]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n", w); + s::project app{work / "app", {{"mcpp.toml", base}, + {"build.mcpp", R"cpp(import std; +import mcpp; +import e2e.tools.x; +int main() { + std::string s = std::string("dir=") + mcpp::xpkg_dir("xim", "mcpp-e2e-absent-tool") + + " source=" + mcpp::xpkg_source("xim", "mcpp-e2e-absent-tool") + + " program=" + mcpp::xpkg_program("xim", "mcpp-e2e-absent-tool"); + mcpp::warning(s.c_str()); + return 0; +} +)cpp"}, + {"src/main.cpp", "int main() { return 0; }\n"}}}; + s::write_executable(work / "opt/bin/absent-tool", "#!/bin/sh\necho tool\n"); + auto probe = [&](std::vector args, std::vector>> extra = {}) { + app.remove("target"); + s::options o{.env = {{"NO_COLOR", "1"}, {"MCPP_NO_AUTO_INSTALL", "1"}}}; + for (auto& e : extra) o.env.push_back(std::move(e)); + return s::text(app.mcpp(std::move(args), o)); + }; + // Control. + auto out = probe({"build"}); + expect(e2e::contains(out, "not provisioned")) << "the baseline did not ask for the payload"; + expect(e2e::contains(out, "mcpp-e2e-absent-tool")) << "the refusal does not name the payload"; + // The environment variable. + out = probe({"build"}, {{"MCPP_XLINGS_OVERRIDE_XIM_MCPP_E2E_ABSENT_TOOL", w + "/opt/bin/absent-tool"}}); + expect(e2e::contains(out, "Finished")) << "an env override still needed the payload"; + expect(e2e::contains(out, "Using xim:mcpp-e2e-absent-tool ← " + w + "/opt/bin/absent-tool [custom · env MCPP_XLINGS_OVERRIDE_XIM_MCPP_E2E_ABSENT_TOOL]")) + << "no Using line naming the env override"; + expect(e2e::contains(out, "dir=" + w + "/opt source=override program=" + w + "/opt/bin/absent-tool")) + << "the build program did not receive the override (root, source, program)"; + expect(s::has_line(out, "Finished .* · custom: xim:mcpp-e2e-absent-tool")) << "Finished does not summarise the source"; + // The manifest, with a version that satisfies the requirement. + auto override_with = [&](const std::string& value) { + app.write("mcpp.toml", base + "\n[xlings.overrides]\n\"xim:mcpp-e2e-absent-tool\" = " + value + "\n"); + }; + override_with(std::format("{{ program = \"{}/opt/bin/absent-tool\", version = \"2.1\" }}", w)); + out = probe({"build"}); + expect(s::has_line(out, R"(\[custom · mcpp\.toml:[0-9]*\])")) << "no Using line naming mcpp.toml"; + // --managed-only refuses it, naming it. + out = probe({"--managed-only", "build"}); + expect(e2e::contains(out, "managed-only") && e2e::contains(out, "mcpp-e2e-absent-tool")) << "--managed-only did not refuse the override"; + // A version below the requirement, naming both sides. + override_with(std::format("{{ program = \"{}/opt/bin/absent-tool\", version = \"1.0\" }}", w)); + out = probe({"build"}); + expect(e2e::contains(out, ">=2.0") && e2e::contains(out, "e2e:plug")) << "a version below the requirement was accepted"; + // A path that does not exist. + override_with(std::format("\"{}/nope\"", w)); + out = probe({"build"}); + expect(e2e::contains(out, "does not exist")) << "a missing override path was accepted"; + // A dependency may not state where a payload comes from. + app.write("mcpp.toml", base); + plug.write("mcpp.toml", plug.read("mcpp.toml") + "\n[xlings.overrides]\n\"xim:mcpp-e2e-absent-tool\" = \"/bin/sh\"\n"); + out = probe({"build"}); + expect(e2e::contains(out, "is a dependency of this build")) << "a dependency's override was accepted"; + }; + + "874 payload on request"_test[tag{"requires:gcc"}][tag{"requires:unix-shell"}] = [] { + // mcpp#755 `provision = "on-request"`: a payload installed when a build program asks with + // xpkg_request, not before it runs. Under MCPP_NO_AUTO_INSTALL=1 the project builds while + // the program does not ask, and is refused naming the program when it does. + s::needs({"gcc", "unix-shell"}); + const auto work = temp_dir(); + const auto w = work.generic_string(); + s::project plug{work / "plug", {{"mcpp.toml", "[package]\nname = \"plug\"\nnamespace = \"e2e\"\nversion = \"0.1.0\"\n\n[build]\nsources = [\"src/plug.cppm\"]\n\n[features.tools-x]\nsources = [\"members/x.cppm\"]\n\n[feature-xlings.tools-x]\n\"xim:mcpp-e2e-absent-tool\" = { version = \"1.0.0\", provision = \"on-request\" }\n\n[targets.plug]\nkind = \"lib\"\n"}, + {"src/plug.cppm", "export module e2e.plug;\n"}, + {"members/x.cppm", "export module e2e.tools.x;\nexport namespace e2e::x { inline int v = 1; }\n"}}}; + s::project app{work / "app", {{"mcpp.toml", std::format("[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[build-dependencies.e2e]\nplug = {{ path = \"{}/plug\", features = [\"tools-x\"], host-module = true }}\n\n[targets.app]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n", w)}, + {"build.mcpp", R"cpp(import std; +import mcpp; +import e2e.tools.x; +int main() { + std::string s = std::string("source=") + mcpp::xpkg_source("xim", "mcpp-e2e-absent-tool"); + if (const char* ask = std::getenv("E2E_ASK"); ask && *ask) { + if (std::string_view(ask) == "undeclared") { + std::printf("mcpp:xpkg-request=xim:mcpp-e2e-undeclared\n"); + return 0; + } + mcpp::xpkg_request("xim", "mcpp-e2e-absent-tool"); + if (mcpp::xpkg_pending()) return 0; + } + mcpp::warning(s.c_str()); + return 0; +} +)cpp"}, + {"src/main.cpp", "int main() { return 0; }\n"}}}; + auto probe = [&](std::optional ask) { + app.remove("target"); + s::options o{.env = {{"NO_COLOR", "1"}, {"MCPP_NO_AUTO_INSTALL", "1"}}}; + if (ask) o.env.emplace_back("E2E_ASK", *ask); + return s::text(app.mcpp({"build"}, o)); + }; + auto out = probe(std::nullopt); + expect(e2e::contains(out, "Finished")) << "a payload nobody asked for was required"; + expect(e2e::contains(out, "source=pending")) << "the build program was not told the payload is pending"; + out = probe("1"); + expect(e2e::contains(out, "payloads requested by the build program of `app`")) << "a request did not reach the installer naming the requester"; + expect(e2e::contains(out, "mcpp-e2e-absent-tool@1.0.0")) << "the refusal does not name the payload"; + out = probe("undeclared"); + expect(e2e::contains(out, "no manifest of this build declares")) << "a request for an undeclared payload was accepted"; + auto db = app.mcpp({"emit", "build-database", "--format", "json"}, + {.env = {{"NO_COLOR", "1"}, {"MCPP_NO_AUTO_INSTALL", "1"}, {"E2E_ASK", "1"}}}); + expect(e2e::contains(db.out, "MCPP_BUILD_DATABASE_PAYLOAD_DEFERRED")) << "planning did not defer the request"; + }; + + "877 why sources"_test[tag{"requires:gcc"}][tag{"requires:unix-shell"}] = [] { + // mcpp#755: `mcpp why sources|tool|payload` and `--format json`, the decision record of a + // prepare as the build reported it. (The script read the JSON with python3.) + s::needs({"gcc", "unix-shell"}); + const auto work = temp_dir(); + const auto w = work.generic_string(); + s::write_executable(work / "opt/bin/tool", "#!/bin/sh\n"); + s::project app{work / "app", {{"mcpp.toml", std::format("[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[xlings.workspace]\n\"xim:mcpp-e2e-absent-tool\" = \"1.0.0\"\n\n[xlings.overrides]\n\"xim:mcpp-e2e-absent-tool\" = \"{}/opt/bin/tool\"\n", w)}, + {"src/main.cpp", "int main() { return 0; }\n"}}}; + const s::options o{.env = {{"NO_COLOR", "1"}, {"MCPP_NO_AUTO_INSTALL", "1"}}}; + auto payload = app.mcpp({"why", "payload", "mcpp-e2e"}, o); + expect(fatal(e2e::succeeded(payload))) << "why payload failed"; + expect(e2e::contains(s::text(payload), "payload:xim:mcpp-e2e-absent-tool " + w + "/opt/bin/tool")) << "the payload is not listed"; + expect(s::has_line(s::text(payload), "custom · mcpp\\.toml:[0-9]*")) << "the origin is not listed"; + auto sources = app.mcpp({"why", "sources", "--format", "json"}, o); + expect(fatal(e2e::succeeded(sources))) << "why sources --format json failed"; + const auto env = s::json(sources.out); + expect(eq(env.value("kind", std::string()), std::string("mcpp.why.sources"))); + std::map subjects; + for (const auto& x : env["data"]["sources"]) subjects[x["subject"].get()] = x; + expect(fatal(subjects.contains("payload:xim:mcpp-e2e-absent-tool"))); + const auto& p = subjects["payload:xim:mcpp-e2e-absent-tool"]; + expect(p["class"] == "custom" && p["origin"]["kind"] == "manifest") << p.dump(); + expect(subjects.contains("toolchain.build")) << "no toolchain.build subject"; + auto nonsense = app.mcpp({"why", "nonsense"}, {.env = {{"NO_COLOR", "1"}}}); + expect(e2e::contains(s::text(nonsense), "is not a topic of `mcpp why`")) << "an unknown topic was accepted"; + }; + + "902 a build dependency library is compiled as its package"_test = [] { + // mcpp#797 R1: a library in [build-dependencies] is usable from build.mcpp as the package + // it is — a module wrapper over a header library, whose global module fragment includes + // its header through the package's include_dirs. A: no `host-module`, the header resolves, + // its defines apply; B: [lib] path outside src/, imported by its declared name; C: no + // object of it in the target's plan. + const s::options home{.where = s::home::inherited}; + const auto root = temp_dir(); + s::project hl{root / "hl", {{"include/hl/hl.hpp", "#pragma once\nnamespace hl { inline int answer() { return HL_BASE + 2; } }\n"}, + {"modules/hl.cppm", "module;\n#include \nexport module acme.hl;\nexport namespace hl { using hl::answer; }\n"}, + {"mcpp.toml", "[package]\nnamespace = \"acme\"\nname = \"hl\"\nversion = \"0.1.0\"\n\n[lib]\npath = \"modules/hl.cppm\"\n\n[build]\nsources = [\"modules/hl.cppm\"]\ninclude_dirs = [\"include\"]\ndefines = [\"HL_BASE=40\"]\n\n[targets.hl]\nkind = \"lib\"\n"}}}; + s::project app{root / "app", {{"mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[build-dependencies.acme]\nhl = { path = \"../hl\" }\n\n[targets.app]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n"}, + {"src/main.cpp", "#include \nint main() { std::printf(\"V=%d\\n\", V); }\n"}, + {"build.mcpp", "import std;\nimport mcpp;\nimport acme.hl;\nint main() { mcpp::cxxflag(std::format(\"-DV={}\", hl::answer()).c_str()); }\n"}}}; + auto b = app.mcpp({"build"}, home); + expect(fatal(e2e::succeeded(b))) << "A: a header-backed module library in [build-dependencies] did not build"; + auto v = s::last_starting(app.mcpp({"run"}, home).out, "V="); + if (v.ends_with('\r')) v.pop_back(); + expect(eq(v, std::string("V=42"))) << "A/B: the library's value did not reach the target"; + expect(s::lacks(s::text(b), "host module")) << "A: the old host-module path was reported"; + bool in_plan = false; + for (const auto& o : s::find_files(app.at("target"), R"(hl\.m\.o.*)")) { + const auto g = o.generic_string(); + in_plan = in_plan || (g.contains("/obj/") && !g.contains("/.build-mcpp/")); + } + expect(!in_plan) << "C: the build dependency was compiled into the target's plan"; + }; + + "58 preinstall mcpp deps for hooks"_test[tag{"requires:gcc"}][tag{"requires:fresh-sandbox"}] = [] { + // A package installed from an mcpp index may need its mcpp.deps before its install hook + // runs: they are installed first (library deps belong in mcpp.deps; only hook-time tools + // are xpm deps). A fake xlings records the order of the installs. + s::needs({"gcc", "fresh-sandbox"}); + const auto home = s::case_home(s::home::inherited); + const auto seed = home / "registry/subos/default/.xlings.json"; + expect(fatal(fs::is_regular_file(seed))) << "inherited toolchain has no default SubOS runtime contract"; + const auto tmp = temp_dir(); + auto package = [](std::string_view name, std::string_view description, std::string_view source, + std::string_view target, std::string_view deps) { + return std::format(R"lua(package = {{ + spec = "1", + namespace = "compat", + name = "{0}", + description = "{1}", + licenses = {{"MIT"}}, + type = "package", + xpm = {{ + linux = {{ + ["1.0.0"] = {{ + url = "https://example.invalid/{3}-1.0.0.tar.gz", + sha256 = "0000000000000000000000000000000000000000000000000000000000000000", + }}, + }}, + }}, + mcpp = {{ + language = "c++23", + import_std = false, + sources = {{ "{2}" }}, + targets = {{ ["{3}"] = {{ kind = "lib" }} }}, + deps = {{{4}}}, + }}, +}} +)lua", name, description, source, target, deps); + }; + s::project index{tmp / "local-index", {}}; + index.write("pkgs/c/compat.proto.lua", package("compat.proto", "Protocol helper dependency", "src/proto.c", "proto", "")); + index.write("pkgs/c/compat.appdep.lua", package("compat.appdep", "Package whose install hook needs mcpp deps", "src/appdep.c", "appdep", + "\n [\"compat.proto\"] = \"1.0.0\",\n ")); + const auto registry = tmp / "fake-registry"; + fs::create_directories(registry / "data"); + if (const auto xpkgs = fs::path(s::env("HOME").value_or("")) / ".mcpp/registry/data/xpkgs"; fs::is_directory(xpkgs)) + fs::create_directory_symlink(xpkgs, registry / "data/xpkgs"); + s::write_executable(tmp / "fake-bin/xlings", R"sh(#!/usr/bin/env bash +# The handshake mcpp asks of every xlings (docs/25 §5). +[[ "${1:-}" == "interface" && "${2:-}" == "--version" ]] && { echo '{"protocol_version":"1.13","version":"9999.1.1.1","capabilities":["install_packages"],"home_layouts":[2],"features":["index_dir","index_store","upstreams","payload_status","sync_index","payload_currency","binding_state","network_diagnostics","download_socket_budgets","read_only_queries"]}'; exit 0; } +set -e +if [[ "${1:-}" == "self" && "${2:-}" == "init" ]]; then + mkdir -p "${XLINGS_HOME:?}/subos/default" + cp "${FAKE_XLINGS_SUBOS_INFO:?}" \ + "$XLINGS_HOME/subos/default/.xlings.json" + exit 0 +fi +if [[ "${1:-}" == "update" || ( "${1:-}" == "interface" && "${2:-}" == "update_packages" ) ]]; then + exit 0 +fi +if [[ "${1:-}" == "interface" && "${2:-}" == "install_packages" ]]; then + args="" + while [[ $# -gt 0 ]]; do + if [[ "$1" == "--args" ]]; then args="$2"; break; fi + shift + done + printf '%s\n' "$args" >> "${FAKE_XLINGS_LOG:?}" + if [[ ! -d "${XLINGS_PROJECT_DIR:?}/.xlings/data/compat/pkgs" \ + && ! -d "${XLINGS_PROJECT_DIR:?}/data/compat/pkgs" ]]; then + echo "missing project local path index link" >&2 + find "${XLINGS_PROJECT_DIR:?}" -maxdepth 4 -type d -print >&2 2>/dev/null || true + exit 23 + fi + case "$args" in + *compat:compat.proto@1.0.0*) + install_root="${XLINGS_PROJECT_DIR:?}/.xlings/data/xpkgs/compat-x-compat.proto/1.0.0" + mkdir -p "$install_root/src" + cat > "$install_root/src/proto.c" <<'SRC' +int proto_value(void) { + return 42; +} +SRC + ;; + *compat:compat.appdep@1.0.0*) + proto_root="${XLINGS_PROJECT_DIR:?}/.xlings/data/xpkgs/compat-x-compat.proto/1.0.0" + if [[ ! -d "$proto_root" ]]; then + echo "mcpp.deps were not installed before appdep install hook" >&2 + exit 24 + fi + install_root="${XLINGS_PROJECT_DIR:?}/.xlings/data/xpkgs/compat-x-compat.appdep/1.0.0" + mkdir -p "$install_root/src" + cat > "$install_root/src/appdep.c" <<'SRC' +extern int proto_value(void); +int app_value(void) { + return proto_value(); +} +SRC + ;; + esac + printf '{"kind":"result","exitCode":0}\n' + exit 0 +fi +if [[ "${1:-}" == "install" ]]; then + echo "direct install should not be used for project path indices" >&2 + exit 31 +fi +exit 0 +)sh"); + s::project{home, {}}.write("config.toml", std::format(R"toml([xlings] +binary = "{}" +home = "{}" + +[index] +default = "mcpplibs" + +[index.repos."mcpplibs"] +url = "https://github.com/mcpp-community/mcpp-index.git" + +[cache] +search_ttl_seconds = 3600 + +[build] +default_jobs = 0 + +[toolchain] +default = "gcc@16.1.0" +)toml", (tmp / "fake-bin/xlings").generic_string(), registry.generic_string())); + s::project app{tmp / "project/app", {{"src/app.cpp", "extern \"C\" int app_value(void);\nint app_smoke(void) {\n return app_value();\n}\n"}}}; + app.write("mcpp.toml", std::format("[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[indices]\ncompat = {{ path = \"{}\" }}\n\n[dependencies.compat]\nappdep = \"1.0.0\"\n\n[targets.app]\nkind = \"lib\"\n", index.dir().generic_string())); + const auto log = tmp / "fake-xlings.log"; + auto b = app.mcpp({"build"}, {.where = s::home::inherited, + .env = {{"FAKE_XLINGS_LOG", log.string()}, + {"FAKE_XLINGS_DIRECT_LOG", (tmp / "fake-xlings-direct.log").string()}, + {"FAKE_XLINGS_SUBOS_INFO", seed.string()}}}); + expect(fatal(e2e::succeeded(b))) << "the build failed"; + const auto installs = s::read_file(log); + expect(fatal(e2e::contains(installs, "compat:compat.proto@1.0.0"))) << "compat.proto was not installed from mcpp.deps"; + const auto proto = installs.find("compat:compat.proto@1.0.0"); + const auto appdep = installs.find("compat:compat.appdep@1.0.0"); + expect(appdep != std::string::npos && proto < appdep) << "mcpp.deps should be installed before dependent package hook"; + }; +}}; + +} // namespace diff --git a/tests/e2e/build-program/graph.cpp b/tests/e2e/build-program/graph.cpp new file mode 100644 index 000000000..a8cc55881 --- /dev/null +++ b/tests/e2e/build-program/graph.cpp @@ -0,0 +1,265 @@ +// The build programs' graph (mcpp#797): a library a build program uses is a package of that graph, +// with instances, features and links of its own (formerly tests/e2e/903, 904, 905). +import std; +import mcpplibs.mtest; +import mcpp.test.support; + +using namespace mcpplibs::mtest; +namespace s = mcpp::test::support; +namespace fs = std::filesystem; + +namespace { + +const s::options inherited{.where = s::home::inherited}; + +// The last line of `mcpp run` (stdout) in `p` that starts with `prefix`. +std::string run_line(const s::project& p, std::string_view prefix, std::vector args = {"run"}) { + auto line = s::last_starting(p.mcpp(std::move(args), inherited).out, prefix); + if (line.ends_with('\r')) line.pop_back(); + return line; +} + +E2E graph{"graph", [] { + "903 one package in both tables has two instances"_test[tag{"requires:elf"}] = [] { + // mcpp#797 R2, R3: one package in [dependencies] and [build-dependencies] has two + // instances, each correct (they merged into one edge read as "build time only"). M1 both + // import it; M2 `host-module = true` beside it changes nothing; M3 a feature of the build + // dependency reaches only the program's instance; M5 a workspace; M6 the target reaches it + // through another library; M7 a dependency's own program uses it; M9 the build database + // has a target set and a `:host` set, compile_commands the target units only; M10 a header + // edit recompiles both and reruns the program. (The script read the database with python3.) + s::needs({"elf"}); + const auto root = temp_dir(); + const std::string header = "#pragma once\nnamespace hl {\ninline int answer() { return 42; }\ninline int x_on() {\n#ifdef HL_X\n return 1;\n#else\n return 0;\n#endif\n}\n}\n"; + s::project hl{root / "hl", {{"include/hl/hl.hpp", header}, + {"src/hl.cppm", "module;\n#include \nexport module hl;\nexport namespace hl { using hl::answer; using hl::x_on; }\n"}, + {"mcpp.toml", "[package]\nname = \"hl\"\nversion = \"0.1.0\"\n\n[build]\nsources = [\"src/hl.cppm\"]\ninclude_dirs = [\"include\"]\n\n[features]\nx = { defines = [\"HL_X\"] }\n\n[targets.hl]\nkind = \"lib\"\n"}}}; + const std::string program = "import std;\nimport mcpp;\nimport hl;\nint main() {\n mcpp::cxxflag(std::format(\"-DV={}\", hl::answer()).c_str());\n mcpp::cxxflag(std::format(\"-DX_IN_PROGRAM={}\", hl::x_on()).c_str());\n}\n"; + const std::string target_main = "#include \nimport hl;\nint main() { std::printf(\"V=%d target=%d xprog=%d xtarget=%d\\n\", V, hl::answer(), X_IN_PROGRAM, hl::x_on()); }\n"; + auto app = [&](const fs::path& dir, std::string_view extra) { + const auto name = dir.filename().string(); + return s::project{dir, {{"mcpp.toml", std::format("[package]\nname = \"{0}\"\nversion = \"0.1.0\"\n{1}\n[targets.{0}]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n", name, extra)}, + {"src/main.cpp", target_main}, + {"build.mcpp", program}}}; + }; + // M1, M3. + const std::string m1_tables = "[dependencies]\nhl = { path = \"../hl\" }\n[build-dependencies]\nhl = { path = \"../hl\", features = [\"x\"] }"; + auto m1 = app(root / "m1", m1_tables); + expect(fatal(e2e::succeeded(m1.mcpp({"build"}, inherited)))) << "M1: one package in both tables did not build"; + expect(eq(run_line(m1, "V="), std::string("V=42 target=42 xprog=1 xtarget=0"))) << "M1/M3"; + // M2. + m1.write("mcpp.toml", std::regex_replace(m1.read("mcpp.toml"), std::regex(R"(features = \["x"\] \})"), R"(features = ["x"], host-module = true })")); + auto b2 = m1.mcpp({"build"}, inherited); + expect(fatal(e2e::succeeded(b2))) << "M2: host-module = true beside the entry broke the build"; + expect(s::lacks(s::text(b2), "deprecated")) << "M2: stating the default was reported"; + expect(eq(run_line(m1, "V="), std::string("V=42 target=42 xprog=1 xtarget=0"))) << "M2"; + // M9. + auto db = m1.mcpp({"emit", "build-database", "--format", "json"}, inherited); + expect(fatal(e2e::succeeded(db))) << "M9: emit build-database failed"; + const auto doc = s::json(db.out)["data"]["database"]; + std::map sets; + for (const auto& x : doc["sets"]) sets[x["name"].get()] = x; + expect(sets.contains("hl") || std::ranges::any_of(sets, [](const auto& kv) { return kv.first.ends_with(".hl"); })) << "M9: no target set for hl"; + auto host = std::ranges::find_if(sets, [](const auto& kv) { return kv.first.ends_with("hl:host"); }); + expect(fatal(host != sets.end())) << "M9: no :host set for hl"; + expect(host->second["ide"]["configuration"] == "build-program") << host->second.dump(); + expect(std::ranges::any_of(sets, [](const auto& kv) { return kv.first.ends_with(":build"); })) << "M9: no :build set"; + auto cc = m1.mcpp({"emit", "build-database", "--spec", "compile-commands"}, inherited); + expect(s::lacks(cc.out, "build.mcpp")) << "M9: compile_commands describes build.mcpp"; + // M10. + std::this_thread::sleep_for(std::chrono::seconds(1)); + hl.write("include/hl/hl.hpp", std::regex_replace(header, std::regex("return 42;"), "return 43;")); + expect(fatal(e2e::succeeded(m1.mcpp({"build"}, inherited)))) << "M10: the build after a header edit failed"; + expect(eq(run_line(m1, "V="), std::string("V=43 target=43 xprog=1 xtarget=0"))) << "M10: an instance kept the old header"; + hl.write("include/hl/hl.hpp", header); + // M5. + s::project ws{root / "ws", {{"mcpp.toml", "[workspace]\nmembers = [\"a\", \"b\"]\n"}}}; + auto a = app(root / "ws/a", "[dependencies]\nhl = { path = \"../../hl\" }"); + a.write("src/main.cpp", "#include \nimport hl;\nint main() { std::printf(\"V=%d\\n\", hl::answer()); }\n"); + a.remove("build.mcpp"); + auto b = app(root / "ws/b", "[build-dependencies]\nhl = { path = \"../../hl\" }"); + b.write("src/main.cpp", "#include \nint main() { std::printf(\"V=%d\\n\", V); }\n"); + expect(fatal(e2e::succeeded(ws.mcpp({"build", "--workspace"}, inherited)))) << "M5: the workspace did not build"; + expect(eq(run_line(ws, "V=", {"run", "-p", "a"}), std::string("V=42"))) << "M5: member a"; + expect(eq(run_line(ws, "V=", {"run", "-p", "b"}), std::string("V=42"))) << "M5: member b"; + // M6, M7. + s::project mid{root / "mid", {{"mcpp.toml", "[package]\nname = \"mid\"\nversion = \"0.1.0\"\n[dependencies]\nhl = { path = \"../hl\" }\n[build-dependencies]\nhl = { path = \"../hl\" }\n[targets.mid]\nkind = \"lib\"\n"}, + {"src/mid.cppm", "export module mid;\nimport hl;\nexport int via_mid() { return hl::answer() + MID; }\n"}, + {"build.mcpp", "import std;\nimport mcpp;\nimport hl;\nint main() { mcpp::cxxflag(std::format(\"-DMID={}\", hl::answer() - 42).c_str()); }\n"}}}; + auto m6 = app(root / "m6", "[dependencies]\nmid = { path = \"../mid\" }\n[build-dependencies]\nhl = { path = \"../hl\" }"); + m6.write("src/main.cpp", "#include \nimport mid;\nint main() { std::printf(\"V=%d mid=%d\\n\", V, via_mid()); }\n"); + expect(fatal(e2e::succeeded(m6.mcpp({"build"}, inherited)))) << "M6/M7: a library reached through another one, used by two programs, did not build"; + expect(eq(run_line(m6, "V="), std::string("V=42 mid=42"))) << "M6/M7"; + }; + + "904 a build dependency feature stays out of the target"_test[tag{"requires:elf"}] = [] { + // mcpp#797 R3: a feature one package requests of a library in its [build-dependencies] is + // a request of the program's instance: the target instance does not have it (A), the + // program's does (B). + s::needs({"elf"}); + const auto root = temp_dir(); + s::project hl{root / "hl", {{"src/hl.cppm", "export module hl;\nexport namespace hl {\ninline int x_on() {\n#ifdef HL_X\n return 1;\n#else\n return 0;\n#endif\n}\n}\n"}, + {"mcpp.toml", "[package]\nname = \"hl\"\nversion = \"0.1.0\"\n[features]\nx = { defines = [\"HL_X\"] }\n[targets.hl]\nkind = \"lib\"\n"}}}; + s::project mid{root / "mid", {{"mcpp.toml", "[package]\nname = \"mid\"\nversion = \"0.1.0\"\n[build-dependencies]\nhl = { path = \"../hl\", features = [\"x\"] }\n[targets.mid]\nkind = \"lib\"\n"}, + {"src/mid.cppm", "export module mid;\nexport int mid_x() { return MID_X; }\n"}, + {"build.mcpp", "import std;\nimport mcpp;\nimport hl;\nint main() { mcpp::cxxflag(std::format(\"-DMID_X={}\", hl::x_on()).c_str()); }\n"}}}; + s::project app{root / "app", {{"mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n[dependencies]\nhl = { path = \"../hl\" }\nmid = { path = \"../mid\" }\n[targets.app]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n"}, + {"src/main.cpp", "#include \nimport hl;\nimport mid;\nint main() { std::printf(\"target=%d program=%d\\n\", hl::x_on(), mid_x()); }\n"}}}; + expect(fatal(e2e::succeeded(app.mcpp({"build"}, inherited)))) << "the build failed"; + expect(eq(run_line(app, "target="), std::string("target=0 program=1"))) << "A/B"; + }; + + "905 a build program links its libraries whole"_test[tag{"requires:elf"}] = [] { + // mcpp#797 Q4/Q5: a library a build program uses is compiled and linked whole: its + // implementation units, its C sources and its own dependencies. + s::needs({"elf"}); + const auto root = temp_dir(); + s::project base{root / "base", {{"src/base.cppm", "export module acme.base;\nexport int base_two();\n"}, + {"src/base_impl.cpp", "module acme.base;\nint base_two() { return 2; }\n"}, + {"mcpp.toml", "[package]\nname = \"base\"\nversion = \"0.1.0\"\n[targets.base]\nkind = \"lib\"\n"}}}; + s::project lib{root / "lib", {{"src/lib.cppm", "export module lib;\nexport int lib_value();\n"}, + {"src/lib_impl.cpp", "module lib;\nimport acme.base;\nextern \"C\" int c_forty();\nint lib_value() { return c_forty() + base_two(); }\n"}, + {"src/forty.c", "int c_forty(void) { return 40; }\n"}, + {"mcpp.toml", "[package]\nname = \"lib\"\nversion = \"0.1.0\"\n[dependencies]\nbase = { path = \"../base\" }\n[targets.lib]\nkind = \"lib\"\n"}}}; + s::project app{root / "app", {{"mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n[build-dependencies]\nlib = { path = \"../lib\" }\n[targets.app]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n"}, + {"src/main.cpp", "#include \nint main() { std::printf(\"V=%d\\n\", V); }\n"}, + {"build.mcpp", "import std;\nimport mcpp;\nimport lib;\nint main() { mcpp::cxxflag(std::format(\"-DV={}\", lib_value()).c_str()); }\n"}}}; + expect(fatal(e2e::succeeded(app.mcpp({"build"}, inherited)))) << "a library with implementation units, C sources and a dependency did not link into the build program"; + expect(eq(run_line(app, "V="), std::string("V=42"))) << "the program computed another value"; + }; + + "906 a build program is compiled when its graph changes"_test[tag{"requires:elf"}] = [] { + // mcpp#797 R6: a build program is compiled again exactly when its identity moves, and a + // build whose programs stand does not plan their graph. A a header edit reruns it with the + // new value; B nothing changed, the identity current, no graph planned; C both stated in + // the verbose log. + s::needs({"elf"}); + const auto root = temp_dir(); + s::project hl{root / "hl", {{"src/value.h", "#define HL_VALUE 42\n"}, + {"src/hl.cppm", "module;\n#include \"value.h\"\nexport module hl;\nexport int hl_value() { return HL_VALUE; }\n"}, + {"mcpp.toml", "[package]\nname = \"hl\"\nversion = \"0.1.0\"\n[targets.hl]\nkind = \"lib\"\n"}}}; + s::project app{root / "app", {{"mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n[build-dependencies]\nhl = { path = \"../hl\" }\n[targets.app]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n"}, + {"src/main.cpp", "#include \nint main() { std::printf(\"V=%d\\n\", V); }\n"}, + {"build.mcpp", "import std;\nimport mcpp;\nimport hl;\nint main() { mcpp::cxxflag(std::format(\"-DV={}\", hl_value()).c_str()); }\n"}}}; + expect(fatal(e2e::succeeded(app.mcpp({"build"}, inherited)))) << "the first build failed"; + expect(eq(run_line(app, "V="), std::string("V=42"))) << "first value"; + // B, C: a swept source touched so that planning runs. + std::this_thread::sleep_for(std::chrono::seconds(1)); + fs::last_write_time(app.at("src/main.cpp"), fs::file_time_type::clock::now()); + auto b2 = app.mcpp({"build", "--verbose"}, inherited); + expect(fatal(e2e::succeeded(b2))) << "B: the second build failed"; + expect(s::has_line(s::text(b2), "build-program: app: identity [0-9a-f]+ current")) << "C: the log does not state the identity stood"; + expect(s::lacks(s::text(b2), "host-graph: build programs in")) << "B: the build programs' graph was planned although nothing changed"; + // A. + std::this_thread::sleep_for(std::chrono::seconds(1)); + hl.write("src/value.h", "#define HL_VALUE 43\n"); + auto b3 = app.mcpp({"build", "--verbose"}, inherited); + expect(fatal(e2e::succeeded(b3))) << "A: the build after the header edit failed"; + expect(s::has_line(s::text(b3), "build-program: app: identity [0-9a-f]+ -> [0-9a-f]+, compiled")) << "C: the log does not state why the program was compiled"; + expect(eq(run_line(app, "V="), std::string("V=43"))) << "A: the program ran on the old header"; + }; + + "908 what a build dependency asks for"_test[tag{"requires:elf"}] = [] { + // SPEC-004 §12 item 5: a [build-dependencies] entry with neither `tools` nor `host-module` + // gives the library (A); `tools` alone gives the program and not the library, refused by + // name before the compiler (B); both keys give both (C); `host-module = false` declines + // the library (D). + s::needs({"elf"}); + const auto root = temp_dir(); + s::project gen{root / "gen", {{"src/gen.cppm", "export module acme.gen;\nexport int gen_value() { return 42; }\n"}, + {"tools/gen_main.cpp", "#include \nint main() { std::puts(\"generated\"); }\n"}, + {"mcpp.toml", "[package]\nnamespace = \"acme\"\nname = \"gen\"\nversion = \"0.1.0\"\n\n[build]\nsources = [\"src/gen.cppm\"]\n\n[targets.gen]\nkind = \"lib\"\n\n[targets.gentool]\nkind = \"bin\"\nmain = \"tools/gen_main.cpp\"\n"}}}; + s::project app{root / "app", {{"src/main.cpp", "#include \nint main() { std::printf(\"V=%d\\n\", V); }\n"}, + {"build.mcpp", "import std;\nimport mcpp;\nimport acme.gen;\nint main() { mcpp::cxxflag(std::format(\"-DV={}\", gen_value()).c_str()); }\n"}}}; + auto manifest = [&](std::string_view extra) { + app.write("mcpp.toml", std::format("[package]\nname = \"app\"\nversion = \"0.1.0\"\n[build-dependencies.acme]\ngen = {{ path = \"../gen\"{} }}\n[targets.app]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n", extra)); + }; + manifest(""); + expect(e2e::succeeded(app.mcpp({"build"}, inherited))) << "A: the default entry did not give the library"; + manifest(", tools = [\"gentool\"]"); + auto b = app.mcpp({"build"}, inherited); + expect(b.started && b.status != 0) << "B: a tools-only entry gave the library"; + expect(e2e::contains(s::text(b), "imports 'acme.gen'")) << "B: the refusal does not name the module"; + expect(e2e::contains(s::text(b), "host-module = true")) << "B: the refusal does not name the key"; + expect(!s::contains_nocase(s::text(b), "failed to read compiled module")) << "B: the compiler was reached"; + manifest(", tools = [\"gentool\"], host-module = true"); + expect(e2e::succeeded(app.mcpp({"build"}, inherited))) << "C: tools and host-module = true did not give both"; + manifest(", host-module = false"); + auto d = app.mcpp({"build"}, inherited); + expect(d.started && d.status != 0) << "D: host-module = false still gave the library"; + expect(e2e::contains(s::text(d), "imports 'acme.gen'")) << "D: the refusal does not name the module"; + }; + + "910 a build plugin is a package of the build program"_test[tag{"requires:elf"}] = [] { + // SPEC-007 §10: a unit importing `mcpp.core` (or `mcpp`) is a build-plugin unit. A a plugin + // on a plugin and a library serves a consumer's program; B it builds as the root of a + // build in the build-program configuration, its tests run, the engine package's entry + // holds `mcpp.core`; C --target for another machine is refused; D a plugin reached through + // [dependencies] of a target is refused naming the chain; E a program importing mcpp is + // refused; F pack refuses a plugin; G a library the plugin depends on is importable by its + // consumers' programs only when handed on with `reexport = true`. + s::needs({"elf"}); + const auto root = temp_dir(); + auto lib = [&](std::string_view name, std::string_view deps) { + return s::project{root / name, {{"mcpp.toml", std::format("[package]\nnamespace = \"acme\"\nname = \"{0}\"\nversion = \"0.1.0\"\n[dependencies.acme]\n{1}\n[targets.{0}]\nkind = \"lib\"\n", name, deps)}}}; + }; + lib("util", "").write("src/util.cppm", "export module acme.util;\nexport int util_base() { return 40; }\n"); + lib("core-rule", "").write("src/core-rule.cppm", "export module acme.core_rule;\nimport std;\nimport mcpp;\nexport namespace acme::core_rule {\ninline void define(std::string_view key, int value) { mcpp::define(std::format(\"{}={}\", key, value).c_str()); }\n}\n"); + auto rule = lib("rule", "core-rule = { path = \"../core-rule\" }\nutil = { path = \"../util\" }"); + rule.write("src/rule.cppm", "export module acme.rule;\nimport mcpp;\nimport acme.core_rule;\nimport acme.util;\nexport namespace acme::rule {\ninline int value() { return util_base() + 2; }\ninline void go() { acme::core_rule::define(\"V\", value()); }\n}\n"); + // A. + s::project app{root / "app", {{"mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n[build-dependencies.acme]\nrule = { path = \"../rule\" }\n[targets.app]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n"}, + {"src/main.cpp", "#include \nint main() { std::printf(\"V=%d\\n\", V); }\n"}, + {"build.mcpp", "import acme.rule;\nint main() { acme::rule::go(); }\n"}}}; + expect(fatal(e2e::succeeded(app.mcpp({"build"}, inherited)))) << "A: a plugin built on a plugin and a library did not serve the build program"; + expect(eq(run_line(app, "V="), std::string("V=42"))) << "A: value"; + bool reached = false; + for (const auto& o : s::find_files(app.at("target"), R"(.*\.m\.o)")) { + const auto g = o.generic_string(); + reached = reached || (g.contains("/obj/") && !g.contains("/.build-mcpp/") && std::regex_search(g, std::regex("rule|util"))); + } + expect(!reached) << "A: a package of the build program reached the target"; + // B. + rule.write("tests/value.cpp", "import mcpp;\nimport acme.rule;\nint main() { return acme::rule::value() == 42 ? 0 : 1; }\n"); + auto b1 = rule.mcpp({"build"}, inherited); + expect(fatal(e2e::succeeded(b1))) << "B: the plugin did not build as the root of a build"; + expect(e2e::contains(s::text(b1), "build-program")) << "B: the build does not state its configuration"; + expect(s::lacks(s::text(b1), "populate failed")) << "B: the engine package's cache entry was not filled"; + expect(!s::glob(s::case_home(s::home::inherited) / "build-cache/v1/pkg/_engine", "*/*/bmi/mcpp.core.*").empty()) + << "B: the engine package's entry lacks mcpp.core"; + auto b2 = rule.mcpp({"test"}, inherited); + expect(e2e::succeeded(b2)) << "B: the plugin's tests did not run"; + expect(e2e::contains(s::text(b2), "1 passed")) << "B: the test did not pass"; + // C. + auto c = rule.mcpp({"build", "--target", "aarch64-linux-gnu"}, inherited); + expect(c.started && c.status != 0) << "C: --target for another machine was accepted"; + expect(e2e::contains(s::text(c), "acme.rule' is a build plugin") && e2e::contains(s::text(c), "cannot apply")) << "C: the refusal does not name the plugin"; + // D. + s::project app2{root / "app2", {{"mcpp.toml", "[package]\nname = \"app2\"\nversion = \"0.1.0\"\n[dependencies.acme]\nrule = { path = \"../rule\" }\n[targets.app2]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n"}, + {"src/main.cpp", "int main() { return 0; }\n"}}}; + auto d = app2.mcpp({"build"}, inherited); + expect(d.started && d.status != 0) << "D: a plugin in [dependencies] of a target was accepted"; + expect(e2e::contains(s::text(d), "is a build plugin") && e2e::contains(s::text(d), "app2 → acme.rule") && e2e::contains(s::text(d), "[build-dependencies]")) + << "D: the refusal does not name the chain and the table"; + // E. + s::project app3{root / "app3", {{"mcpp.toml", "[package]\nname = \"app3\"\nversion = \"0.1.0\"\n[targets.app3]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n"}, + {"src/main.cpp", "import mcpp;\nint main() { return 0; }\n"}}}; + auto e = app3.mcpp({"build"}, inherited); + expect(e.started && e.status != 0) << "E: a program importing mcpp was accepted"; + expect(e2e::contains(s::text(e), "exists only in a build")) << "E: the refusal does not say why"; + // F. + auto f = rule.mcpp({"pack"}, inherited); + expect(f.started && f.status != 0) << "F: mcpp pack accepted a plugin"; + expect(e2e::contains(s::text(f), "is a build plugin")) << "F: the refusal does not say why"; + // G. + app.write("build.mcpp", "import acme.rule;\nimport acme.util;\nint main() { if (util_base() == 40) acme::rule::go(); }\n"); + auto g1 = app.mcpp({"build"}, inherited); + expect(g1.started && g1.status != 0) << "G: a library the plugin does not hand on was importable"; + expect(e2e::contains(s::text(g1), "reexport = true")) << "G: the refusal does not name reexport"; + rule.write("mcpp.toml", std::regex_replace(rule.read("mcpp.toml"), std::regex(R"(util = \{ path = "\.\./util" \})"), R"(util = { path = "../util", reexport = true })")); + expect(fatal(e2e::succeeded(app.mcpp({"build"}, inherited)))) << "G: a library the plugin hands on was refused"; + expect(eq(run_line(app, "V="), std::string("V=42"))) << "G: value"; + }; +}}; + +} // namespace diff --git a/tests/e2e/build-program/hosttools.cpp b/tests/e2e/build-program/hosttools.cpp new file mode 100644 index 000000000..a242dbc0c --- /dev/null +++ b/tests/e2e/build-program/hosttools.cpp @@ -0,0 +1,1126 @@ +// A dependency's host tools: its `bin` targets built for the host by a nested sub-build, kept in +// the tool store and handed to the consumer's build program (formerly tests/e2e/187). +import std; +import mcpplibs.mtest; +import mcpp.test.support; + +using namespace mcpplibs::mtest; +namespace s = mcpp::test::support; +namespace fs = std::filesystem; + +namespace { + +// A tool that writes a source whose function `fn` returns `value`, to its first argument. +std::string generator(std::string_view fn, int value) { + return std::format(R"cpp(#include +int main(int c, char** v) {{ + if (c < 2) return 2; + FILE* f = std::fopen(v[1], "w"); + if (!f) return 3; + std::fprintf(f, "int {}() {{ return {}; }}\n"); + std::fclose(f); + return 0; +}} +)cpp", fn, value); +} + +// A consumer's build.mcpp that runs dep_bin(`spelling`, `tool`) into a generated source; every +// spelling of `check` must resolve. +std::string runner(std::initializer_list check, std::string_view use, std::string_view tool) { + std::string out = "#include \n#include \n#include \nimport mcpp;\nint main() {\n"; + for (auto c : check) + out += std::format(" if (!*mcpp::dep_bin(\"{0}\", \"{1}\")) {{ std::fprintf(stderr, \"{0} did not resolve\\n\"); return 1; }}\n", c, tool); + out += std::format(R"cpp( const char* t = mcpp::dep_bin("{}", "{}"); + if (!t || !*t) {{ std::fprintf(stderr, "no tool path\n"); return 1; }} + std::string out = std::string(mcpp::out_dir()) + "/gen.cpp"; + std::string cmd = std::string("\"") + t + "\" \"" + out + "\""; + if (std::system(cmd.c_str()) != 0) {{ std::fprintf(stderr, "tool failed\n"); return 1; }} + mcpp::generated(out.c_str()); +}} +)cpp", use, tool); + return out; +} + +E2E hosttools{"hosttools", [] { + "187 dep host tool"_test[tag{"requires:gcc"}] = [] { + // #355: the tool is built for the host into the store, found by mcpp::dep_bin(), and what + // it generates is linked; a tool behind required_features is built with them active; a + // consumer that does not ask gets nothing; a bad name lists the bin targets; an override + // skips the build; a namespaced package answers both of its spellings. The tool store is + // the case's own (a linked home), so a stale entry cannot make a broken build look green. + s::needs({"gcc"}); + const s::options home{.where = s::home::linked}; + const auto root = temp_dir(); + s::project toolpkg{root / "toolpkg", {{"mcpp.toml", R"cpp([package] +name = "toolpkg" +version = "0.1.0" + +[build] +sources = ["src/lib.cpp"] + +[features.codegen] +sources = ["src/codegen.cpp"] + +[targets.codegen] +kind = "bin" +main = "src/codegen.cpp" +required_features = ["codegen"] +)cpp"}, + {"src/lib.cpp", "int toolpkg_lib() { return 1; }\n"}, + {"src/codegen.cpp", generator("generated_answer", 42)}}}; + const std::string app_manifest = "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies]\ntoolpkg = { path = \"../toolpkg\", tools = [\"codegen\"] }\n"; + s::project app{root / "app", {{"mcpp.toml", app_manifest}, + {"src/main.cpp", "#include \nint generated_answer();\nint main() { std::printf(\"ANSWER=%d\\n\", generated_answer()); }\n"}, + {"build.mcpp", runner({}, "toolpkg", "codegen")}}}; + auto b1 = app.mcpp({"build"}, home); + expect(fatal(e2e::succeeded(b1))) << "build with a dep host tool failed"; + expect(e2e::contains(s::text(b1), "host tool")) << "no tool-build announcement"; + expect(eq(s::last_starting(s::text(app.mcpp({"run"}, home)), "ANSWER="), std::string("ANSWER=42"))); + // The store keeps the tool: a second build does not build it again. + app.remove("target"); + auto b2 = app.mcpp({"build"}, home); + expect(fatal(e2e::succeeded(b2))) << "second build failed"; + expect(s::no_line(s::text(b2), "Building.*host tool")) << "the tool was rebuilt despite a valid store entry"; + + // Without `tools = [...]` nothing is built. + s::project plain{root / "plain", {{"mcpp.toml", "[package]\nname = \"plain\"\nversion = \"0.1.0\"\n\n[dependencies]\ntoolpkg = { path = \"../toolpkg\" }\n"}, + {"src/main.cpp", "int main() {}\n"}}}; + auto b3 = plain.mcpp({"build"}, home); + expect(fatal(e2e::succeeded(b3))) << "plain consumer build failed"; + expect(s::lacks(s::text(b3), "host tool")) << "a tool was provisioned for a consumer that never asked"; + + // A bad tool name names the request and the bin targets there are. + app.write("mcpp.toml", std::regex_replace(app_manifest, std::regex(R"(tools = \["codegen"\])"), R"(tools = ["nosuchtool"])")); + app.remove("target"); + auto b4 = app.mcpp({"build"}, home); + expect(b4.started && b4.status != 0) << "an unknown tool name was accepted"; + expect(e2e::contains(s::text(b4), "nosuchtool")) << "error does not name the request"; + expect(e2e::contains(s::text(b4), "codegen")) << "error does not list the available bin targets"; + + // MCPP_TOOL__ overrides the tool and skips its build. + app.write("mcpp.toml", app_manifest); + s::write_executable(root / "fake_codegen.sh", "#!/usr/bin/env bash\nprintf 'int generated_answer() { return 7; }\\n' > \"$1\"\n"); + app.remove("target"); + auto b5 = app.mcpp({"build"}, {.where = s::home::linked, .env = {{"MCPP_TOOL_TOOLPKG_CODEGEN", (root / "fake_codegen.sh").string()}}}); + expect(fatal(e2e::succeeded(b5))) << "build with a tool override failed"; + expect(e2e::contains(s::text(b5), "override")) << "the override was not reported"; + expect(eq(s::last_starting(s::text(app.mcpp({"run"}, home)), "ANSWER="), std::string("ANSWER=7"))) + << "the override was not actually used"; + + // A legacy dotted name answers its canonical and short spellings. + s::project ns{root / "ns", {{"mcpp.toml", "[package]\nname = \"myns.tp\"\nversion = \"0.1.0\"\n\n[build]\nsources = [\"src/lib.cpp\"]\n\n[targets.gen]\nkind = \"bin\"\nmain = \"src/gen.cpp\"\n"}, + {"src/lib.cpp", "int tp_lib(){return 1;}\n"}, + {"src/gen.cpp", generator("gv", 5)}}}; + s::project nsapp{root / "nsapp", {{"mcpp.toml", "[package]\nname = \"nsapp\"\nversion = \"0.1.0\"\n\n[dependencies]\n\"myns.tp\" = { path = \"../ns\", tools = [\"gen\"] }\n"}, + {"src/main.cpp", "#include \nint gv();\nint main(){std::printf(\"G=%d\\n\",gv());}\n"}, + {"build.mcpp", runner({"myns.tp", "tp"}, "tp", "gen")}}}; + expect(fatal(e2e::succeeded(nsapp.mcpp({"build"}, home)))) << "namespaced tool lookup failed"; + expect(eq(s::last_starting(s::text(nsapp.mcpp({"run"}, home)), "G="), std::string("G=5"))); + + // `namespace` and `name` written apart (#647 E4.3): dep_bin answers the qualified name as + // dep_dir does. + s::project ns2{root / "ns2", {{"mcpp.toml", "[package]\nname = \"tp2\"\nnamespace = \"myns2\"\nversion = \"0.1.0\"\n\n[targets.gen]\nkind = \"bin\"\nmain = \"src/gen.cpp\"\n"}, + {"src/gen.cpp", generator("gv", 5)}}}; + s::project ns2app{root / "ns2app", {{"mcpp.toml", "[package]\nname = \"ns2app\"\nversion = \"0.1.0\"\n\n[dependencies]\nmyns2.tp2 = { path = \"../ns2\", tools = [\"gen\"] }\n"}, + {"src/main.cpp", "#include \nint gv();\nint main(){std::printf(\"G2=%d\\n\",gv());}\n"}, + {"build.mcpp", runner({"myns2.tp2", "tp2"}, "myns2.tp2", "gen")}}}; + expect(fatal(e2e::succeeded(ns2app.mcpp({"build"}, home)))) << "a namespace + name package's tool was not addressable by its qualified name"; + expect(eq(s::last_starting(s::text(ns2app.mcpp({"run"}, home)), "G2="), std::string("G2=5"))); + }; + + "189 host module rules"_test[tag{"requires:gcc"}] = [] { + // `host-module = true` makes a dependency's module importable from the consumer's + // build.mcpp: a rule written once, as a C++ module in an ordinary package, compiled in the + // same command as build.mcpp. + s::needs({"gcc"}); + const auto root = temp_dir(); + auto rule = [](std::string_view param, std::string_view value) { + return std::format(R"cpp(module; +#include +export module rules; +export namespace rules {{ +inline void banner(const char* macro) {{ + std::printf("mcpp:cxxflag=-D%s=1\n", macro); +}} +inline void define_answer(int {}) {{ + std::printf("mcpp:cxxflag=-DRULE_ANSWER=%d\n", {}); +}} +}} +)cpp", param, value); + }; + s::project rules{root / "rules", {{"mcpp.toml", "[package]\nname = \"rules\"\nversion = \"0.1.0\"\n\n[targets.rules]\nkind = \"lib\"\n"}, + {"src/rules.cppm", rule("n", "n")}}}; + s::project app{root / "app", {{"mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies]\nrules = { path = \"../rules\", host-module = true }\n"}, + {"src/main.cpp", R"cpp(#include +#ifndef RULE_ANSWER +#error "the rule package's directive never reached the compile" +#endif +#ifndef RULE_BANNER_OK +#error "the second rule directive never reached the compile" +#endif +int main() { std::printf("ANSWER=%d\n", RULE_ANSWER); } +)cpp"}, + {"build.mcpp", "import mcpp;\nimport rules;\nint main() {\n rules::banner(\"RULE_BANNER_OK\");\n rules::define_answer(42);\n}\n"}}}; + auto answer = [&] { return s::last_starting(s::text(app.mcpp({"run"})), "ANSWER="); }; + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "build with a host rule module failed"; + expect(eq(answer(), std::string("ANSWER=42"))) << "the rule package's directives did not reach the build"; + // An edited rule runs build.mcpp again: the rule is part of what the helper was built from. + rules.write("src/rules.cppm", rule("n_", "n_ + 1")); + fs::last_write_time(app.at("src/main.cpp"), fs::file_time_type::clock::now()); + auto b2 = app.mcpp({"build"}); + expect(fatal(e2e::succeeded(b2))) << "rebuild after editing the rule failed"; + expect(s::has_line(s::text(b2), R"(^ *build\.mcpp .* ran [0-9])")) << "editing the rule module did not re-run build.mcpp"; + expect(eq(answer(), std::string("ANSWER=43"))) << "the edited rule did not take effect"; + + // A rule that uses `import std;` and the `import mcpp;` it wraps; the package is not also + // compiled into the consumer's target. + const auto edited = rules.read("src/rules.cppm"); + rules.write("src/rules.cppm", R"cpp(export module rules; +import std; +import mcpp; +export namespace rules { +inline void banner(const char* macro) { mcpp::define(macro); } +inline void define_answer(int n) { + mcpp::cxxflag(std::format("-DRULE_ANSWER={}", n).c_str()); +} +} +)cpp"); + app.remove("target"); + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "a rule using import std + import mcpp did not build"; + expect(eq(answer(), std::string("ANSWER=42"))) << "the std/mcpp-based rule's directives did not land"; + bool compiled_as_library = false; + for (const auto& o : s::find_files(app.at("target"), R"(.*\.o)")) + compiled_as_library = compiled_as_library || o.generic_string().contains("obj/rules/"); + expect(!compiled_as_library) << "the host-module package was also built as a normal library"; + + // mcpp#797: the module is found by the name its unit declares, wherever the file is. + rules.remove("src/rules.cppm"); + rules.write("src/elsewhere.cppm", edited); + app.remove("target"); + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "a library whose interface is not at the conventional lib root was refused"; + expect(eq(answer(), std::string("ANSWER=43"))) << "the renamed interface's directive did not land"; + }; + + "193 provision reexport"_test[tag{"requires:gcc"}] = [] { + // #359: a library stands up a toolchain on its user's behalf. Without `reexport` its tool + // stays with it (an arbitrary transitive dependency cannot put entries in your build + // program's tool namespace); with it, the consumer sees the tool, the rule module and the + // package's directory through one dependency; a feature can add the request to an edge. + s::needs({"gcc"}); + const s::options home{.where = s::home::linked}; + const auto root = temp_dir(); + s::project toolpkg{root / "toolpkg", {{"mcpp.toml", "[package]\nname = \"toolpkg\"\nversion = \"0.1.0\"\n\n[build]\nsources = [\"src/lib.cpp\"]\n\n[targets.codegen]\nkind = \"bin\"\nmain = \"src/codegen.cpp\"\n"}, + {"src/lib.cpp", "int toolpkg_lib(){return 1;}\n"}, + {"src/codegen.cpp", generator("generated_answer", 42)}}}; + s::project rulepkg{root / "rulepkg", {{"mcpp.toml", "[package]\nname = \"rulepkg\"\nversion = \"0.1.0\"\n\n[lib]\npath = \"src/rulepkg.cppm\"\n"}, + {"src/rulepkg.cppm", R"cpp(export module rulepkg; +import std; +import mcpp; +export namespace rulepkg { +bool generate() { + const char* tool = mcpp::dep_bin("toolpkg", "codegen"); + if (!tool || !*tool) { + std::println(std::cerr, "rulepkg: no codegen tool"); + return false; + } + if (std::string(mcpp::dep_dir("toolpkg")).empty()) { + std::println(std::cerr, "rulepkg: no dep_dir for toolpkg"); + return false; + } + std::string out = std::string(mcpp::out_dir()) + "/gen.cpp"; + std::string cmd = std::string("\"") + tool + "\" \"" + out + "\""; + if (std::system(cmd.c_str()) != 0) return false; + mcpp::generated(out.c_str()); + return true; +} +} +)cpp"}}}; + const std::string lib_head = "[package]\nname = \"mylib\"\nversion = \"0.1.0\"\n\n[build]\nsources = [\"src/mylib.cpp\"]\n\n"; + s::project lib{root / "lib", {{"mcpp.toml", lib_head + "[dependencies]\ntoolpkg = { path = \"../toolpkg\", tools = [\"codegen\"] }\nrulepkg = { path = \"../rulepkg\", host-module = true }\n"}, + {"src/mylib.cpp", "int mylib_fn(){return 1;}\n"}}}; + const std::string app_manifest = "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies]\nmylib = { path = \"../lib\" }\n"; + s::project app{root / "app", {{"mcpp.toml", app_manifest}, + {"src/main.cpp", "#include \nint generated_answer();\nint main() { std::printf(\"ANSWER=%d\\n\", generated_answer()); }\n"}, + {"build.mcpp", R"cpp(#include +import mcpp; +int main() { + const char* tool = mcpp::dep_bin("toolpkg", "codegen"); + std::printf("SEES_TOOL=%d\n", (tool && *tool) ? 1 : 0); + return 1; +} +)cpp"}}}; + // 1. No reexport: the program (which fails on purpose) does not see the tool. + auto b1 = app.mcpp({"build"}, home); + expect(b1.started && b1.status != 0) << "build.mcpp returned 1 but the build succeeded"; + expect(s::lacks(s::text(b1), "SEES_TOOL=1")) << "a library's tool leaked to its consumer without reexport"; + expect(e2e::contains(s::text(b1), "SEES_TOOL=0")) << "build.mcpp did not run"; + + // 2. reexport: one dependency line is enough; the rule is build-time only. + lib.write("mcpp.toml", lib_head + "[dependencies]\ntoolpkg = { path = \"../toolpkg\", tools = [\"codegen\"], reexport = true }\nrulepkg = { path = \"../rulepkg\", host-module = true, reexport = true }\n"); + app.write("build.mcpp", "import mcpp;\nimport rulepkg;\nint main() { return rulepkg::generate() ? 0 : 1; }\n"); + app.remove("target"); + expect(fatal(e2e::succeeded(app.mcpp({"build"}, home)))) << "build with re-exported provisions failed"; + expect(eq(s::last_starting(s::text(app.mcpp({"run"}, home)), "ANSWER="), std::string("ANSWER=42"))) + << "the re-exported tool's output was not linked"; + if (auto nm = s::on_path("nm")) + for (const auto& bin : s::glob(app.at("target"), "*/*/bin/app")) + expect(s::lacks(s::run(*nm, {"-C", bin.string()}).out, "rulepkg")) << "the rule package was linked into the consumer's binary"; + + // 3. A feature adds the request (and reexport) to an edge declared unconditionally. + lib.write("mcpp.toml", lib_head + "[dependencies]\ntoolpkg = { path = \"../toolpkg\" }\nrulepkg = { path = \"../rulepkg\", host-module = true, reexport = true }\n\n" + "[feature-deps.codegen]\ntoolpkg = { path = \"../toolpkg\", tools = [\"codegen\"], reexport = true }\n"); + app.write("mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies]\nmylib = { path = \"../lib\", features = [\"codegen\"] }\n"); + app.remove("target"); + expect(fatal(e2e::succeeded(app.mcpp({"build"}, home)))) << "a feature could not add a tool request to an existing dependency"; + expect(eq(s::last_starting(s::text(app.mcpp({"run"}, home)), "ANSWER="), std::string("ANSWER=42"))); + // ... and without the feature, nothing is built. + app.write("mcpp.toml", app_manifest); + app.remove("target"); + auto b4 = app.mcpp({"build"}, home); + expect(b4.started && b4.status != 0) << "the build succeeded without the codegen feature — the tool ran anyway"; + expect(s::lacks(s::text(b4), "host tool")) << "a tool was built for a consumer that did not enable the feature"; + }; + + "309 host module identity"_test[tag{"requires:gcc"}] = [] { + // A rule's module name is what its source declares, not its package's name (Clang and MSVC + // are handed an explicit name=BMI mapping, so a package-derived name built only under + // GCC); a dotted name survives whole; two rules declaring one name are refused naming both + // packages; an `mcpp.` name from another namespace warns and still builds. + s::needs({"gcc"}); + const auto root = temp_dir(); + // 1. Package `protobufgen`, module `acme.rules.protobuf`. + s::project rules{root / "rules", {{"mcpp.toml", "[package]\nname = \"protobufgen\"\nversion = \"0.1.0\"\n\n[targets.protobufgen]\nkind = \"lib\"\n"}, + {"src/protobufgen.cppm", "export module acme.rules.protobuf;\nimport std;\nimport mcpp;\nexport namespace acme::rules::protobuf {\ninline void apply() { mcpp::cxxflag(\"-DDIVERGENT_NAME_REACHED=1\"); }\n}\n"}}}; + s::project app{root / "app", {{"mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies]\nprotobufgen = { path = \"../rules\", host-module = true }\n"}, + {"src/main.cpp", "#include \n#ifndef DIVERGENT_NAME_REACHED\n#error \"the rule's directive never reached the compile\"\n#endif\nint main() { std::printf(\"DIVERGENT=%d\\n\", DIVERGENT_NAME_REACHED); }\n"}, + {"build.mcpp", "import mcpp;\nimport acme.rules.protobuf;\nint main() { acme::rules::protobuf::apply(); }\n"}}}; + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "a rule whose declared module name differs from its package name did not build"; + expect(eq(s::last_starting(s::text(app.mcpp({"run"})), "DIVERGENT="), std::string("DIVERGENT=1"))); + // The import does not discriminate on GCC (its BMIs are keyed by the declared name); the + // BMI the build programs' graph names (mcpp#797) is the evidence on every platform. + const auto bm = app.at("target/.build-mcpp/host"); + expect(!s::find_files(bm, R"(acme\.rules\.protobuf\..*)").empty()) << "the host module was not named for its DECLARED module name"; + expect(s::find_files(bm, R"(protobufgen\.(gcm|pcm|ifc))").empty()) << "a BMI is named for the PACKAGE name"; + + // 2. Two packages, one module name: refused, naming the name, both packages and both units. + for (std::string n : {"alpha", "beta"}) { + s::project dup{root / ("dup-" + n), {}}; + dup.write("mcpp.toml", std::format("[package]\nname = \"{0}\"\nnamespace = \"vendor{0}\"\nversion = \"0.1.0\"\n\n[targets.{0}]\nkind = \"lib\"\n", n)); + dup.write("src/" + n + ".cppm", std::format("export module sharedname;\nimport std;\nimport mcpp;\nexport namespace shared {{ inline void go() {{ mcpp::cxxflag(\"-DFROM_{}=1\"); }} }}\n", n)); + } + s::project dupapp{root / "dupapp", {{"mcpp.toml", "[package]\nname = \"dupapp\"\nversion = \"0.1.0\"\n\n[dependencies]\nalpha = { path = \"../dup-alpha\", host-module = true }\nbeta = { path = \"../dup-beta\", host-module = true }\n"}, + {"src/main.cpp", "int main() { return 0; }\n"}, + {"build.mcpp", "import mcpp;\nimport sharedname;\nint main() { shared::go(); }\n"}}}; + auto b2 = dupapp.mcpp({"build"}); + expect(b2.started && b2.status != 0) << "two rules declaring the same module name were accepted"; + for (auto want : {"sharedname", "vendoralpha.alpha", "vendorbeta.beta", "dup-alpha/src/alpha.cppm", "dup-beta/src/beta.cppm"}) + expect(e2e::contains(s::text(b2), want)) << "the collision diagnostic does not name '" << want << "'"; + + // 3. The reserved `mcpp.` prefix from another namespace warns, and the build succeeds. + const std::string claimer_manifest = "[package]\nname = \"claimer\"\nnamespace = \"acme\"\nversion = \"0.1.0\"\n\n[targets.claimer]\nkind = \"lib\"\n"; + s::project claimer{root / "claimer", {{"mcpp.toml", claimer_manifest}, + {"src/claimer.cppm", "export module mcpp.build.claimed;\nimport std;\nimport mcpp;\nexport namespace claimed { inline void go() { mcpp::cxxflag(\"-DCLAIMED=1\"); } }\n"}}}; + s::project claimapp{root / "claimapp", {{"mcpp.toml", "[package]\nname = \"claimapp\"\nversion = \"0.1.0\"\n\n[dependencies]\nclaimer = { path = \"../claimer\", host-module = true }\n"}, + {"src/main.cpp", "#include \n#ifndef CLAIMED\n#error \"the rule never ran\"\n#endif\nint main() { std::printf(\"CLAIMED=%d\\n\", CLAIMED); }\n"}, + {"build.mcpp", "import mcpp;\nimport mcpp.build.claimed;\nint main() { claimed::go(); }\n"}}}; + auto b3 = claimapp.mcpp({"build"}); + expect(fatal(e2e::succeeded(b3))) << "the reserved prefix must WARN, not fail the build"; + expect(e2e::contains(s::text(b3), "mcpp.build.claimed")) << "no warning naming the claimed module"; + expect(e2e::contains(s::text(b3), "acme.claimer")) << "the warning does not name the package making the claim"; + expect(eq(s::last_starting(s::text(claimapp.mcpp({"run"})), "CLAIMED="), std::string("CLAIMED=1"))); + // A package in the mcpp namespace says the same without a warning. + claimer.write("mcpp.toml", std::regex_replace(claimer_manifest, std::regex("namespace = \"acme\""), "namespace = \"mcpp\"")); + claimapp.remove("target"); + auto b4 = claimapp.mcpp({"build"}); + expect(fatal(e2e::succeeded(b4))) << "the official namespace build failed"; + expect(s::lacks(s::text(b4), "reserved for rules maintained by the mcpp project")) << "a package in the mcpp namespace was warned about its own prefix"; + }; + + "312 build rules example"_test[tag{"requires:gcc"}] = [] { + // examples/08-build-rules builds and runs: two rule packages, one depending on the other + // through its [build-dependencies], a `role = "check"` action and a generated source. It + // is copied, without what an earlier run left in it, so that a second run does not + // measure the first one's cache. + s::needs({"gcc"}); + const auto src = s::repository() / "examples/08-build-rules"; + expect(fatal(fs::is_directory(src))) << src.string() << " is missing"; + const auto ex = temp_dir() / "ex"; + fs::copy(src, ex, fs::copy_options::recursive); + for (const auto& t : s::glob(ex, "target")) fs::remove_all(t); + for (const auto& t : s::glob(ex, "*/target")) fs::remove_all(t); + for (const auto& t : s::glob(ex, "*/*/target")) fs::remove_all(t); + for (const auto& l : s::find_files(ex, R"(mcpp\.lock)")) fs::remove(l); + s::project app{ex / "app", {}}; + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "the example did not build"; + expect(eq(s::last_line(app.mcpp({"run"}).out), std::string("hello from an embedded asset"))); + // The rules are build-time only, read on the artifact; the host modules' store under + // .build-mcpp has an obj/ of its own and is not the consumer's. + std::vector objs; + for (const auto& o : s::find_files(app.at("target"), R"(.*\.o)")) { + const auto g = o.generic_string(); + if (g.contains("obj") && !g.contains("/.build-mcpp/")) objs.push_back(o.filename().string()); + } + std::ranges::sort(objs); + expect(eq(objs, std::vector{"embed_greeting.o", "main.o"})) << "a rule package reached the link, or the denominator is wrong"; + // The generated source is a graph node: an edited input runs its edge again. + std::this_thread::sleep_for(std::chrono::seconds(1)); + app.write("assets/greeting.txt", "a different embedded asset\n"); + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "rebuild failed"; + expect(eq(s::last_line(app.mcpp({"run"}).out), std::string("a different embedded asset"))) + << "editing the asset did not re-run the embed action"; + // mcpp writes the check's stamp when its command succeeds (ninja would otherwise run the + // edge again on every build, and stay green); a failing check fails the build. + expect(eq(s::find_files(app.at("target"), R"(.*\.stamp)").size(), std::size_t{1})) << "expected exactly one check stamp"; + auto b3 = app.mcpp({"build"}); + expect(fatal(e2e::succeeded(b3))) << "no-op rebuild failed"; + expect(s::lacks(s::text(b3), "CHECK")) << "the check re-ran with nothing changed — its stamp is not satisfying the edge"; + app.write("src/main.cpp", "int main() { goto done; done: return 0; }\n"); + auto b4 = app.mcpp({"build"}); + expect(b4.started && b4.status != 0) << "a failing check did not fail the build"; + expect(e2e::contains(s::text(b4), "goto")) << "the build failed without reporting what the check found"; + }; + + "624 a missing host module names the key"_test[tag{"requires:elf"}][tag{"requires:gcc"}] = [] { + // An import no dependency provides is refused by name before the compiler is reached + // (whose message names neither the package nor the key); the names that can compile are + // a closed set. The fix the message names is the fix that works. + s::needs({"elf", "gcc"}); + const auto root = temp_dir(); + s::project rule{root / "rule", {{"src/rule.cppm", "export module rule;\nexport namespace testrule { inline int answer() { return 42; } }\n"}, + {"mcpp.toml", "[package]\nname = \"rule\"\nversion = \"0.1.0\"\n\n[language]\nstandard = \"c++23\"\nmodules = true\nimport_std = true\n\n[build]\nsources = [\"src/rule.cppm\"]\n\n[targets.rule]\nkind = \"lib\"\n"}}}; + auto manifest = [](std::string_view host) { + return std::format("[package]\nname = \"consumer\"\nversion = \"0.1.0\"\n\n[language]\nstandard = \"c++23\"\nmodules = true\nimport_std = true\n\n" + "[build-dependencies]\nrule = {{ path = \"../rule\", host-module = {} }}\n\n[targets.consumer]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n", host); + }; + s::project app{root / "app", {{"src/main.cpp", "int main() { return 0; }\n"}, + {"build.mcpp", "import std;\nimport mcpp;\nimport rule;\nint main() { return testrule::answer() == 42 ? 0 : 1; }\n"}, + {"mcpp.toml", manifest("false")}}}; + auto r = app.mcpp({"build"}); + const auto out = s::text(r); + expect(r.started && r.status != 0) << "the build succeeded with an unprovided import"; + expect(e2e::contains(out, "imports 'rule'")) << "the refusal does not name the module"; + expect(e2e::contains(out, "host-module = true")) << "the refusal does not name the key"; + expect(e2e::contains(out, "rule (in [build-dependencies])")) << "the refusal does not name the candidate dependency"; + expect(!s::contains_nocase(out, "failed to read compiled module")) << "the compiler was reached; the check must run first"; + app.write("mcpp.toml", manifest("true")); + expect(e2e::succeeded(app.mcpp({"build"}))) << "adding host-module = true did not make it build"; + }; + + "633 host modules are ordered by what they import"_test[tag{"requires:gcc"}] = [] { + // A host-module package's units are compiled in the order their imports require, not in + // the order their paths sort; with no internal imports, path order survives. The reading + // is a value the modules compute together, printed through mcpp::warning. + s::needs({"gcc"}); + const auto root = temp_dir(); + const std::string head = "[package]\nname = \"probe\"\nnamespace = \"ex\"\nversion = \"0.1.0\"\n\n[language]\nstandard = \"c++23\"\nmodules = true\n\n[build]\n"; + const std::string tail = "\n[features]\ndefault = []\nthing = { sources = [\"rules/thing.cppm\"] }\n"; + // Leg 1: import order is the reverse of path order. + s::project probe{root / "probe", {{"mcpp.toml", head + "sources = [\"src/probe.cppm\", \"src/mm_mid.cppm\", \"src/zz_base.cppm\"]\n" + tail}, + {"src/probe.cppm", "export module probe;\nexport namespace probe { inline int root() { return 1000; } }\n"}, + {"src/zz_base.cppm", "export module probe.base;\nexport namespace probe::base { inline int value() { return 40; } }\n"}, + {"src/mm_mid.cppm", "export module probe.mid;\nimport probe.base;\nexport namespace probe::mid { inline int value() { return probe::base::value() + 1; } }\n"}, + {"rules/thing.cppm", "export module probe.rules.thing;\nimport probe;\nimport probe.mid;\nexport namespace probe::rules::thing {\ninline int value() { return probe::root() + probe::mid::value() + 1; }\n}\n"}}}; + s::project app{root / "app", {{"mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[language]\nstandard = \"c++23\"\nmodules = true\n\n[build-dependencies.ex]\nprobe = { path = \"../probe\", features = [\"thing\"], host-module = true }\n"}, + {"src/main.cpp", "int main() { return 0; }\n"}, + {"build.mcpp", "#include \nimport mcpp;\nimport probe.rules.thing;\nint main() {\n const std::string m = \"CHAIN=\" + std::to_string(probe::rules::thing::value());\n mcpp::warning(m.c_str());\n}\n"}}}; + auto b1 = app.mcpp({"build"}); + expect(fatal(e2e::succeeded(b1))) << "a package whose units import each other was compiled in path order"; + expect(e2e::contains(s::text(b1), "CHAIN=1042")) << "the chain did not compute 1000 + 41 + 1"; + // Leg 2: nothing constrains the order. + probe.remove("src/mm_mid.cppm"); + probe.remove("src/zz_base.cppm"); + probe.write("mcpp.toml", head + "sources = [\"src/probe.cppm\", \"src/aa_one.cppm\", \"src/bb_two.cppm\"]\n" + tail); + probe.write("src/aa_one.cppm", "export module probe.one;\nexport namespace probe::one { inline int value() { return 7; } }\n"); + probe.write("src/bb_two.cppm", "export module probe.two;\nexport namespace probe::two { inline int value() { return 35; } }\n"); + probe.write("rules/thing.cppm", "export module probe.rules.thing;\nimport probe;\nimport probe.one;\nimport probe.two;\nexport namespace probe::rules::thing {\ninline int value() { return probe::root() + probe::one::value() + probe::two::value(); }\n}\n"); + app.remove("target"); + auto b2 = app.mcpp({"build"}); + expect(fatal(e2e::succeeded(b2))) << "a package with no internal imports stopped building"; + expect(e2e::contains(s::text(b2), "CHAIN=1042")) << "the unconstrained case did not compute 1000 + 7 + 35"; + }; + + "665 a host tool follows its source"_test[tag{"requires:gcc"}] = [] { + // #630 item 6: the tool store's key carries a stamp of a `path` package's tree and the + // resolved commit of a `git` one, so an edit under an unchanged version reaches the + // consumer, so does its reversal, and an unchanged tree (or a commit seen before) is not + // rebuilt. The store is the case's own. + s::needs({"gcc"}); + const s::options home{.where = s::home::linked}; + const auto root = temp_dir(); + auto write_tool = [&](const fs::path& dir, int value) { + s::project t{dir, {{"mcpp.toml", "[package]\nname = \"toolpkg\"\nversion = \"0.1.0\"\n\n[targets.gen]\nkind = \"bin\"\nmain = \"src/gen.cpp\"\n"}, + {"src/gen.cpp", generator("generated_answer", value)}}}; + }; + auto write_app = [&](const fs::path& dir, const std::string& dep) { + return s::project{dir, {{"mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies]\n" + dep + "\n"}, + {"src/main.cpp", "#include \nint generated_answer();\nint main() { std::printf(\"ANSWER=%d\\n\", generated_answer()); }\n"}, + {"build.mcpp", runner({}, "toolpkg", "gen")}}}; + }; + // Builds from nothing; the program's answer and whether the tool was built. + auto answer = [&](const s::project& p) { + p.remove("target"); + auto b = p.mcpp({"build"}, home); + expect(fatal(e2e::succeeded(b))) << "build failed"; + return std::pair{s::last_starting(s::text(p.mcpp({"run"}, home)), "ANSWER="), + bool(s::has_line(s::text(b), "Building.*host tool"))}; + }; + auto second = [] { std::this_thread::sleep_for(std::chrono::seconds(1)); }; + write_tool(root / "toolpkg", 41); + auto app = write_app(root / "app", "toolpkg = { path = \"../toolpkg\", tools = [\"gen\"] }"); + auto [a1, built1] = answer(app); + expect(eq(a1, std::string("ANSWER=41"))); + expect(built1) << "the first build did not build the tool"; + second(); + auto [a2, built2] = answer(app); + expect(eq(a2, std::string("ANSWER=41"))); + expect(!built2) << "an unchanged path tool was rebuilt"; + second(); + write_tool(root / "toolpkg", 42); + auto [a3, built3] = answer(app); + expect(eq(a3, std::string("ANSWER=42"))) << "an edit to a path tool did not reach the consumer"; + expect(built3) << "the edit did not rebuild the tool"; + second(); + write_tool(root / "toolpkg", 41); + auto [a4, built4] = answer(app); + expect(eq(a4, std::string("ANSWER=41"))) << "the reversal did not reach the consumer"; + expect(built4) << "the reversal did not rebuild the tool"; + + // A `git` tool: keyed by the resolved commit, each commit's entry reused. + const auto git_path = s::on_path("git"); + expect(fatal(git_path.has_value())) << "git is not on PATH"; + const auto git = [&](std::vector args) { + args.insert(args.begin(), {"-C", (root / "gittool").string(), "-c", "user.name=t", "-c", "user.email=t@t"}); + auto r = s::run(*git_path, args); + expect(fatal(e2e::succeeded(r))) << "git failed"; + return s::last_line(r.out); + }; + write_tool(root / "gittool", 7); + git({"init", "-q", "-b", "main"}); + git({"add", "-A"}); + git({"commit", "-qm", "seven"}); + const auto a = git({"rev-parse", "HEAD"}); + write_tool(root / "gittool", 8); + git({"commit", "-qam", "eight"}); + const auto b = git({"rev-parse", "HEAD"}); + auto pin = [&](const std::string& rev) { + return std::format("toolpkg = {{ git = \"file://{}\", rev = \"{}\", tools = [\"gen\"] }}", (root / "gittool").generic_string(), rev); + }; + auto gapp = write_app(root / "gapp", pin(a)); + expect(eq(answer(gapp).first, std::string("ANSWER=7"))) << "git rev A"; + gapp = write_app(root / "gapp", pin(b)); + auto [g2, gbuilt2] = answer(gapp); + expect(eq(g2, std::string("ANSWER=8"))) << "git rev B"; + expect(gbuilt2) << "moving the pin to B did not rebuild the tool"; + gapp = write_app(root / "gapp", pin(a)); + auto [g3, gbuilt3] = answer(gapp); + expect(eq(g3, std::string("ANSWER=7"))) << "git rev A again"; + expect(!gbuilt3) << "the entry for commit A was not reused"; + }; + + "698 a tool source under its dependency has a bounded address"_test = [] { + // #641 item 3: a source a build program takes from under its dependency's root has the + // object address obj//__pkg//, which does not + // grow with the distance between the packages (A); such a tool builds and runs through + // `tools = [...]` (B), also with the file nested deep, which on Windows passes 260 + // characters in the tool store's scratch (C: asserted wherever the runner enables long + // paths; the readings say which case this runner is). + const auto root = temp_dir(); + auto write_dep = [&](const fs::path& dir, std::string_view inner) { + s::project d{dir, {{"mcpp.toml", "[package]\nname = \"dep\"\nversion = \"0.1.0\"\n\n[build]\nsources = [\"src/*.cpp\"]\n\n[targets.dep]\nkind = \"lib\"\n"}, + {"src/dep.cpp", "int dep_base() { return 40; }\n"}}}; + d.write(std::string(inner) + "/helper.cpp", "int dep_base(); int helper_answer() { return dep_base() + 2; }\n"); + }; + auto write_installer = [&](const fs::path& dir, std::string_view dep, std::string_view inner) { + return s::project{dir, {{"mcpp.toml", std::format("[package]\nname = \"installer\"\nversion = \"0.1.0\"\n\n[dependencies]\ndep = {{ path = \"{}\" }}\n\n[targets.installer-app]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n", dep)}, + {"src/main.cpp", R"cpp(#include +int helper_answer(); +int main(int argc, char** argv) { + if (argc < 2) { std::printf("HELPER=%d\n", helper_answer()); return 0; } + FILE* f = std::fopen(argv[1], "w"); + if (!f) return 3; + std::fprintf(f, "int generated_answer() { return %d; }\n", helper_answer()); + std::fclose(f); + return 0; +} +)cpp"}, + {"build.mcpp", std::format("#include \nimport mcpp;\nint main() {{\n const char* dep = mcpp::dep_dir(\"dep\");\n if (!dep || !*dep) return 1;\n mcpp::source((std::string(dep) + \"/{}/helper.cpp\").c_str());\n}}\n", inner)}}}; + }; + auto write_app = [&](const fs::path& dir, std::string_view installer) { + return s::project{dir, {{"mcpp.toml", std::format("[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies]\ninstaller = {{ path = \"{}\", tools = [\"installer-app\"] }}\n", installer)}, + {"src/main.cpp", "#include \nint generated_answer();\nint main() { std::printf(\"ANSWER=%d\\n\", generated_answer()); }\n"}, + {"build.mcpp", R"cpp(#include +#include +#include +import mcpp; +int main() { + const char* tool = mcpp::dep_bin("installer", "installer-app"); + if (!tool || !*tool) { std::fprintf(stderr, "no tool path\n"); return 1; } + std::string out = std::string(mcpp::out_dir()) + "/gen.cpp"; + std::string cmd = std::string("\"") + tool + "\" \"" + out + "\""; +#ifdef _WIN32 + cmd = "\"" + cmd + "\""; +#endif + if (std::system(cmd.c_str()) != 0) { std::fprintf(stderr, "tool failed\n"); return 1; } + mcpp::generated(out.c_str()); +} +)cpp"}}}; + }; + // The helper's object address, read from the plan ninja was given. + auto helper_address = [](const s::project& p) { + std::smatch m; + for (const auto& n : s::glob(p.at("target"), "*/*/build.ninja")) { + const auto g = s::read_file(n); + if (std::regex_search(g, m, std::regex(R"(obj/[^ :|\n]*helper\.o(bj)?)"))) + return m.str(); + } + return std::string(); + }; + // A. + write_dep(root / "fw/dep", "platform/impl"); + auto near_inst = write_installer(root / "fw/tools/installer", "../../dep", "platform/impl"); + expect(fatal(e2e::succeeded(near_inst.mcpp({"build"})))) << "the installer did not build at its own root"; + const auto near = helper_address(near_inst); + expect(fatal(!near.empty())) << "no object address for helper.cpp in the installer's plan"; + expect(s::lacks(near, "__up")) << "the address climbs with __up"; + // The owner's slug is its qualified name (`mcpplibs.dep` for a path package with no + // namespace), spelled with `_`. + expect(e2e::matches(near, R"(^obj/installer/__pkg/.*dep/platform/impl/helper\.o(bj)?$)")) + << "the address does not name the owning package and the path inside it"; + auto deep_inst = write_installer(root / "fw/tools/windows/nested/installer", "../../../../dep", "platform/impl"); + expect(fatal(e2e::succeeded(deep_inst.mcpp({"build"})))) << "the deeper installer did not build"; + expect(eq(helper_address(deep_inst), near)) << "moving the installer two levels deeper changed the address"; + expect(eq(s::last_starting(s::text(deep_inst.mcpp({"run"})), "HELPER="), std::string("HELPER=42"))) + << "the installer does not run its dependency's helper"; + // B. + auto app = write_app(root / "app", "../fw/tools/windows/nested/installer"); + auto b = app.mcpp({"build"}); + expect(fatal(e2e::succeeded(b))) << "the consumer did not build the tool"; + expect(e2e::contains(s::text(b), "host tool")) << "no host tool was built"; + expect(eq(s::last_starting(s::text(app.mcpp({"run"})), "ANSWER="), std::string("ANSWER=42"))); + // C. + const std::string deep = "platform/windows/installer_interface_components/generated_resources_and_views/bootstrapper_application_integration"; + write_dep(root / "deep/dep", deep); + auto c_inst = write_installer(root / "deep/tools/installer", "../../dep", deep); + auto c_app = write_app(root / "deep/app", "../tools/installer"); + std::smatch m; + const auto env = c_inst.mcpp({"self", "env"}).out; + const std::string cache_dir = std::regex_search(env, m, std::regex("(^|\n)build cache *= *([^\r\n]*)")) ? m[2].str() : ""; + expect(fatal(e2e::succeeded(c_inst.mcpp({"build"})))) << "the deep installer did not build at its own root"; + const auto deep_addr = helper_address(c_inst); + const auto builds = s::glob(c_inst.at("target"), "*/*"); + const auto triple_fp = builds.empty() ? std::string() : builds.front().lexically_relative(c_inst.dir()).generic_string() + "/"; + const auto longest = cache_dir.size() + std::string_view("/tool/.build/0123456789abcdef/").size() + triple_fp.size() + deep_addr.size() + 8; + std::println("READING C cache directory: {}", cache_dir); + std::println("READING C longest sub-build path (estimated, characters): {}", longest); + std::string long_paths = "unknown"; + if (s::on_windows()) { + auto r = e2e::run({.argv = {"powershell", "-NoProfile", "-Command", + R"((Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem').LongPathsEnabled)"}}); + long_paths = s::last_line(r.out); + if (long_paths.ends_with('\r')) long_paths.pop_back(); + if (long_paths.empty()) long_paths = "unreadable"; + } + std::println("READING C LongPathsEnabled: {}", long_paths); + auto c = c_app.mcpp({"build"}); + std::println("READING C deep tool build exit: {}", c.status); + if (c) { + expect(eq(s::last_starting(s::text(c_app.mcpp({"run"})), "ANSWER="), std::string("ANSWER=42"))) + << "the deep tool's output did not reach the consumer"; + } else if (s::on_windows() && long_paths != "1") { + std::println("READING C the runner does not enable long paths; the failure is recorded, not asserted"); + } else { + expect(e2e::succeeded(c)) << "the deep tool did not build"; + } + }; + + "712 a tool package contributes nothing to its consumer"_test[tag{"requires:gcc"}] = [] { + // #649 E6: a package whose targets are all programs contributes nothing to a consumer's + // graph; its tools come from the tool sub-build. An SDK's program behind a feature that + // depends on the SDK builds (it was a cycle of the consumer's graph) and runs, read under + // both spellings; a tool's own library is not linked into the application; a cycle among + // libraries is refused alike under both cache modes; a tool whose own build requests it + // again is refused once, naming the repetition. The tool store is the case's own. + s::needs({"gcc"}); + const s::options home{.where = s::home::linked}; + auto fixture = [](const fs::path& w, std::string_view tool_dep) { + s::project{w / "fw", {{"mcpp.toml", R"cpp([package] +name = "fw" +namespace = "spike" +version = "0.1.0" + +[targets.fw] +kind = "lib" + +[build] +sources = ["src/*.cpp"] + +[features] +installer = [] + +[feature-deps.installer] +spike.fw-installer = { path = "tool", tools = ["fw-installer"], reexport = true } +)cpp"}, + {"src/fw.cpp", "int fw_answer() { return 42; }\n"}, + {"tool/mcpp.toml", std::format("[package]\nname = \"fw-installer\"\nnamespace = \"spike\"\nversion = \"0.1.0\"\n\n[targets.fw-installer]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n\n[dependencies]\n{}\n", tool_dep)}}}; + s::project{w / "z", {{"mcpp.toml", "[package]\nname = \"z\"\nnamespace = \"spike\"\nversion = \"0.1.0\"\n\n[targets.z]\nkind = \"lib\"\n\n[build]\nsources = [\"src/*.cpp\"]\n"}, + {"src/z.cpp", "int z_only_in_the_tool() { return 7; }\n"}}}; + return s::project{w / "app", {{"mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[targets.app]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n\n[dependencies]\nspike.fw = { path = \"../fw\" }\n\n[features]\nwindows-installer = [\"spike.fw/installer\"]\n"}, + {"src/main.cpp", "int fw_answer();\nint main() { return fw_answer() == 42 ? 0 : 1; }\n"}, + {"build.mcpp", R"cpp(import std; +import mcpp; +int main() { + const char* a = mcpp::dep_bin("fw-installer", "fw-installer"); + const char* b = mcpp::dep_bin("spike.fw-installer", "fw-installer"); + mcpp::warning((std::string("DEPBIN short=[") + (a ? a : "") + "] qualified=[" + + (b ? b : "") + "]").c_str()); + return 0; +} +)cpp"}}}; + }; + const auto root = temp_dir(); + // 1. The SDK shape: the tool depends on the package declaring it. + auto app1 = fixture(root / "s1", "spike.fw = { path = \"..\" }"); + app1.write("../fw/tool/src/main.cpp", "#include \nint fw_answer();\nint main() { std::printf(\"fw-installer ran %d\\n\", fw_answer()); return 0; }\n"); + auto b1 = app1.mcpp({"build", "--features", "windows-installer"}, home); + expect(fatal(e2e::succeeded(b1))) << "a feature tool depending on its declaring package did not build"; + std::smatch m; + const auto t1 = s::text(b1); + expect(fatal(std::regex_search(t1, m, std::regex(R"(DEPBIN short=\[([^\]]*))")) && m[1].length() > 0)) + << "dep_bin(\"fw-installer\") read nothing"; + const fs::path tool = m[1].str(); + expect(s::has_line(t1, R"(qualified=\[[^\]])")) << "dep_bin(\"spike.fw-installer\") read nothing"; + auto ran = s::run(tool); + expect(e2e::succeeded(ran)) << "the tool did not run"; + expect(eq(s::last_line(ran.out), std::string("fw-installer ran 42"))); + expect(e2e::succeeded(app1.mcpp({"run"}, home))) << "the application does not run"; + + // 2. A tool's own library does not reach the application. + auto app2 = fixture(root / "s2", "spike.z = { path = \"../../z\" }"); + app2.write("../fw/tool/src/main.cpp", "#include \nint z_only_in_the_tool();\nint main() { std::printf(\"tool %d\\n\", z_only_in_the_tool()); return 0; }\n"); + expect(fatal(e2e::succeeded(app2.mcpp({"build", "--features", "windows-installer"}, home)))) << "the tool-with-a-library build failed"; + std::optional graph; + for (const auto& f : s::find_files(app2.at("target"), R"(build\.ninja)")) + if (!graph && !f.generic_string().contains("/.build-mcpp/")) graph = f; + expect(fatal(graph.has_value())); + const auto g = s::read_file(*graph); + expect(s::no_line(g, "^build bin/app.*spike_z")) << "the tool's own library was linked into the application"; + expect(s::lacks(g, "spike_fw-installer")) << "the tool package's sources were compiled in the consumer's build"; + + // 3. A cycle among libraries, under both cache modes. + for (std::string p : {"a", "b"}) { + const std::string q = p == "a" ? "b" : "a"; + s::project{root / "s3" / p, {}}.write("mcpp.toml", std::format("[package]\nname = \"{0}\"\nnamespace = \"spike\"\nversion = \"0.1.0\"\n\n[targets.{0}]\nkind = \"lib\"\n\n[build]\nsources = [\"src/*.cpp\"]\n\n[dependencies]\nspike.{1} = {{ path = \"../{1}\" }}\n", p, q)); + s::project{root / "s3" / p, {}}.write("src/" + p + ".cpp", std::format("int {}_answer() {{ return 1; }}\n", p)); + } + s::project app3{root / "s3/app", {{"mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[targets.app]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n\n[dependencies]\nspike.a = { path = \"../a\" }\n"}, + {"src/main.cpp", "int main() { return 0; }\n"}}}; + for (std::vector args : {std::vector{"build"}, std::vector{"build", "--cache=local"}}) { + auto b3 = app3.mcpp(args, home); + expect(b3.started && b3.status != 0) << "a cycle of packages was accepted (" << args.size() << " args)"; + expect(e2e::contains(s::text(b3), "dependency cycle: 'spike.a' -> 'spike.b' -> 'spike.a'")) << "the cycle refusal does not name its edges"; + } + + // 4. A tool whose own build requests it again: refused once. + auto app4 = fixture(root / "s4", "spike.fw = { path = \"..\", features = [\"installer\"] }"); + app4.write("../fw/tool/src/main.cpp", "int main() { return 0; }\n"); + auto b4 = app4.mcpp({"build", "--features", "windows-installer"}, home); + expect(b4.started && b4.status != 0) << "a tool requesting itself through its own build was accepted"; + expect(e2e::contains(s::text(b4), "is requested by its own build")) << "the repetition is not named"; + std::size_t nested = 0; + for (auto at = s::text(b4).find("building host tool"); at != std::string::npos; at = s::text(b4).find("building host tool", at + 1)) ++nested; + expect(le(nested, std::size_t{1})) << "the repetition was refused only after nested sub-builds"; + }; + + "775 a host tool merges its conditional sections once"_test = [] { + // #690 F12: a dependency compiled as a host tool receives each entry of its matching + // [target..build] sections once (the sub-build merged the merged manifest again). + // The entry is `-include once.h`, whose variable has no include guard; the package builds + // on its own (the control). + const s::options home{.where = s::home::inherited}; + const auto root = temp_dir(); + s::project toolpkg{root / "toolpkg", {{"inc/once.h", "int once_counter = 1;\n"}, + {"mcpp.toml", "[package]\nname = \"toolpkg\"\nversion = \"0.1.0\"\n\n[targets.gen]\nkind = \"bin\"\nmain = \"src/gen.cpp\"\n\n[build]\ninclude_dirs = [\"inc\"]\n\n[target.'cfg(not(os = \"none\"))'.build]\ncxxflags = [\"-include once.h\"]\n"}, + {"src/gen.cpp", R"cpp(#include +int main(int argc, char** argv) { + if (argc < 2) return 2; + std::FILE* f = std::fopen(argv[1], "w"); + if (!f) return 3; + std::fprintf(f, "int generated_answer() { return %d; }\n", 40 + once_counter); + std::fclose(f); + return 0; +} +)cpp"}}}; + s::project app{root / "app", {{"mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies]\ntoolpkg = { path = \"../toolpkg\", tools = [\"gen\"] }\n"}, + {"src/main.cpp", "#include \nint generated_answer();\nint main() { std::printf(\"ANSWER=%d\\n\", generated_answer()); }\n"}, + {"build.mcpp", R"cpp(import std; +import mcpp; +int main() { + const char* tool = mcpp::dep_bin("toolpkg", "gen"); + if (!tool || !*tool) return 1; + std::string out = std::string(mcpp::out_dir()) + "/gen.cpp"; + std::string cmd = std::string("\"") + tool + "\" \"" + out + "\""; +#ifdef _WIN32 + cmd = "\"" + cmd + "\""; +#endif + if (std::system(cmd.c_str()) != 0) return 1; + mcpp::generated(out.c_str()); +} +)cpp"}}}; + expect(fatal(e2e::succeeded(toolpkg.mcpp({"build"}, home)))) << "control: the tool package does not build on its own"; + auto b = app.mcpp({"build"}, home); + expect(s::lacks(s::text(b), "redefinition")) << "the host tool received a conditional entry twice"; + expect(fatal(e2e::succeeded(b))) << "the consumer did not build"; + auto r = app.mcpp({"run"}, home); + expect(e2e::succeeded(r)) << "the program did not run"; + expect(s::has_line(s::text(r), "^ANSWER=41")) << "wrong answer"; + }; + + "800 a feature provides its host tools"_test[tag{"requires:gcc"}] = [] { + // mcpp#709: `[features.] tools = [""]`: a consumer that enables the feature + // receives the tool as if its edge had written `tools = [...]`; one that does not gets + // nothing built; an entry naming no bin target is refused, naming the bin targets. + s::needs({"gcc"}); + const s::options home{.where = s::home::linked}; + const auto root = temp_dir(); + const std::string tool_manifest = "[package]\nname = \"toolpkg\"\nversion = \"0.1.0\"\n\n[build]\nsources = [\"src/lib.cpp\"]\n\n[features.codegen]\ntools = [\"codegen\"]\n\n[targets.codegen]\nkind = \"bin\"\nmain = \"src/codegen.cpp\"\n\n[targets.toolpkg]\nkind = \"lib\"\n"; + s::project toolpkg{root / "toolpkg", {{"mcpp.toml", tool_manifest}, + {"src/lib.cpp", "int toolpkg_lib() { return 1; }\n"}, + {"src/codegen.cpp", generator("generated_answer", 42)}}}; + s::project app{root / "app", {{"mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies]\ntoolpkg = { path = \"../toolpkg\", features = [\"codegen\"] }\n"}, + {"src/main.cpp", "#include \nint generated_answer();\nint main() { std::printf(\"ANSWER=%d\\n\", generated_answer()); }\n"}, + {"build.mcpp", runner({}, "toolpkg", "codegen")}}}; + auto b1 = app.mcpp({"build"}, home); + expect(fatal(e2e::succeeded(b1))) << "1: build failed"; + expect(e2e::contains(s::text(b1), "host tool toolpkg:codegen")) << "1: the feature's tool was not built"; + expect(eq(s::last_starting(s::text(app.mcpp({"run"}, home)), "ANSWER="), std::string("ANSWER=42"))) << "1: generated source not linked"; + s::project plain{root / "plain", {{"mcpp.toml", "[package]\nname = \"plain\"\nversion = \"0.1.0\"\n\n[dependencies]\ntoolpkg = { path = \"../toolpkg\" }\n"}, + {"src/main.cpp", "int main() {}\n"}}}; + auto b2 = plain.mcpp({"build"}, home); + expect(fatal(e2e::succeeded(b2))) << "2: build failed"; + expect(s::lacks(s::text(b2), "host tool")) << "2: a tool was built for a consumer that did not enable the feature"; + toolpkg.write("mcpp.toml", std::regex_replace(tool_manifest, std::regex(R"(tools = \["codegen"\])"), R"(tools = ["nosuch"])")); + app.remove("target"); + auto b3 = app.mcpp({"build"}, home); + expect(b3.started && b3.status != 0) << "3: an unknown tool name was accepted"; + expect(e2e::contains(s::text(b3), "nosuch") && e2e::contains(s::text(b3), "codegen")) << "3: the refusal does not name the entry and the bin targets"; + }; + + "802 a host build applies its host row"_test[tag{"requires:gcc"}][tag{"requires:elf"}] = [] { + // mcpp#704: a build without --target targets the host, and [target.] + // describes it: without the row a plain build is self-contained (no NEEDED + // libstdc++.so.6); with `cxx_runtime = "toolchain-coupled"` it needs it, as + // `--target ` does; the row is found under another spelling of the triple. + s::needs({"gcc", "elf"}); + const auto uname = s::on_path("uname"); + expect(fatal(uname.has_value())); + const auto arch = s::last_line(s::run(*uname, {"-m"}).out); + s::project p{{"src/main.cpp", "#include \n#include \nint main() { std::string s = \"RT_OK\"; std::printf(\"%s\\n\", s.c_str()); }\n"}}; + auto write_manifest = [&](const std::string& selector) { + std::string m = "[package]\nname = \"cxxrt\"\nversion = \"0.1.0\"\n\n[language]\nstandard = \"c++23\"\nimport_std = false\n\n[toolchain]\ndefault = \"gcc@16.1.0\"\n\n[targets.cxxrt]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n"; + if (!selector.empty()) + m += std::format("\n[target.{}]\ncxx_runtime = \"toolchain-coupled\"\n", selector); + p.write("mcpp.toml", m); + }; + auto needs_libstdcxx = [&] { + std::optional bin; + for (const auto& f : s::find_files(p.at("target"), "cxxrt")) + if (!bin && f.parent_path().filename() == "bin") bin = f; + expect(fatal(bin.has_value())) << "no program built"; + return std::ranges::any_of(s::read_header(*bin).needed, [](const std::string& n) { return n.starts_with("libstdc++.so"); }); + }; + write_manifest(""); + expect(fatal(e2e::succeeded(p.mcpp({"build"})))) << "1: build failed"; + expect(!needs_libstdcxx()) << "1: the default build is not self-contained"; + p.remove("target"); + write_manifest(arch + "-linux-gnu"); + expect(fatal(e2e::succeeded(p.mcpp({"build"})))) << "2: build failed"; + expect(needs_libstdcxx()) << "2: [target.] cxx_runtime was not applied to a host build"; + expect(eq(s::last_line(p.mcpp({"run"}).out), std::string("RT_OK"))) << "2: the program does not run"; + p.remove("target"); + write_manifest(arch + "-unknown-linux-gnu"); + expect(fatal(e2e::succeeded(p.mcpp({"build"})))) << "3: build failed"; + expect(needs_libstdcxx()) << "3: the row was not matched under another spelling"; + }; + + "804 a path host tool builds with its chosen toolchain"_test[tag{"requires:gcc"}][tag{"requires:elf"}] = [] { + // mcpp#710: a host tool is built by one compiler recorded in the store's key: + // MCPP_TOOLCHAIN, else the tool package's own [toolchain], else the consumer's + // build-program compiler. Through a plain path dependency: 1. no toolchain of its own, + // the consumer's llvm (not the global default gcc); 2. its own gcc 16.1.0 while the + // consumer keeps llvm. The compiler is read from the tool's .comment strings (the script + // used `readelf -p .comment`; the case reads the bytes). + s::needs({"gcc", "elf"}); + const bool llvm_only = s::env("MCPP_E2E_804_LLVM_HOST_ONLY").value_or("0") == "1"; + if (llvm_only) + expect(fatal(s::on_linux() && !s::has("x86_64-host"))) << "LLVM-only host-helper admission requires native Linux ARM64"; + const auto llvm = s::llvm_version(); + const s::options home{.where = s::home::linked, .env = {{"MCPP_TOOLCHAIN", std::nullopt}}}; + if (auto index = s::env("MCPP_NATIVE_XIM_INDEX"); index && !index->empty()) { + const auto python = s::on_path("python3"); + expect(fatal(python.has_value())); + expect(fatal(e2e::succeeded(s::run(*python, {(s::repository() / ".github/tools/seed_native_xim_index.py").string(), + s::case_home(s::home::linked).string(), *index})))); + } + const auto root = temp_dir(); + s::project toolpkg{root / "toolpkg", {{"src/stamp.cpp", "int main() { return 0; }\n"}}}; + auto write_tool = [&](std::string_view body) { + std::string m = "[package]\nname = \"toolpkg\"\nversion = \"0.1.0\"\n\n[targets.stamp]\nkind = \"bin\"\nmain = \"src/stamp.cpp\"\n"; + if (!body.empty()) m += std::format("\n[toolchain]\n{}\n", body); + toolpkg.write("mcpp.toml", m); + }; + s::project app{root / "app", {{"mcpp.toml", std::format("[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[toolchain]\ndefault = \"llvm@{}\"\n\n[dependencies]\ntoolpkg = {{ path = \"../toolpkg\", tools = [\"stamp\"] }}\n", llvm)}, + {"src/main.cpp", "int main() {}\n"}, + {"build.mcpp", "#include \nimport mcpp;\nint main() {\n const char* tool = mcpp::dep_bin(\"toolpkg\", \"stamp\");\n if (!tool || !*tool) return 1;\n mcpp::warning((std::string(\"TOOL=\") + tool).c_str());\n}\n"}}}; + auto tool_path = [](const e2e::outcome& r) { + std::string t; + for (const auto& l : s::lines_with(s::text(r), "TOOL=")) t = l.substr(l.find("TOOL=") + 5); + return fs::path(t); + }; + // 1. + write_tool(""); + auto b1 = app.mcpp({"build"}, home); + expect(fatal(e2e::succeeded(b1))) << "1: build failed"; + const auto t1 = tool_path(b1); + expect(fatal(!t1.empty() && fs::is_regular_file(t1))) << "1: no tool binary at '" << t1.string() << "'"; + expect(e2e::contains(s::read_file(t1), "clang version " + llvm)) << "1: the tool was not built by the consumer's llvm " << llvm; + if (llvm_only) + return; // ARM64 publishes LLVM GNU only: the LLVM case is the one this host proves + // 2. + write_tool("default = \"gcc@16.1.0\""); + app.remove("target"); + auto b2 = app.mcpp({"build"}, home); + expect(fatal(e2e::succeeded(b2))) << "2: build failed"; + const auto t2 = tool_path(b2); + expect(fatal(!t2.empty() && fs::is_regular_file(t2))) << "2: no tool binary at '" << t2.string() << "'"; + const auto bytes = s::read_file(t2); + expect(std::regex_search(bytes, std::regex(R"(GCC: \([^\n]*\) 16\.1\.0)"))) << "2: the tool's own [toolchain] was not used"; + expect(s::lacks(bytes, "clang version")) << "2: the tool was built by the consumer's clang"; + }; + + "826 plugin diagnostics features and names"_test[tag{"requires:unix-shell"}] = [] { + // #734 E11, E7, E10: a plugin `acme.gen` offers `mcpp.acme.gen` and two modules behind + // features. D1 a structured diagnostic in the engine's form (impact, help); D2 a cache hit + // reports it again; N1 a module behind a disabled feature fails naming the package and the + // feature; R1 `mcpp.acme.*` from `acme` draws no warning; R2 `mcpp.rules.*` from `acme` + // does, naming `mcpp.acme.*`. + s::needs({"unix-shell"}); + const auto root = temp_dir(); + s::project gen{root / "gen", {{"mcpp.toml", "[package]\nnamespace = \"acme\"\nname = \"gen\"\nversion = \"0.1.0\"\n\n[lib]\npath = \"src/gen.cppm\"\n\n[features.extra]\nsources = [\"src/extra.cppm\"]\n\n[features.bad]\nsources = [\"src/bad.cppm\"]\n"}, + {"src/gen.cppm", R"cpp(export module mcpp.acme.gen; +import mcpp.core; +export namespace acme { +inline void tell() { + mcpp::report({.severity = "warning", + .message = "the generator found no schema", + .impact = "no bindings are generated", + .hint = "add schema/*.proto"}); +} +} +)cpp"}, + {"src/extra.cppm", "export module mcpp.acme.gen.extra;\nexport namespace acme { inline int extra() { return 1; } }\n"}, + {"src/bad.cppm", "export module mcpp.rules.bad;\nexport namespace acme { inline int bad() { return 2; } }\n"}}}; + s::project app{root / "app", {{"src/main.cpp", "int main() { return 0; }\n"}}}; + auto edge = [&](std::string_view extra) { + app.write("mcpp.toml", std::format("[package]\nname = \"app826\"\nversion = \"0.1.0\"\n\n[build-dependencies]\n\"acme.gen\" = {{ path = \"../gen\", host-module = true{} }}\n", extra)); + }; + // D1, R1. + edge(""); + app.write("build.mcpp", "import mcpp.core;\nimport mcpp.acme.gen;\nint main() { acme::tell(); return 0; }\n"); + auto d1 = app.mcpp({"build"}); + expect(fatal(e2e::succeeded(d1))) << "D1: the build failed"; + expect(e2e::contains(s::text(d1), "the generator found no schema")) << "D1: the message is missing"; + expect(e2e::contains(s::text(d1), "impact: no bindings are generated")) << "D1: the impact line is missing"; + expect(e2e::contains(s::text(d1), "help: add schema/*.proto")) << "D1: the help line is missing"; + expect(s::no_line(s::text(d1), R"(mcpp\.acme\.gen'.*mcpp\.|claims an origin)")) << "R1: an own-namespace module was warned about"; + // D2: past the fast path, a cache hit. + fs::last_write_time(app.at("src/main.cpp"), fs::file_time_type::clock::now()); + auto d2 = app.mcpp({"build", "-v"}); + expect(fatal(e2e::succeeded(d2))) << "D2: the second build failed"; + expect(s::has_line(s::text(d2), R"(^ *build\.mcpp .* cached)")) << "D2: the second build ran the program; the replay is not measured"; + expect(e2e::contains(s::text(d2), "impact: no bindings are generated")) << "D2: the cached run did not report the diagnostic"; + // N1. + app.write("build.mcpp", "import mcpp.core;\nimport mcpp.acme.gen.extra;\nint main() { return acme::extra() == 1 ? 0 : 1; }\n"); + auto n1 = app.mcpp({"build"}); + expect(n1.started && n1.status != 0) << "N1: a module behind a disabled feature was importable"; + expect(e2e::contains(s::text(n1), "provided by: acme.gen, feature \"extra\" (not enabled)")) << "N1: the error does not name the feature"; + edge(", features = [\"extra\"]"); + expect(e2e::succeeded(app.mcpp({"build"}))) << "N1: enabling the named feature did not fix the build"; + // R2. + edge(", features = [\"bad\"]"); + app.write("build.mcpp", "import mcpp.core;\nimport mcpp.rules.bad;\nint main() { return acme::bad() == 2 ? 0 : 1; }\n"); + auto r2 = app.mcpp({"build"}); + expect(fatal(e2e::succeeded(r2))) << "R2: the build failed"; + expect(e2e::contains(s::text(r2), "uses 'mcpp.rules'")) << "R2: a reserved segment was not warned about"; + expect(e2e::contains(s::text(r2), "'mcpp.acme.*'")) << "R2: the warning does not name the accepted form"; + }; + + "831 a path dependency host module outside src"_test[tag{"requires:unix-shell"}] = [] { + // #734: a path dependency's host module is compiled into the consumer's build program, + // which no edge of build.ninja names, so the fast path sees an edit to it itself (it swept + // only src/). S1 an edit to rules/hm.cppm re-runs the program; S2 a newer file of a + // package nested in the dependency does not decline the fast path. + s::needs({"unix-shell"}); + const auto root = temp_dir(); + s::project plug{root / "plug", {{"mcpp.toml", "[package]\nnamespace = \"probe\"\nname = \"plug831\"\nversion = \"0.1.0\"\n\n[build]\nsources = [\"src/plug831.cppm\"]\n\n[features.hm]\nsources = [\"rules/hm.cppm\"]\n"}, + {"src/plug831.cppm", "export module probe.plug831;\nexport int plug_root() { return 0; }\n"}, + {"rules/hm.cppm", "export module probe.hm831;\nexport inline int value() { return 1; }\n"}, + {"tests/fixture/mcpp.toml", "[package]\nname = \"fixture831\"\nversion = \"0.1.0\"\n"}, + {"tests/fixture/src/main.cpp", "int main() { return 0; }\n"}}}; + s::project app{root / "app", {{"mcpp.toml", "[package]\nname = \"app831\"\nversion = \"0.1.0\"\n\n[build-dependencies.probe]\nplug831 = { path = \"../plug\", features = [\"hm\"], host-module = true }\n"}, + {"build.mcpp", "import std;\nimport mcpp;\nimport probe.hm831;\nint main() {\n if (value() != 1) { std::println(\"host module value {}\", value()); return 1; }\n return 0;\n}\n"}, + {"src/main.cpp", "int main() { return 0; }\n"}}}; + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "the first build failed"; + expect(fatal(e2e::succeeded(app.mcpp({"build", "-v"})))) << "the second build failed"; + // S1. + std::this_thread::sleep_for(std::chrono::milliseconds(1100)); + plug.write("rules/hm.cppm", "export module probe.hm831;\nexport inline int value() { return 2; }\n"); + auto s1 = app.mcpp({"build"}); + expect(s1.started && s1.status != 0) << "S1: the build after an edit to rules/hm.cppm succeeded without re-running the build program"; + expect(e2e::contains(s::text(s1), "host module value 2")) << "S1: the build program did not read the edited host module"; + // S2. + plug.write("rules/hm.cppm", "export module probe.hm831;\nexport inline int value() { return 1; }\n"); + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "S2: the build after restoring the host module failed"; + std::this_thread::sleep_for(std::chrono::milliseconds(1100)); + fs::last_write_time(plug.at("tests/fixture/src/main.cpp"), fs::file_time_type::clock::now()); + auto s2 = app.mcpp({"build", "-v"}); + expect(fatal(e2e::succeeded(s2))) << "S2: the build failed"; + expect(s::lacks(s::text(s2), "declined")) << "S2: the fast path declined after a file of a package nested in the dependency changed"; + expect(s::lacks(s::text(s2), "Resolving toolchain")) << "S2: the build was planned instead of replayed"; + }; + + "859 a host module entry is reused while its inputs agree"_test = [] { + // #748 (B1), mcpp#797 (R6): a build program's library is compiled again exactly when what + // it is compiled from changed (the identity holds the digest of the package's tree, so an + // included header counts), and the program never runs on a stale compile. A: a stale + // program compiles nothing; B: an interface edit compiles both; C: a header edit does, + // and the program reads the new value; D: a record that disagrees is a miss, replaced. + const s::options home{.where = s::home::inherited, .env = {{"MCPP_VERBOSE", "1"}}}; + const auto root = temp_dir(); + s::project rules{root / "rules", {{"mcpp.toml", "[package]\nname = \"rule859\"\nversion = \"0.1.0\"\n\n[targets.rule859]\nkind = \"lib\"\n"}, + {"src/value.h", "#define T859_VALUE 1\n"}, + {"src/rule859.cppm", "module;\n#include \"value.h\"\nexport module t859.rules;\nimport mcpp;\nexport int rule_value() { return T859_VALUE; }\n"}}}; + s::project app{root / "app", {{"mcpp.toml", "[package]\nname = \"app859\"\nversion = \"0.1.0\"\n\n[build-dependencies]\nrule859 = { path = \"../rules\", host-module = true }\n\n[targets.app859]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n"}, + {"src/main.cpp", "int main() { return 0; }\n"}, + {"build.mcpp", R"cpp(import mcpp; +import t859.rules; +int main() { + mcpp::rerun_if_changed("marker.txt"); + mcpp::warning(rule_value() == 1 ? "T859 value one" + : rule_value() == 2 ? "T859 value two" + : "T859 value other"); + return 0; +} +)cpp"}, + {"marker.txt", "0\n"}}}; + const auto host = app.at("target/.build-mcpp/host"); + auto stamp = [&] { + auto o = s::find_file(host, "rule859.m.o"); + expect(fatal(o.has_value())) << "no library object"; + return std::pair{fs::last_write_time(*o), fs::file_size(*o)}; + }; + auto compiled = [](const e2e::outcome& r) { + std::size_t n = 0; + const std::regex re(R"(build-program: app859: identity [0-9a-f]+ -> [0-9a-f]+, compiled)"); + for (const auto& l : s::lines_with(s::text(r), "build-program: app859")) + n += std::regex_search(l, re) ? 1 : 0; + return n; + }; + // A stale program: its declared input changed, the sources of its imports did not. + auto rerun = [&](std::string_view marker) { + app.write("marker.txt", std::string(marker) + "\n"); + auto r = app.mcpp({"build"}, home); + expect(fatal(e2e::succeeded(r))) << "the build failed"; + return r; + }; + auto first = app.mcpp({"build"}, home); + expect(fatal(e2e::succeeded(first))) << "the first build failed"; + expect(fatal(s::find_file(host, "rule859.m.o").has_value())) << "the first build compiled no library object"; + expect(e2e::contains(s::text(first), "T859 value one")) << "the program did not read the library's value"; + const auto s0 = stamp(); + auto second = [] { std::this_thread::sleep_for(std::chrono::seconds(1)); }; + // A. + second(); + auto a = rerun("1"); + expect(eq(compiled(a), std::size_t{0})) << "A: a stale program was compiled again"; + expect(stamp() == s0) << "A: the library was compiled again"; + expect(e2e::contains(s::text(a), "T859 value one")) << "A: the program gave another value"; + // B. + second(); + rules.write("src/rule859.cppm", "module;\n#include \"value.h\"\nexport module t859.rules;\nimport mcpp;\nexport int rule_value() { return T859_VALUE + 0; }\n"); + auto b = rerun("2"); + expect(eq(compiled(b), std::size_t{1})) << "B: an edit to the interface did not compile the program again"; + expect(stamp() != s0) << "B: an edit to the interface did not compile the library"; + const auto s1 = stamp(); + // C. + second(); + rules.write("src/value.h", "#define T859_VALUE 2\n"); + auto c = rerun("3"); + expect(eq(compiled(c), std::size_t{1})) << "C: an edit to an included header did not compile the program again"; + expect(stamp() != s1) << "C: an edit to an included header did not compile the library"; + expect(e2e::contains(s::text(c), "T859 value two")) << "C: the program took the compile made before the header changed"; + // D. + auto recs = s::glob(host, "*/programs.json"); + expect(fatal(!recs.empty())) << "D: no build programs' record"; + const auto rec = recs.front(); + s::project{rec.parent_path(), {}}.write("programs.json", std::regex_replace(s::read_file(rec), std::regex(R"("identity": "[0-9a-f]*")"), R"("identity": "0000000000000000")")); + auto d = rerun("4"); + expect(eq(compiled(d), std::size_t{1})) << "D: a record that disagrees was used without compiling"; + expect(s::lacks(s::read_file(rec), R"("identity": "0000000000000000")")) << "D: the record that disagreed was not replaced"; + expect(e2e::contains(s::text(d), "T859 value two")) << "D: the replaced compile gave another value"; + }; + + "997 a members dev dependencies stay out of a host tool"_test[tag{"requires:gcc"}] = [] { + // mcpp#874: a host tool has one store key whatever the command and the member; its + // closure is the tool package's own manifests (it was read from the graph the build had + // walked, holding a member's dev-dependencies). A: test --workspace then build: one entry, + // naming `lib` and not `testing`; B: a second test --workspace builds no host tool. + s::needs({"gcc"}); + const s::options home{.where = s::home::linked}; + s::project ws{temp_dir() / "ws", {{"mcpp.toml", "[workspace]\nmembers = [\"testing\", \"lib\", \"toolpkg\", \"app\"]\n"}, + {"testing/mcpp.toml", "[package]\nname = \"testing\"\nversion = \"0.1.0\"\n\n[targets.testing]\nkind = \"lib\"\n"}, + {"testing/src/testing.cpp", "int testing_ok() { return 0; }\n"}, + {"lib/mcpp.toml", "[package]\nname = \"lib\"\nversion = \"0.1.0\"\n\n[targets.lib]\nkind = \"lib\"\n\n[dev-dependencies]\ntesting = { path = \"../testing\" }\n"}, + {"lib/src/lib.cpp", "int lib_value() { return 41; }\n"}, + {"lib/tests/smoke.cpp", "int testing_ok();\nint lib_value();\nint main() { return testing_ok() + (lib_value() == 41 ? 0 : 1); }\n"}, + {"toolpkg/mcpp.toml", "[package]\nname = \"toolpkg\"\nversion = \"0.1.0\"\n\n[dependencies]\nlib = { path = \"../lib\" }\n\n[targets.gen]\nkind = \"bin\"\nmain = \"src/gen.cpp\"\n"}, + {"toolpkg/src/gen.cpp", R"cpp(#include +int lib_value(); +int main(int argc, char** argv) { + if (argc < 2) return 2; + FILE* f = std::fopen(argv[1], "w"); + if (!f) return 3; + std::fprintf(f, "int generated_answer() { return %d; }\n", lib_value() + 1); + return std::fclose(f); +} +)cpp"}, + {"app/mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[build-dependencies]\ntoolpkg = { path = \"../toolpkg\", tools = [\"gen\"] }\n"}, + {"app/src/main.cpp", "#include \nint generated_answer();\nint main() { std::printf(\"ANSWER=%d\\n\", generated_answer()); }\n"}, + {"app/tests/answer.cpp", "int generated_answer();\nint main() { return generated_answer() == 42 ? 0 : 1; }\n"}, + {"app/build.mcpp", R"cpp(#include +#include +import mcpp; +int main() { + const std::string out = std::string(mcpp::out_dir()) + "/gen.cpp"; + const std::string cmd = std::string("\"") + mcpp::dep_bin("toolpkg", "gen") + "\" \"" + out + "\""; + if (std::system(cmd.c_str()) != 0) return 1; + mcpp::generated(out.c_str()); +} +)cpp"}}}; + // A. + expect(fatal(e2e::succeeded(ws.mcpp({"test", "--workspace"}, home)))) << "the first test run failed"; + expect(fatal(e2e::succeeded(ws.mcpp({"build"}, home)))) << "the build failed"; + std::vector entries; + for (const auto& e : s::find_files(s::case_home(s::home::linked) / "build-cache/v1/tool", R"(entry\.json)")) + if (e.generic_string().contains("toolpkg@")) entries.push_back(e); + expect(fatal(eq(entries.size(), std::size_t{1}))) << "the tool's store entries"; + const auto entry = s::read_file(entries.front()); + expect(e2e::contains(entry, "\"lib@")) << "the key does not name the tool's dependency lib"; + expect(s::lacks(entry, "\"testing@")) << "the key names lib's dev-dependency testing"; + // B. + auto t2 = ws.mcpp({"test", "--workspace"}, home); + expect(fatal(e2e::succeeded(t2))) << "the second test run failed"; + expect(s::no_line(s::text(t2), "Building.*host tool")) << "the second test run built a host tool again"; + }; +}}; + +} // namespace diff --git a/tests/e2e/build-program/protocol.cpp b/tests/e2e/build-program/protocol.cpp new file mode 100644 index 000000000..d9c1d830b --- /dev/null +++ b/tests/e2e/build-program/protocol.cpp @@ -0,0 +1,898 @@ +// A project's build program (`build.mcpp`): the `mcpp:` directives it prints, the typed +// `import mcpp;` library that prints them, the environment it is given and the cache that decides +// when it runs again (formerly tests/e2e/89, 92, 110). +import std; +import mcpplibs.mtest; +import mcpp.test.support; + +using namespace mcpplibs::mtest; +namespace s = mcpp::test::support; +namespace fs = std::filesystem; + +namespace { + +constexpr std::string_view ran_again = R"(^ *build\.mcpp .* ran [0-9])"; + +E2E protocol{"protocol", [] { + "89 build mcpp"_test[tag{"requires:gcc"}] = [] { + // mcpp compiles build.mcpp with the host toolchain and runs it before the build; its + // directives add a define and a generated source; a declared-input cache runs it again + // only when its source, inputs or environment change. + s::needs({"gcc"}); + s::project p{{"mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n"}, + {"src/main.cpp", R"cpp(#ifndef FROM_BUILD_MCPP +#error "build.mcpp cxxflag did not reach the translation unit" +#endif +int generated_value(); +int main() { return generated_value() == 42 ? 0 : 1; } +)cpp"}, + {"build.mcpp", R"cpp(#include +#include +int main() { + std::ofstream("src/generated.cpp") << "int generated_value() { return 42; }\n"; + std::puts("mcpp:cxxflag=-DFROM_BUILD_MCPP=1"); + std::puts("mcpp:generated=src/generated.cpp"); + std::puts("mcpp:rerun-if-env-changed=MCPP_TEST_TOGGLE"); + return 0; +} +)cpp"}}; + auto b1 = p.mcpp({"build"}); + expect(fatal(e2e::succeeded(b1))) << "build 1 errored"; + expect(e2e::contains(s::text(b1), "build.mcpp")) << "build.mcpp not invoked"; + expect(p.exists("src/generated.cpp")) << "generated source not written"; + expect(p.exists("target/.build-mcpp/build.mcpp.cache")) << "cache not written under target/"; + // The program returns 0 only if both the define and the generated source took effect. + expect(e2e::succeeded(p.mcpp({"run"}))) << "run returned non-zero (define/generated source missing)"; + + // A touched source makes prepare run; the program's inputs are unchanged, so its cache + // answers (a reused program is stated under --verbose only). + fs::last_write_time(p.at("src/main.cpp"), fs::file_time_type::clock::now()); + auto b2 = p.mcpp({"build", "-v"}); + expect(fatal(e2e::succeeded(b2))) << "build 2 errored"; + expect(s::has_line_nocase(s::text(b2), R"(build\.mcpp.*cached)")) << "build.mcpp did not short-circuit"; + + // A declared environment input changed: the program runs again. + fs::last_write_time(p.at("src/main.cpp"), fs::file_time_type::clock::now()); + auto b3 = p.mcpp({"build"}, {.env = {{"MCPP_TEST_TOGGLE", "1"}}}); + expect(fatal(e2e::succeeded(b3))) << "build 3 errored"; + expect(s::has_line(s::text(b3), ran_again)) << "changed env did not force build.mcpp re-run"; + + // The program runs in the project root even when mcpp is started in a subdirectory. + s::project sub{p.dir().parent_path() / "sub", + {{"mcpp.toml", "[package]\nname = \"sub\"\nversion = \"0.1.0\"\n"}, + {"src/main.cpp", "#ifndef FROM_BUILD_MCPP\n#error \"define missing\"\n#endif\n" + "int gen();\nint main() { return gen() == 7 ? 0 : 1; }\n"}, + {"build.mcpp", R"cpp(#include +#include +int main() { + std::ofstream("src/gen.cpp") << "int gen(){return 7;}\n"; + std::puts("mcpp:generated=src/gen.cpp"); + std::puts("mcpp:cxxflag=-DFROM_BUILD_MCPP=1"); + return 0; +} +)cpp"}}}; + expect(e2e::succeeded(sub.mcpp_in("src", {"build"}))) << "build from subdir errored (cwd not project root?)"; + expect(sub.exists("src/gen.cpp")) << "generated source not written to project root"; + }; + + "92 build mcpp import"_test[tag{"requires:gcc"}] = [] { + // A modules-first build program (no #include, no `import std;`) calls the typed API of + // the bundled `import mcpp;`, which prints the same directives. + s::needs({"gcc"}); + s::project p{{"mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n"}, + {"build.mcpp", R"cpp(import mcpp; +int main() { + mcpp::generated("src/gen.cpp"); + mcpp::cxxflag("-DVIA_MODULE=1"); + mcpp::define("MODULE_DEFINE"); + mcpp::rerun_if_changed("build.mcpp"); + return 0; +} +)cpp"}, + {"src/gen.cpp", "int gen_value() { return 99; }\n"}, + {"src/main.cpp", R"cpp(#if !defined(VIA_MODULE) || !defined(MODULE_DEFINE) +#error "import mcpp typed directives did not reach the build" +#endif +int gen_value(); +int main() { return gen_value() == 99 ? 0 : 1; } +)cpp"}}; + auto b = p.mcpp({"build"}); + expect(fatal(e2e::succeeded(b))) << "import mcpp build errored"; + expect(e2e::contains(s::text(b), "build.mcpp")) << "build.mcpp not invoked"; + // The bundled module is compiled once and kept by key: in the global cache, or, when the + // cache mode asks for no global entry, in the build programs' graph under target/ + // (#748, #797). + auto dir = p.mcpp({"cache", "dir"}); + const fs::path cache_root = dir.out.substr(0, dir.out.find_first_of("\r\n")); + bool compiled = false; + for (const auto& root : {cache_root / "pkg/_engine", p.at("target/.build-mcpp/host")}) + compiled = compiled || !s::find_files(root, R"(mcpp\.m\.o(bj)?)").empty(); + expect(compiled) << "bundled mcpp module not compiled"; + expect(e2e::succeeded(p.mcpp({"run"}))) << "run non-zero (module directives/generated source missing)"; + }; + + "110 build mcpp env contract"_test[tag{"requires:gcc"}] = [] { + // The program is given MCPP_TARGET, HOST, PROFILE, OUT_DIR, MANIFEST_DIR and FEATURES; + // they are part of its cache key (a feature change runs it again, an identical build does + // not). + s::needs({"gcc"}); + auto p = s::project::scaffold("envbp"); + p.write("build.mcpp", R"cpp(#include +#include +#include +#include +static const char* env_or(const char* n) { const char* v = std::getenv(n); return v ? v : ""; } +int main() { + std::ofstream f("src/contract_gen.cpp"); + f << "extern \"C\" const char* bp_target() { return \"" << env_or("MCPP_TARGET") << "\"; }\n"; + f << "extern \"C\" const char* bp_host() { return \"" << env_or("MCPP_HOST") << "\"; }\n"; + f << "extern \"C\" const char* bp_profile() { return \"" << env_or("MCPP_PROFILE") << "\"; }\n"; + f << "extern \"C\" const char* bp_tos() { return \"" << env_or("MCPP_TARGET_OS") << "\"; }\n"; + f << "extern \"C\" const char* bp_tarch() { return \"" << env_or("MCPP_TARGET_ARCH") << "\"; }\n"; + f << "extern \"C\" int bp_tenv_set() { return " + << (std::getenv("MCPP_TARGET_ENV") ? 1 : 0) << "; }\n"; + f << "extern \"C\" const char* bp_features() { return \"" << env_or("MCPP_FEATURES") << "\"; }\n"; + f << "extern \"C\" int bp_has_extra() { return " + << (std::getenv("MCPP_FEATURE_EXTRA") ? 1 : 0) << "; }\n"; + if (!f) return 1; + const char* out = std::getenv("MCPP_OUT_DIR"); + if (!out) { std::fprintf(stderr, "MCPP_OUT_DIR unset\n"); return 1; } + std::ofstream probe(std::string(out) + "/probe.txt"); + if (!probe) { std::fprintf(stderr, "MCPP_OUT_DIR not writable\n"); return 1; } + std::printf("mcpp:generated=src/contract_gen.cpp\n"); + return 0; +} +)cpp"); + p.write("src/main.cpp", R"cpp(import std; +extern "C" const char* bp_target(); +extern "C" const char* bp_host(); +extern "C" const char* bp_profile(); +extern "C" const char* bp_features(); +extern "C" int bp_has_extra(); +extern "C" const char* bp_tos(); +extern "C" const char* bp_tarch(); +extern "C" int bp_tenv_set(); +int main() { + std::println("target={} host={} profile={} features=[{}] extra={} tos={} tarch={} tenvset={}", + bp_target(), bp_host(), bp_profile(), bp_features(), bp_has_extra(), + bp_tos(), bp_tarch(), bp_tenv_set()); + return 0; +} +)cpp"); + p.write("mcpp.toml", "[package]\nname = \"envbp\"\nversion = \"0.1.0\"\n\n[features]\ndefault = []\nextra = []\n"); + expect(fatal(e2e::succeeded(p.mcpp({"build"})))) << "build 1 failed"; + const auto out = s::last_line(p.mcpp({"run"}).out); // the program's line, after mcpp's own + const std::string triple = R"([a-z0-9_]+-(linux|macos|windows)(-[a-z]+)?)"; + expect(e2e::matches(out, "target=" + triple + " host=" + triple + R"( profile=dev features=\[\] extra=0)")) + << "unexpected contract values"; + // OS is one of the canonical spellings, ARCH is not empty, ENV is always set (empty on + // macOS), and they agree with the triple. + std::smatch m; + expect(fatal(std::regex_search(out, m, std::regex(R"(tos=(linux|macos|windows) tarch=([a-z0-9_]+) tenvset=1)")))) + << "target split vars missing/odd: " << out; + expect(e2e::contains(out, "target=" + m[2].str() + "-" + m[1].str())) << "target split disagrees with MCPP_TARGET"; + + // An identical build does not run the program again. + auto b2 = p.mcpp({"build"}); + expect(s::no_line(s::text(b2), ran_again)) << "identical build re-ran build.mcpp"; + + // A feature changes the contract: the program runs again and sees it. + auto b3 = p.mcpp({"build", "--features", "extra"}); + expect(fatal(e2e::succeeded(b3))) << "build 3 failed"; + expect(s::has_line(s::text(b3), ran_again)) << "feature change did not re-run build.mcpp"; + auto bin = s::newest_file(p.at("target"), "envbp"); + expect(fatal(bin.has_value())); + expect(e2e::contains(s::last_line(s::run(*bin).out), "features=[extra] extra=1")) << "feature not visible to build.mcpp"; + + // So does the profile. + auto b4 = p.mcpp({"build", "--release"}); + expect(fatal(e2e::succeeded(b4))) << "build 4 failed"; + expect(s::has_line(s::text(b4), ran_again)) << "profile change did not re-run build.mcpp"; + bin = s::newest_file(p.at("target"), "envbp"); + expect(fatal(bin.has_value())); + expect(e2e::contains(s::last_line(s::run(*bin).out), "profile=release")) << "profile not visible"; + }; + + "139 build program advisory"_test[tag{"requires:unix-shell"}][resource{"machine"}] = [] { + // A program that succeeds and still says something (`mcpp:warning=`). The second build is + // the test: the program's result is cached, and an advisory printed only where the + // program ran would appear once and never again. + s::needs({"unix-shell"}); + s::project p{{"mcpp.toml", "[package]\nname = \"advisory\"\nversion = \"0.1.0\"\n"}, + {"src/main.cpp", "int main() { return 0; }\n"}, + {"build.mcpp", R"cpp(import mcpp; +import std; +int main() { + mcpp::warning("no emulator found; `xlings install qemu-riscv -y` enables `mcpp run`"); + mcpp::define("ADVISORY_RAN"); + return 0; +} +)cpp"}}; + // 1. The advisory reaches the user on a build that succeeded, named by its package. + const auto first = s::text(p.mcpp({"build"})); + expect(e2e::contains(first, "no emulator found")) << "the advisory did not reach the output"; + expect(e2e::contains(first, "advisory: no emulator found")) << "the advisory was not attributed to its package"; + expect(s::contains_nocase(first, "Finished")) << "the build did not finish"; + // 2. A touched source makes the project stale and leaves the program's inputs unchanged: + // a cache hit, which still carries the line (an unmodified build would take the + // whole-project fast path and reach neither). + fs::last_write_time(p.at("src/main.cpp"), fs::file_time_type::clock::now()); + const auto second = s::text(p.mcpp({"build", "-v"})); + expect(s::has_line(second, R"(^ *build\.mcpp .* cached)")) << "the second build re-ran the program; the replay path was not exercised"; + expect(e2e::contains(second, "advisory: no emulator found")) << "the advisory was lost on a cache hit"; + // 3. Two members, two advisories, each named. + s::project ws{temp_dir() / "ws", {{"mcpp.toml", "[workspace]\nmembers = [\"a\", \"b\"]\n"}}}; + for (std::string m : {"a", "b"}) { + ws.write(m + "/mcpp.toml", std::format("[package]\nname = \"{}\"\nversion = \"0.1.0\"\n", m)); + ws.write(m + "/src/main.cpp", "int main() { return 0; }\n"); + ws.write(m + "/build.mcpp", std::format("import mcpp;\nimport std;\nint main() {{ mcpp::warning(\"advisory from {}\"); return 0; }}\n", m)); + } + const auto all = s::text(ws.mcpp({"build"})); + expect(e2e::contains(all, "a: advisory from a")) << "member a's advisory is missing or unattributed"; + expect(e2e::contains(all, "b: advisory from b")) << "member b's advisory is missing or unattributed"; + }; + + "143 build mcpp source directive"_test[tag{"requires:gcc"}] = [] { + // `mcpp:source=` selects a source that already exists, outside every sources glob, into + // the compile set (typed: mcpp::source); the cached record keeps it there. + s::needs({"gcc"}); + const std::string manifest = "[package]\nname = \"srcsel\"\nversion = \"0.1.0\"\n\n[modules]\nsources = [\"src/**/*.cpp\"]\n\n[targets.srcsel]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n"; + s::project p{{"extra/impl.cpp", "extern \"C\" int selected_value() { return 42; }\n"}, + {"build.mcpp", "import mcpp;\nint main() {\n mcpp::source(\"extra/impl.cpp\");\n return 0;\n}\n"}, + {"src/main.cpp", "import std;\nextern \"C\" int selected_value();\nint main() {\n std::println(\"SELECTED={}\", selected_value());\n return 0;\n}\n"}, + {"mcpp.toml", manifest}}; + auto b1 = p.mcpp({"build"}); + expect(fatal(e2e::succeeded(b1))) << "build failed"; + expect(s::lacks(s::text(b1), "ignoring unknown directive")) << "mcpp:source= not recognized"; + expect(s::has_line(s::text(p.mcpp({"run"})), "^SELECTED=42$")) << "selected source not linked"; + // A changed main.cpp leaves the program's inputs alone: the cached `d source` record applies. + p.write("src/main.cpp", "import std;\nextern \"C\" int selected_value();\nint main() {\n std::println(\"SELECTED2={}\", selected_value());\n return 0;\n}\n"); + auto b2 = p.mcpp({"build"}); + expect(fatal(e2e::succeeded(b2))) << "rebuild failed"; + expect(s::no_line(s::text(b2), ran_again)) << "unchanged build.mcpp re-ran"; + expect(s::has_line(s::text(p.mcpp({"run"})), "^SELECTED2=42$")) << "cached source record lost"; + // A selected source that does not exist is an error that says so. + p.write("build.mcpp", "import mcpp;\nint main() {\n mcpp::source(\"extra/nope.cpp\");\n return 0;\n}\n"); + auto b3 = p.mcpp({"build"}); + expect(b3.started && b3.status != 0) << "missing source did not fail"; + expect(e2e::contains(s::text(b3), "selected source")) << "missing-source error not surfaced"; + }; + + "144 build mcpp include dir"_test[tag{"requires:gcc"}] = [] { + // `mcpp:include-dir=` adds a private include directory (here a header the program wrote + // under MCPP_OUT_DIR); `mcpp:include-dir-after=` reaches the compile edges as -idirafter; + // both survive the directive cache. + s::needs({"gcc"}); + s::project p{{"build.mcpp", R"cpp(#include +#include +#include +#include +#include +import mcpp; +int main() { + std::string inc = std::string(mcpp::out_dir()) + "/inc"; + std::filesystem::create_directories(inc); + std::ofstream(inc + "/bp_config.h") << "#define BP_ANSWER 42\n"; + mcpp::include_dir(inc.c_str()); + mcpp::include_dir_after("vendor/sysinc"); + return 0; +} +)cpp"}, + {"src/main.cpp", "import std;\n#include \nint main() {\n std::println(\"ANSWER={}\", BP_ANSWER);\n return 0;\n}\n"}, + {"mcpp.toml", "[package]\nname = \"incdir\"\nversion = \"0.1.0\"\n\n[modules]\nsources = [\"src/**/*.cpp\"]\n\n[targets.incdir]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n"}}; + fs::create_directories(p.at("vendor/sysinc")); + auto b1 = p.mcpp({"build"}); + expect(fatal(e2e::succeeded(b1))) << "build failed"; + expect(s::lacks(s::text(b1), "ignoring unknown directive")) << "include-dir directive not recognized"; + expect(s::has_line(s::text(p.mcpp({"run"})), "^ANSWER=42$")) << "include dir not on the -I chain"; + std::optional ninja; + for (const auto& f : s::find_files(p.at("target"), R"(build\.ninja)")) + if (!ninja && !f.generic_string().contains("/.build-mcpp/")) ninja = f; + expect(fatal(ninja.has_value())); + expect(s::has_line(s::read_file(*ninja), "-idirafter.*vendor/sysinc")) << "include-dir-after not emitted as -idirafter in build.ninja"; + p.write("src/main.cpp", "import std;\n#include \nint main() {\n std::println(\"ANSWER2={}\", BP_ANSWER);\n return 0;\n}\n"); + auto b2 = p.mcpp({"build"}); + expect(fatal(e2e::succeeded(b2))) << "rebuild failed"; + expect(s::no_line(s::text(b2), ran_again)) << "unchanged build.mcpp re-ran"; + expect(s::has_line(s::text(p.mcpp({"run"})), "^ANSWER2=42$")) << "cached include-dir record lost"; + }; + + "181 build mcpp import std"_test[tag{"requires:unix-shell"}] = [] { + // build.mcpp can `import std;`: alone, beside `import mcpp;`, and an #include-only program + // that mentions `import std;` in a comment still takes the plain path. + s::needs({"unix-shell"}); + auto p = s::project::scaffold("imp_std"); + auto step = [&](std::string_view build, std::string_view main, std::string_view said) { + p.write("build.mcpp", build); + p.write("src/main.cpp", main); + auto b = p.mcpp({"build"}); + expect(fatal(e2e::succeeded(b))) << "build: " << said; + auto r = p.mcpp({"run"}); + expect(e2e::succeeded(r)) << "run: " << said; + expect(e2e::contains(s::text(r), said)); + }; + step(R"cpp(import std; +int main() { + std::vector defines{"MCPP_FROM_IMPORT_STD", "MCPP_STD_COUNT_2"}; + std::ranges::sort(defines); + for (auto const& d : defines) std::println("mcpp:cfg={}", d); + std::println("mcpp:rerun-if-changed=build.mcpp"); + return 0; +} +)cpp", R"cpp(import std; +int main() { +#if defined(MCPP_FROM_IMPORT_STD) && defined(MCPP_STD_COUNT_2) + std::println("import-std-ok"); + return 0; +#else + std::println("defines missing"); + return 1; +#endif +} +)cpp", "import-std-ok"); + step(R"cpp(import std; +import mcpp; +int main() { + std::string tag = std::format("MCPP_BOTH_{}", 1 + 1); + mcpp::define(tag.c_str()); + mcpp::rerun_if_changed("build.mcpp"); + return 0; +} +)cpp", R"cpp(import std; +int main() { +#ifdef MCPP_BOTH_2 + std::println("both-modules-ok"); + return 0; +#else + std::println("define missing"); + return 1; +#endif +} +)cpp", "both-modules-ok"); + step(R"cpp(#include +// This program deliberately mentions import std; in a comment. +int main() { + std::puts("mcpp:cfg=MCPP_PLAIN_PATH"); + std::puts("mcpp:rerun-if-changed=build.mcpp"); + return 0; +} +)cpp", R"cpp(import std; +int main() { +#ifdef MCPP_PLAIN_PATH + std::println("plain-path-ok"); + return 0; +#else + return 1; +#endif +} +)cpp", "plain-path-ok"); + }; + + "186 build mcpp protocol and bound"_test[tag{"requires:gcc"}] = [] { + // The contract's hardening: a program that announces a protocol has an unknown directive + // refused, a printf program keeps warn-and-ignore, a newer protocol is refused with an + // upgrade hint, a hanging program is bounded, and the cache's epoch guards its replay. + s::needs({"gcc"}); + const std::string manifest = "[package]\nname = \"app\"\nversion = \"0.1.0\"\n"; + s::project p{{"mcpp.toml", manifest}, + {"src/main.cpp", "#ifndef FROM_BUILD_MCPP\n#error \"define missing\"\n#endif\nint main() { return 0; }\n"}}; + auto fresh = [&] { p.remove("target"); }; + auto failed = [](const e2e::outcome& r) { return r.started && !r.timed_out && r.status != 0; }; + + // 1. `import mcpp;` announces a protocol: an unknown directive is fatal, and says why. + p.write("build.mcpp", "#include \nimport mcpp;\nint main() {\n mcpp::cxxflag(\"-DFROM_BUILD_MCPP=1\");\n std::printf(\"mcpp:no-such-directive=1\\n\");\n}\n"); + fresh(); + auto b1 = p.mcpp({"build"}); + expect(failed(b1)) << "unknown directive from an announcing program was accepted"; + expect(e2e::contains(s::text(b1), "no-such-directive")) << "error does not name the offending directive"; + expect(s::contains_nocase(s::text(b1), "protocol")) << "error does not mention the protocol"; + + // 2. A hand-written printf program keeps warn-and-ignore. + p.write("build.mcpp", "#include \nint main() {\n std::printf(\"mcpp:cxxflag=-DFROM_BUILD_MCPP=1\\n\");\n std::printf(\"mcpp:no-such-directive=1\\n\");\n}\n"); + fresh(); + auto b2 = p.mcpp({"build"}); + expect(e2e::succeeded(b2)) << "legacy printf program was rejected"; + expect(e2e::contains(s::text(b2), "ignoring unknown directive")) << "legacy program lost its warn-and-ignore behaviour"; + + // 3. A newer protocol is refused with an upgrade hint. + p.write("build.mcpp", "#include \nint main() {\n std::printf(\"mcpp:protocol=999\\n\");\n std::printf(\"mcpp:cxxflag=-DFROM_BUILD_MCPP=1\\n\");\n}\n"); + fresh(); + auto b3 = p.mcpp({"build"}); + expect(failed(b3)) << "a newer-protocol program was accepted"; + expect(e2e::contains(s::text(b3), "999")) << "error does not name the claimed protocol"; + expect(s::contains_nocase(s::text(b3), "upgrade")) << "error gives no actionable next step"; + + // 4. A hanging program is killed; the error names the package, the variable and the manifest. + p.write("build.mcpp", "#include \n#include \nint main() {\n std::printf(\"mcpp:cxxflag=-DFROM_BUILD_MCPP=1\\n\");\n std::fflush(stdout);\n for (;;) ::sleep(60);\n}\n"); + fresh(); + auto b4 = p.mcpp({"build"}, {.env = {{"MCPP_BUILD_PROGRAM_TIMEOUT", "3"}}}); + expect(failed(b4)) << "a hanging build.mcpp did not fail the build"; + expect(lt(b4.duration, std::chrono::milliseconds(std::chrono::seconds(60)))) << "the bound did not fire"; + expect(e2e::contains(s::text(b4), "time limit")) << "no timeout diagnostic"; + expect(e2e::contains(s::text(b4), "'app'")) << "timeout error does not name the package"; + expect(e2e::contains(s::text(b4), "MCPP_BUILD_PROGRAM_TIMEOUT")) << "timeout error does not say how to change the bound"; + expect(e2e::contains(s::text(b4), "mcpp.toml")) << "timeout error does not name the manifest to edit"; + + // 4b. The manifest's bound is read; the variable overrides it; a negative one is refused. + p.write("mcpp.toml", manifest + "\n[build]\nbuild_program_timeout = 2\n"); + fresh(); + auto b4b = p.mcpp({"build"}); + expect(failed(b4b)) << "[build] build_program_timeout did not bound the run"; + expect(lt(b4b.duration, std::chrono::milliseconds(std::chrono::seconds(60)))) << "the manifest bound did not fire"; + expect(e2e::contains(s::text(b4b), "exceeded its 2s time limit")) << "the manifest value is not the effective bound"; + fresh(); + auto b4c = p.mcpp({"build"}, {.env = {{"MCPP_BUILD_PROGRAM_TIMEOUT", "1"}}}); + expect(failed(b4c)) << "build succeeded under a 1s bound"; + expect(e2e::contains(s::text(b4c), "exceeded its 1s time limit")) << "MCPP_BUILD_PROGRAM_TIMEOUT did not override the manifest"; + p.write("mcpp.toml", manifest + "\n[build]\nbuild_program_timeout = -1\n"); + auto b4d = p.mcpp({"build"}); + expect(failed(b4d)) << "a negative build_program_timeout was accepted"; + expect(e2e::contains(s::text(b4d), "build_program_timeout")) << "the error does not name the key"; + p.write("mcpp.toml", manifest); + + // 5. The cache entry carries an epoch; a foreign epoch, or a record this mcpp cannot read, + // runs the program again. + p.write("build.mcpp", "#include \nint main() { std::printf(\"mcpp:cxxflag=-DFROM_BUILD_MCPP=1\\n\"); }\n"); + fresh(); + expect(fatal(e2e::succeeded(p.mcpp({"build"})))) << "build failed"; + const std::string cache = "target/.build-mcpp/build.mcpp.cache"; + expect(fatal(p.exists(cache))) << "no build.mcpp cache written"; + const auto good = p.read(cache); + expect(fatal(s::has_line(good, "^epoch "))) << "cache carries no epoch"; + auto touch = [&] { fs::last_write_time(p.at("src/main.cpp"), fs::file_time_type::clock::now()); }; + touch(); + auto b6 = p.mcpp({"build", "-v"}); + expect(e2e::succeeded(b6)); + expect(s::has_line(s::text(b6), R"(^ *build\.mcpp .* cached)")) << "an unchanged build.mcpp was re-run"; + p.write(cache, std::regex_replace(good, std::regex("(^|\n)epoch [^\n]*"), "$1epoch 987654")); + touch(); + auto b7 = p.mcpp({"build"}); + expect(e2e::succeeded(b7)); + expect(s::has_line(s::text(b7), ran_again)) << "a foreign cache epoch did not force a re-run"; + p.write(cache, good + "d some-future-tag /x\n"); + touch(); + auto b8 = p.mcpp({"build"}); + expect(e2e::succeeded(b8)); + expect(s::has_line(s::text(b8), ran_again)) << "an unknown cache record did not force a re-run"; + }; + + "194 build mcpp glob input"_test[tag{"requires:gcc"}] = [] { + // #359: an input that is a set of files. A file that appears or goes runs the program + // again (through the build.mcpp cache and the project's fast path); an edited one does + // not on the glob's account; the program's own outputs are never in the set. + s::needs({"gcc"}); + const std::string program = R"cpp(import std; +import mcpp; +int main() { + namespace fs = std::filesystem; + mcpp::rerun_if_changed_glob("inputs/**/*.in"); + std::string root = mcpp::manifest_dir(); + int n = 0; + std::error_code ec; + for (auto const& e : fs::recursive_directory_iterator(root + "/inputs", ec)) + if (e.path().extension() == ".in") ++n; + std::string out = std::string(mcpp::out_dir()) + "/count.cpp"; + { std::ofstream os(out); os << "int count_inputs() { return " << n << "; }\n"; } + { std::ofstream os(root + "/marker.txt"); os << n; } + mcpp::generated(out.c_str()); + return 0; +} +)cpp"; + s::project p{{"mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n"}, + {"src/main.cpp", "#include \nint count_inputs();\nint main() { std::printf(\"COUNT=%d\\n\", count_inputs()); }\n"}, + {"build.mcpp", program}, + {"inputs/a.in", "a\n"}}; + auto ran = [](const e2e::outcome& r) { return s::has_line(s::text(r), ran_again); }; + auto count = [&] { + std::string last; + for (const auto& l : s::lines_with(s::text(p.mcpp({"run"})), "COUNT=")) + if (l.starts_with("COUNT=")) last = l; + return last; + }; + expect(fatal(e2e::succeeded(p.mcpp({"build"})))) << "first build failed"; + expect(eq(count(), std::string("COUNT=1"))); + // 1. A new matching file. + p.write("inputs/b.in", "b\n"); + auto b2 = p.mcpp({"build"}); + expect(fatal(e2e::succeeded(b2))); + expect(ran(b2)) << "adding a matching file did not re-run build.mcpp"; + expect(eq(count(), std::string("COUNT=2"))); + // 2. A removed one. + p.remove("inputs/b.in"); + auto b3 = p.mcpp({"build"}); + expect(fatal(e2e::succeeded(b3))); + expect(ran(b3)) << "removing a matching file did not re-run build.mcpp"; + expect(eq(count(), std::string("COUNT=1"))); + // 3. An edited one: the set is unchanged. + p.write("inputs/a.in", "a totally different body\n"); + auto b4 = p.mcpp({"build"}); + expect(fatal(e2e::succeeded(b4))); + expect(!ran(b4)) << "editing a matched file's content re-ran the program via the glob"; + // 4. A maximal pattern: the program's outputs (marker.txt, target/) are not in the set. + p.write("build.mcpp", std::regex_replace(program, std::regex(R"("inputs/\*\*/\*\.in")"), "\"**\"")); + auto b5 = p.mcpp({"build"}); + expect(fatal(e2e::succeeded(b5))) << "wide-glob build failed"; + expect(ran(b5)) << "the edited program did not run"; + auto b6 = p.mcpp({"build"}); + expect(fatal(e2e::succeeded(b6))) << "second wide-glob build failed"; + expect(!ran(b6)) << "a wide glob re-runs forever — the build output tree is in its set"; + }; + + "612 build program file input fast path"_test[tag{"requires:gcc"}] = [] { + // A file declared with rerun_if_changed is compared on the fast path (a data file is not + // a source, so the mtime sweep cannot see it); an unchanged one still takes the fast path. + // The reading is the program's output. + s::needs({"gcc"}); + auto p = s::project::scaffold("probe"); + for (const auto& m : s::glob(p.at("src"), "*.cppm")) fs::remove(m); + p.write("data/value.txt", "first\n"); + p.write("mcpp.toml", "[package]\nname = \"probe\"\nversion = \"0.1.0\"\n\n[language]\nstandard = \"c++23\"\nmodules = true\nimport_std = true\n\n[targets.probe]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n"); + p.write("build.mcpp", R"cpp(import std; +import mcpp; +int main() { + const std::filesystem::path root(mcpp::manifest_dir()); + const std::filesystem::path gen(std::string(mcpp::out_dir()) + "/include"); + std::error_code ec; + std::filesystem::create_directories(gen, ec); + std::ifstream in(root / "data" / "value.txt"); + std::string value; + std::getline(in, value); + std::ofstream out(gen / "value.h", std::ios::trunc); + out << "#pragma once\n#define PROBE_VALUE \"" << value << "\"\n"; + mcpp::rerun_if_changed((root / "data" / "value.txt").string().c_str()); + mcpp::include_dir(gen.string().c_str()); + return 0; +} +)cpp"); + p.write("src/main.cpp", "#include \n#include \"value.h\"\nint main() { std::printf(\"VALUE=%s\\n\", PROBE_VALUE); }\n"); + auto value = [&] { return s::last_starting(s::text(p.mcpp({"run"})), "VALUE="); }; + expect(fatal(e2e::succeeded(p.mcpp({"build"})))) << "the first build failed"; + expect(eq(value(), std::string("VALUE=first"))); + p.write("data/value.txt", "second\n"); + expect(fatal(e2e::succeeded(p.mcpp({"build"})))) << "the second build failed"; + expect(eq(value(), std::string("VALUE=second"))) << "a declared file input changed and the build replayed the old header"; + // Nothing touched: the second of two builds takes the fast path. + expect(s::has_line(s::text(p.mcpp({"build"})), "Finished .* in ")) << "the third build did not finish"; + expect(s::lacks(s::text(p.mcpp({"build"})), "Compiling probe")) << "an unchanged input recompiled the project"; + }; + + "617 build program knows the cxx stdlib"_test[tag{"requires:elf"}][tag{"requires:gcc"}] = [] { + // mcpp::cxx_stdlib(): the C++ standard library the engine resolved (clang answers "clang" + // with libc++ or libstdc++). The program writes what it read to a file (its stdout is + // shown only when it fails), compared with resolution.json and with the compiler family. + s::needs({"elf", "gcc"}); + s::project p{{"src/main.cpp", "int main() { return 0; }\n"}, + {"build.mcpp", R"cpp(#include +#include +#include +import mcpp; +int main() { + const char* stdlib = mcpp::cxx_stdlib(); + const char* compiler = mcpp::compiler(); + std::string out = std::string(mcpp::manifest_dir()) + "/answer.txt"; + std::FILE* f = std::fopen(out.c_str(), "w"); + if (f == nullptr) return 1; + std::fprintf(f, "compiler=%s\nstdlib=%s\n", + compiler == nullptr ? "" : compiler, + stdlib == nullptr ? "" : stdlib); + std::fclose(f); + return 0; +} +)cpp"}, + {"mcpp.toml", "[package]\nname = \"stdlibq\"\nversion = \"0.1.0\"\n\n[targets.stdlibq]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n"}}; + expect(fatal(e2e::succeeded(p.mcpp({"build"})))) << "build"; + expect(fatal(p.exists("answer.txt"))) << "the build program wrote no answer"; + const auto answer = p.read("answer.txt"); + const auto compiler = s::last_starting(answer, "compiler=").substr(9); + const auto stdlib = s::last_starting(answer, "stdlib=").substr(7); + expect(fatal(!stdlib.empty())) << "mcpp::cxx_stdlib() is empty"; + std::optional res; + for (const auto& f : s::find_files(p.at("target"), R"(resolution\.json)")) + if (!res && !f.generic_string().contains("/.build-mcpp/")) res = f; + expect(fatal(res.has_value())) << "no resolution.json under target/"; + std::smatch m; + const auto doc = s::read_file(*res); + expect(fatal(std::regex_search(doc, m, std::regex(R"re("stdlib"[ \t]*:[ \t]*"([^"]*)")re")))); + expect(eq(stdlib, m[1].str())) << "the build program and the engine disagree"; + expect(eq(compiler, std::string("gcc"))) << "expected the gcc toolchain"; + expect(eq(stdlib, std::string("libstdc++"))) << "gcc's C++ standard library is libstdc++"; + }; + + "632 a build program is told its package name"_test[tag{"requires:gcc"}] = [] { + // mcpp::package_name() and package_namespace() answer the [package] identity; the + // directory (`app`) and the package (`vulkan-saxpy`) differ on purpose, or the case would + // pass against a directory-derived answer. The answer is written to a file. + s::needs({"gcc"}); + s::project p{temp_dir() / "app", + {{"src/main.cpp", "int main() { return 0; }\n"}, + {"build.mcpp", R"cpp(#include +#include +import mcpp; +int main() { + std::string out = std::string(mcpp::manifest_dir()) + "/answered.txt"; + std::FILE* f = std::fopen(out.c_str(), "w"); + if (f == nullptr) return 3; + std::fprintf(f, "name=%s\n", mcpp::package_name()); + std::fprintf(f, "namespace=%s\n", mcpp::package_namespace()); + std::fclose(f); + return 0; +} +)cpp"}, + {"mcpp.toml", "[package]\nname = \"vulkan-saxpy\"\nnamespace = \"example\"\nversion = \"0.1.0\"\n\n[build]\nsources = [\"src/*.cpp\"]\n\n[targets.vulkan-saxpy]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n"}}}; + expect(fatal(e2e::succeeded(p.mcpp({"build"})))) << "build"; + expect(fatal(p.exists("answered.txt"))) << "the build program wrote no answer"; + expect(s::has_line(p.read("answered.txt"), "^name=vulkan-saxpy$")) << "package_name() did not answer the [package] name"; + expect(s::has_line(p.read("answered.txt"), "^namespace=example$")) << "package_namespace() did not answer the [package] namespace"; + expect(eq(p.dir().filename().string(), std::string("app"))) << "this fixture no longer distinguishes the two derivations"; + }; + + "651 a build program deploys what it generated"_test[tag{"requires:elf"}][tag{"requires:gcc"}] = [] { + // mcpp::deploy(from, to) (#622 A4) places a file the program produced beside the artifact: + // `from` may be an action's absolute output; the copy edge's input is that output (read in + // build.ninja); a changed input changes it; a cache hit restores it; pack stages it; + // `to = "../x"` is refused naming the directive and the package; and + // MCPP_TARGET_MIN_PLATFORM_VERSION (#622 A11) is empty on Linux. + s::needs({"elf", "gcc"}); + const std::string build = R"cpp(import mcpp; +#include +#include +int main() { + const std::string root = mcpp::manifest_dir(); + const std::string src = root + "/res.txt"; + const std::string out = std::string(mcpp::out_dir()) + "/gen/res.bin"; + mcpp::action a; + a.id = "gen-res"; + a.role = "source"; + a.arg((root + "/gen.sh").c_str()).arg(src.c_str()).arg(out.c_str()) + .input(src.c_str()) + .output(out.c_str()) + .submit(); + mcpp::deploy(out.c_str(), "app.resources"); + if (FILE* f = std::fopen((std::string(mcpp::out_dir()) + "/minplat.txt").c_str(), "w")) { + std::fputs(mcpp::min_platform_version(), f); + std::fclose(f); + } + return 0; +} +)cpp"; + const std::string manifest = "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[toolchain]\nlinux = \"gcc@16.1.0\"\n\n[targets.app]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n"; + s::project p{temp_dir() / "app", {{"mcpp.toml", manifest}, + {"src/main.cpp", "#include \nint main() { std::puts(\"app\"); return 0; }\n"}, + {"res.txt", "hello resource\n"}, + {"build.mcpp", build}}}; + s::write_executable(p.at("gen.sh"), "#!/usr/bin/env bash\nset -e\ncp \"$1\" \"$2\"\n"); + auto touch = [&] { fs::last_write_time(p.at("src/main.cpp"), fs::file_time_type::clock::now()); }; + // 1. A plain build deploys the generated file beside the executable. + expect(fatal(e2e::succeeded(p.mcpp({"build"})))) << "build failed"; + std::optional deployed; + for (const auto& f : s::find_files(p.at("target"), R"(res\.bin)")) + if (!deployed && f.generic_string().ends_with("/bin/app.resources/res.bin")) deployed = f; + expect(fatal(deployed.has_value())) << "the deployed file does not exist"; + const auto bindir = deployed->parent_path().parent_path(); + expect(eq(s::read_file(*deployed), std::string("hello resource\n"))) << "the deployed file does not carry the action's output"; + auto minplat = s::find_file(p.at("target"), "minplat.txt"); + expect(fatal(minplat.has_value())) << "the program never wrote minplat.txt"; + expect(eq(s::read_file(*minplat), std::string())) << "MCPP_TARGET_MIN_PLATFORM_VERSION was not empty on Linux"; + // 2. The copy edge's input is the action's declared output. + std::optional graph; + for (const auto& f : s::find_files(p.at("target"), R"(build\.ninja)")) + if (!graph && !f.generic_string().contains("/.build-mcpp/")) graph = f; + expect(fatal(graph.has_value())) << "no build.ninja"; + const auto g = s::read_file(*graph); + std::string action_out, stage; + for (const auto& l : s::lines_with(g, " : mcpp_action_")) + if (action_out.empty() && l.contains("gen/res.bin") && l.starts_with("build ")) + action_out = l.substr(6, l.find(' ', 6) - 6); + for (const auto& l : s::lines_with(g, "stage_file")) + if (stage.empty() && l.contains("app.resources/res.bin")) stage = l; + expect(fatal(!action_out.empty())) << "no build.ninja action edge names gen/res.bin"; + expect(fatal(!stage.empty())) << "no stage_file edge for app.resources/res.bin"; + expect(e2e::contains(stage, action_out)) << "the copy edge's input is not the action's own output path"; + // 3. A changed input changes the deployed file. + p.write("res.txt", "changed resource\n"); + touch(); + expect(fatal(e2e::succeeded(p.mcpp({"build"})))) << "second build failed"; + expect(eq(s::read_file(*deployed), std::string("changed resource\n"))) << "touching the action's input did not change the deployed file"; + // 4. bin/ deleted, rebuilt on a build.mcpp cache hit. + fs::remove_all(bindir); + touch(); + auto b3 = p.mcpp({"build", "-v"}); + expect(fatal(e2e::succeeded(b3))) << "third build failed"; + expect(s::has_line(s::text(b3), R"(^ *build\.mcpp .* cached)")) << "the third build re-ran build.mcpp; the replay path was not exercised"; + expect(fatal(fs::is_regular_file(*deployed))) << "the deployed file was not restored on a build.mcpp cache hit"; + expect(eq(s::read_file(*deployed), std::string("changed resource\n"))) << "the restored file does not carry the action's latest output"; + // 5. pack --pkg-format dir stages it. + expect(fatal(e2e::succeeded(p.mcpp({"pack", "--pkg-format", "dir"})))) << "pack --pkg-format dir failed"; + auto staged = s::find_file(p.at("target/dist"), "res.bin"); + expect(fatal(staged.has_value())) << "mcpp pack --pkg-format dir did not stage the deployed file"; + expect(staged->generic_string().ends_with("/bin/app.resources/res.bin")) << "the staged file is not at bin/app.resources/res.bin"; + // 6. `to = "../x"` is refused. + fs::copy(p.dir(), temp_dir() / "escapes", fs::copy_options::recursive); + s::project esc{temp_dir() / "escapes", {}}; + esc.write("mcpp.toml", std::regex_replace(manifest, std::regex("name = \"app\""), "name = \"escapes\"")); + esc.write("build.mcpp", std::regex_replace(build, std::regex(R"re(mcpp::deploy\(out\.c_str\(\), "app\.resources"\))re"), "mcpp::deploy(out.c_str(), \"../x\")")); + auto b5 = esc.mcpp({"build"}); + expect(b5.started && b5.status != 0) << "a deploy 'to' escaping the executable directory was accepted"; + expect(e2e::contains(s::text(b5), "deploy")) << "the refusal does not name the directive"; + expect(e2e::contains(s::text(b5), "escapes")) << "the refusal does not name the package"; + expect(e2e::contains(s::text(b5), "component")) << "the refusal does not name the path problem"; + }; + + "683 a build program reads the payload pkg config view"_test = [] { + // mcpp::pkg_config_libdir() names the registry SubOS's usr/lib/pkgconfig and + // usr/share/pkgconfig, in that order (#634 A7); it is an accessor, not an environment + // default, so the program's environment carries no PKG_CONFIG_LIBDIR. + s::project p{{"mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n"}, + {"src/main.cpp", "int main() { return 0; }\n"}, + {"build.mcpp", R"cpp(import std; +import mcpp; +int main() { + std::string line = std::string("libdir=") + mcpp::pkg_config_libdir(); + const char* inherited = std::getenv("PKG_CONFIG_LIBDIR"); + line += std::string(" inherited=") + (inherited ? inherited : "(unset)"); + mcpp::warning(line.c_str()); + return 0; +} +)cpp"}}; + auto b = p.mcpp({"build"}, {.env = {{"PKG_CONFIG_LIBDIR", std::nullopt}}}); + expect(fatal(e2e::succeeded(b))) << "build failed"; + std::smatch m; + const auto env = p.mcpp({"self", "env"}).out; + expect(fatal(std::regex_search(env, m, std::regex("(^|\n)xlings home *= *([^\r\n]*)")))) << "cannot read the registry from mcpp self env"; + // `self env` prints the native spelling; the accessor joins generic paths with the list + // separator, which `.` matches. + auto registry = std::regex_replace(m[2].str(), std::regex(R"(\\)"), "/"); + const auto quoted = std::regex_replace(registry, std::regex(R"([.^$|()\[\]{}*+?\\])"), R"(\$&)"); + const auto text = s::text(b); + const auto at = text.find("libdir="); + expect(fatal(at != std::string::npos)) << "the build program printed no libdir"; + const auto line = text.substr(at, text.find('\n', at) - at); + expect(std::regex_match(line, std::regex("libdir=" + quoted + "/subos/default/usr/lib/pkgconfig." + quoted + + "/subos/default/usr/share/pkgconfig inherited=\\(unset\\)\r?"))) + << "A/B: the build program read '" << line << "'"; + }; + + "823 mcpp core and mcpp name one interface"_test[tag{"requires:unix-shell"}] = [] { + // #734 E8: the build-program interface is `mcpp.core` (SPEC-007) and `mcpp` its permanent + // equivalent; a program may import either, or both. + s::needs({"unix-shell"}); + for (auto [label, imports] : {std::pair{"C1", "import mcpp.core;"}, std::pair{"C2", "import mcpp;"}, + std::pair{"C3", "import mcpp.core;\nimport mcpp;"}}) { + s::project p{temp_dir() / label, {{"mcpp.toml", "[package]\nname = \"core823\"\nversion = \"0.1.0\"\n"}, + {"src/main.cpp", "#include \nint main() { std::printf(\"%d\\n\", CORE823); return 0; }\n"}}}; + p.write("build.mcpp", std::format("{}\nint main() {{\n mcpp::cxxflag(\"-DCORE823=823\");\n return 0;\n}}\n", imports)); + expect(fatal(e2e::succeeded(p.mcpp({"build"})))) << label << ": the build failed"; + auto r = p.mcpp({"run"}); + expect(e2e::succeeded(r)) << label << ": the program did not run"; + expect(s::has_line(s::text(r), "^823$")) << label << ": the directive did not take effect"; + } + }; + + "824 a build program reads the resolved toolchain"_test[tag{"requires:unix-shell"}] = [] { + // #734 E2 (protocol 14): the resolved toolchain as facts. B1 tool("cxx") and tool("cc") + // exist, tool("ld") is the driver; B2 abi_tool equals tool off the MSVC ABI; B3 the MSVC + // facts are empty; B4 the identity is " " with no path; B5 + // ninja_program() runs; B6 cxx_runtime() follows the manifest. + s::needs({"unix-shell"}); + s::project p{{"src/main.cpp", "int main() { return 0; }\n"}, + {"build.mcpp", R"cpp(import std; +import mcpp.core; +int main() { + std::ofstream out(std::string(mcpp::out_dir()) + "/info.txt"); + for (const char* r : {"cc", "cxx", "ld", "ar", "rc", "as", "mt"}) + out << "tool." << r << "=" << mcpp::tool(r) << "\n" + << "abi." << r << "=" << mcpp::abi_tool(r) << "\n"; + out << "env=" << mcpp::tool_env() << "\n" + << "identity=" << mcpp::toolset_identity() << "\n" + << "instance=" << mcpp::msvc_instance_dir() << "\n" + << "ninja=" << mcpp::ninja_program() << "\n" + << "runtime=" << mcpp::cxx_runtime() << "\n" + << "crt=" << mcpp::msvc_crt_linkage() << "\n"; + return 0; +} +)cpp"}, + {"mcpp.toml", "[package]\nname = \"info824\"\nversion = \"0.1.0\"\n"}}; + expect(fatal(e2e::succeeded(p.mcpp({"build"})))) << "the build failed"; + auto info = s::find_file(p.at("target"), "info.txt"); + expect(fatal(info.has_value() && fs::file_size(*info) > 0)) << "the build program wrote no info.txt"; + auto val = [](const fs::path& f, std::string_view key) { + for (const auto& l : s::lines_with(s::read_file(f), "=")) + if (l.starts_with(std::string(key) + "=")) return l.substr(key.size() + 1); + return std::string(); + }; + for (auto r : {"cxx", "cc"}) { + const fs::path t = val(*info, std::string("tool.") + r); + expect(!t.empty() && fs::is_regular_file(t) && (fs::status(t).permissions() & fs::perms::owner_exec) != fs::perms::none) + << "B1: tool(" << r << ") is not an executable file"; + } + expect(eq(val(*info, "tool.ld"), val(*info, "tool.cxx"))) << "B1: tool(ld) is not the driver"; + for (std::string r : {"cc", "cxx", "ld", "ar", "rc", "as", "mt"}) + expect(eq(val(*info, "abi." + r), val(*info, "tool." + r))) << "B2: abi_tool(" << r << ") differs from tool(" << r << ") off the MSVC ABI"; + for (auto k : {"env", "instance", "crt"}) + expect(eq(val(*info, k), std::string())) << "B3: " << k << " is not empty off the MSVC ABI"; + const auto id = val(*info, "identity"); + expect(e2e::matches(id, "^(clang|gcc) [0-9]")) << "B4: identity '" << id << "' is not ' '"; + expect(s::lacks(id, "/")) << "B4: identity holds a path"; + expect(e2e::succeeded(s::run(val(*info, "ninja"), {"--version"}))) << "B5: ninja_program() does not run"; + expect(eq(val(*info, "runtime"), std::string("self-contained"))) << "B6: the default contract"; + const auto first = fs::last_write_time(*info); + p.write("mcpp.toml", "[package]\nname = \"info824\"\nversion = \"0.1.0\"\n\n[build]\ncxx_runtime = \"host-coupled\"\n"); + expect(fatal(e2e::succeeded(p.mcpp({"build"})))) << "the second build failed"; + // The file written by this build (one newer than the first), else the one there is. + std::optional again; + for (const auto& f : s::find_files(p.at("target"), R"(info\.txt)")) + if (!again && fs::last_write_time(f) > first) again = f; + if (!again) again = s::find_file(p.at("target"), "info.txt"); + expect(eq(val(*again, "runtime"), std::string("host-coupled"))) << "B6: the stated contract"; + }; + + "958 a build program runs again when its output is gone"_test[tag{"requires:gcc"}] = [] { + // A cached run is replayed only while what the program wrote is there: with its include + // directory deleted, a replay would hand the compile an empty directory, so the program + // runs again. A: a second build replays and compiles nothing; B: output deleted, it runs. + s::needs({"gcc"}); + s::project p{{"build.mcpp", R"cpp(#include +#include +#include +import mcpp; +int main() { + const std::string inc = std::string(mcpp::out_dir()) + "/inc"; + std::filesystem::create_directories(inc); + std::ofstream(inc + "/generated.h") << "#define GENERATED 7\n"; + mcpp::include_dir(inc.c_str()); + return 0; +} +)cpp"}, + {"src/main.cpp", "#include \n#include \nint main() { std::printf(\"generated %d\\n\", GENERATED); return 0; }\n"}, + {"mcpp.toml", "[package]\nname = \"regen\"\nversion = \"0.1.0\"\n"}}; + auto a1 = p.mcpp({"run"}); + expect(fatal(e2e::succeeded(a1))) << "A: the first build"; + expect(e2e::contains(s::text(a1), "generated 7")) << "A: the header did not arrive"; + auto a2 = p.mcpp({"build", "-v"}); + expect(fatal(e2e::succeeded(a2))) << "A: the second build"; + expect(s::lacks(s::text(a2), "Compiling regen")) << "A: the second build compiled"; + auto out = s::find_file(p.at("target"), "generated.h"); + expect(fatal(out.has_value())) << "B: the generated header is not under target/"; + fs::remove_all(out->parent_path()); + for (const auto& d : s::glob(p.at("target"), "*/*/bin")) fs::remove_all(d); + for (const auto& d : s::glob(p.at("target"), "*/*/obj")) fs::remove_all(d); + auto b = p.mcpp({"run", "-v"}); + expect(fatal(e2e::succeeded(b))) << "B: the build after the output was deleted"; + expect(e2e::contains(s::text(b), "generated 7")) << "B: the program did not run again"; + }; + + "1036 a context states the engine values in process"_test[tag{"requires:gcc"}] = [] { + // docs/30 "Testing a program's logic": within a mcpp::context the accessors read the + // values it states, the innermost first, the rest from the context around it or the + // environment; has_feature and dep_dir answer within them. + s::needs({"gcc"}); + auto p = s::project::scaffold("ctx"); + p.write("build.mcpp", R"cpp(import std; +import mcpp; +int main() { + const std::string os = mcpp::target_os(); + std::string seen; + { + mcpp::context outer({{"MCPP_TARGET_OS", "plan9"}, {"MCPP_FEATURE_FAST", "1"}}); + seen += mcpp::target_os(); + seen += mcpp::has_feature("fast") ? " fast" : " slow"; + { + mcpp::context inner({{"MCPP_PROFILE", "inner"}, {"MCPP_DEP_ZLIB_DIR", "/z"}}); + seen += std::format(" {} {} {}", mcpp::target_os(), mcpp::profile(), mcpp::dep_dir("zlib")); + } + seen += std::format(" {}", std::string(mcpp::profile()) == "inner" ? "leaked" : "restored"); + } + seen += std::string(mcpp::target_os()) == os ? " environment" : " leaked"; + std::ofstream("src/seen.cpp") << "extern \"C\" const char* seen() { return \"" << seen << "\"; }\n"; + mcpp::generated("src/seen.cpp"); + return 0; +} +)cpp"); + p.write("src/main.cpp", "import std;\nextern \"C\" const char* seen();\nint main() { std::println(\"{}\", seen()); return 0; }\n"); + auto r = p.mcpp({"run"}); + expect(fatal(e2e::succeeded(r))) << "the build or the program failed"; + expect(e2e::contains(s::text(r), "plan9 fast plan9 inner /z restored environment")) << "the values in scope"; + }; +}}; + +} // namespace diff --git a/tests/support/src/support.cpp b/tests/support/src/support.cpp index 2dbffdcbe..b63c2a48a 100644 --- a/tests/support/src/support.cpp +++ b/tests/support/src/support.cpp @@ -148,6 +148,63 @@ bool probe(std::string_view c) { } // namespace +namespace { +// `sort -V` order of two versions: numbers as numbers, the rest as text. +bool version_less(std::string_view a, std::string_view b) { + std::size_t i = 0, j = 0; + while (i < a.size() && j < b.size()) { + if (std::isdigit(static_cast(a[i])) && std::isdigit(static_cast(b[j]))) { + std::size_t ie = i, je = j; + while (ie < a.size() && std::isdigit(static_cast(a[ie]))) ++ie; + while (je < b.size() && std::isdigit(static_cast(b[je]))) ++je; + const auto x = std::stoull(std::string(a.substr(i, ie - i))); + const auto y = std::stoull(std::string(b.substr(j, je - j))); + if (x != y) return x < y; + i = ie; j = je; + } else { + if (a[i] != b[j]) return a[i] < b[j]; + ++i; ++j; + } + } + return a.size() - i < b.size() - j; +} +} // namespace + +std::optional newest_payload(std::string_view package, std::string_view probe) { + for (const auto& root : {home_dir() / "registry/data/xpkgs", user_dir() / ".xlings/data/xpkgs"}) { + std::vector versions; + for (const auto& v : glob(root / package, "*")) + if (probe.empty() || !glob(v, probe).empty()) + versions.push_back(v.filename().string()); + if (!versions.empty()) + return *std::ranges::max_element(versions, version_less); + } + return std::nullopt; +} + +std::string family_version(std::string_view package, std::string_view override, std::string_view fallback) { + if (auto v = env(override); v && !v->empty()) + return *v; + // _toolchain_env.sh reads the home's registry only (USERPROFILE's on Windows when unset). + std::vector versions; + static const std::regex numeric{R"(^[0-9]+(\.[0-9]+)*$)"}; + for (const auto& v : glob(home_dir() / "registry/data/xpkgs" / package, "*")) { + const auto name = v.filename().string(); + if (std::regex_match(name, numeric)) + versions.push_back(name); + } + if (versions.empty()) + return std::string(fallback); + return *std::ranges::max_element(versions, version_less); +} + +fs::path payload_root(std::string_view package, std::string_view version) { + const auto mine = home_dir() / "registry/data/xpkgs" / package / version; + if (std::error_code ec; fs::exists(mine, ec)) + return mine; + return user_dir() / ".xlings/data/xpkgs" / package / version; +} + bool has(std::string_view capability) { static std::mutex m; static std::map> known; @@ -167,6 +224,27 @@ void needs(std::initializer_list capabilities) { // ---- mcpp ---------------------------------------------------------------------------------------- +fs::path repository() { + // The directory holding tests/support/mcpp.toml: above this source file when the compiler + // named it absolutely, else above the working directory (`mcpp test` runs in the package). + const auto holds = [](const fs::path& d) { + std::error_code ec; + return fs::is_regular_file(d / "tests/support/mcpp.toml", ec); + }; + std::vector starts; + if (const fs::path here = std::source_location::current().file_name(); here.is_absolute()) + starts.push_back(here.parent_path()); + starts.push_back(fs::current_path()); + for (auto d : starts) + for (; !d.empty(); d = d.parent_path()) { + if (holds(d)) + return d; + if (d == d.parent_path()) + break; + } + mcpplibs::mtest::fail("the mcpp repository was not found above this program"); +} + std::string mcpp_path() { if (auto v = env("MCPP_BIN_MCPP"); v && !v->empty()) return *v; @@ -184,10 +262,8 @@ e2e::command mcpp_command(std::vector args, const fs::path& cwd, co c.argv.push_back(mcpp_path()); for (auto& a : args) c.argv.push_back(std::move(a)); c.cwd = cwd; - if (o.where == home::fresh) { - auto h = mcpplibs::mtest::temp_dir() / "home"; - fs::create_directories(h); - c.env.emplace_back("MCPP_HOME", h.string()); + if (o.where != home::shared) { + c.env.emplace_back("MCPP_HOME", case_home(o.where).string()); } else if (!env("MCPP_HOME")) { if (auto u = env(on_windows() ? "USERPROFILE" : "HOME")) c.env.emplace_back("MCPP_HOME", (fs::path(*u) / ".mcpp").string()); @@ -200,6 +276,93 @@ e2e::command mcpp_command(std::vector args, const fs::path& cwd, co std::string text(const e2e::outcome& r) { return r.out + r.err; } +fs::path shared_home() { return home_dir(); } + +namespace { +void link_or_copy(const fs::path& from, const fs::path& to) { + std::error_code ec; + if (fs::exists(fs::symlink_status(to, ec))) + return; + fs::create_directory_symlink(from, to, ec); + if (ec) + fs::copy(from, to, fs::copy_options::recursive, ec); +} +// _inherit_toolchain.sh's link_xpkg_payloads: each package directory of `from` is a directory of +// `to` whose versions are links; anything else is linked whole. +void link_payloads(const fs::path& from, const fs::path& to) { + std::error_code ec; + if (!fs::is_directory(from, ec)) + return; + fs::create_directories(to); + for (const auto& e : fs::directory_iterator(from, ec)) { + const auto there = to / e.path().filename(); + if (!e.is_directory(ec) || fs::is_symlink(fs::symlink_status(there, ec))) { + link_or_copy(e.path(), there); + continue; + } + fs::create_directories(there); + for (const auto& v : fs::directory_iterator(e.path(), ec)) + link_or_copy(v.path(), there / v.path().filename()); + } +} +void inherit(const fs::path& h, bool whole) { + const auto user = home_dir(); + const auto xlings = user_dir() / ".xlings/data/xpkgs"; + link_payloads(user / "registry/data/xpkgs", h / "registry/data/xpkgs"); + link_payloads(user / "xpkgs", h / "xpkgs"); + link_payloads(xlings, h / "registry/data/xpkgs"); + std::error_code ec; + for (auto name : {"xim-pkgindex", "xim-index-repos"}) + if (fs::is_directory(user / "registry/data" / name, ec)) { + fs::create_directories(h / "registry/data"); + link_or_copy(user / "registry/data" / name, h / "registry/data" / name); + } + if (whole && env("MCPP_INHERIT_SUBOS").value_or("1") != "0" && fs::is_directory(user / "registry/subos", ec)) + link_or_copy(user / "registry/subos", h / "registry/subos"); + if (whole && env("MCPP_INHERIT_CONFIG").value_or("1") != "0" && fs::is_regular_file(user / "config.toml", ec)) + fs::copy_file(user / "config.toml", h / "config.toml", fs::copy_options::overwrite_existing, ec); + if (fs::is_directory(user / "bin", ec)) + link_or_copy(user / "bin", h / "bin"); +} +} // namespace + +namespace { +// The homes of the cases, made under the system's temporary directory: a payload may hold its +// install directory in a fixed space (glibc's binaries hold 255 bytes), and a case's own directory +// is too long for it, where the scripts' `mktemp -d` was not. Removed when the program ends. +struct homes { + std::mutex m; + std::map of_case; + ~homes() { + std::error_code ec; + for (const auto& [_, h] : of_case) fs::remove_all(h, ec); + } +}; +homes& case_homes() { + static homes h; + return h; +} +} // namespace + +fs::path case_home(home kind) { + auto& all = case_homes(); + const auto key = mcpplibs::mtest::temp_dir(); + std::scoped_lock lock(all.m); + if (auto it = all.of_case.find(key); it != all.of_case.end()) + return it->second; + std::random_device rd; + fs::path h; + std::error_code ec; + do h = fs::temp_directory_path() / std::format("mcpp-e2e-home-{:08x}", rd()); + while (!fs::create_directory(h, ec) && !ec); + all.of_case.emplace(key, h); + if (kind == home::linked && fs::is_directory(home_dir() / "registry", ec)) + link_or_copy(home_dir() / "registry", h / "registry"); + else if (kind == home::inherited || kind == home::payloads) + inherit(h, kind == home::inherited); + return h; +} + // ---- a project ----------------------------------------------------------------------------------- namespace { @@ -278,6 +441,49 @@ std::vector find_files(const fs::path& root, std::string_view pattern) return found; } +std::vector listing(const fs::path& root) { + std::vector out; + std::error_code ec; + for (auto it = fs::recursive_directory_iterator(root, fs::directory_options::skip_permission_denied, ec); + it != fs::recursive_directory_iterator(); it.increment(ec)) + out.push_back(it->path().lexically_relative(root).generic_string()); + std::ranges::sort(out); + return out; +} + +std::optional newest_file(const fs::path& root, std::string_view name) { + std::optional best; + fs::file_time_type when{}; + std::error_code ec; + if (!fs::is_directory(root, ec)) + return best; + for (auto it = fs::recursive_directory_iterator(root, fs::directory_options::skip_permission_denied, ec); + it != fs::recursive_directory_iterator(); it.increment(ec)) { + if (!it->is_regular_file(ec) || it->path().filename() != name) + continue; + const auto t = fs::last_write_time(it->path(), ec); + if (!best || t > when) { best = it->path(); when = t; } + } + return best; +} + +void write_executable(const fs::path& file, std::string_view content) { + write_into(file.parent_path(), file.filename().string(), content); + std::error_code ec; + fs::permissions(file, fs::perms::owner_all | fs::perms::group_read | fs::perms::group_exec | + fs::perms::others_read | fs::perms::others_exec, ec); +} + +e2e::outcome run(const fs::path& program, std::vector args, const fs::path& cwd, + std::vector>> env) { + e2e::command c; + c.argv.push_back(program.string()); + for (auto& a : args) c.argv.push_back(std::move(a)); + c.cwd = cwd; + c.env = std::move(env); + return e2e::run(std::move(c)); +} + namespace { // A shell's `*` within one name; no leading dot unless the pattern has one. bool wildcard(std::string_view pattern, std::string_view name) { @@ -411,6 +617,28 @@ mcpplibs::mtest::comparison contains_nocase(std::string_view text, std::string_v return {lower(text).contains(lower(needle)), "contains_nocase", std::format("text containing \"{}\" (any case)", needle), tail_of(text)}; } +mcpplibs::mtest::comparison has_line_nocase(std::string_view text, std::string_view pattern) { + const std::regex re{std::string(pattern), std::regex::ECMAScript | std::regex::icase}; + bool held = false; + for (auto l : split_lines(text)) + held = held || std::regex_search(l.begin(), l.end(), re); + return {held, "has_line_nocase", std::format("a line matching /{}/i", pattern), tail_of(text)}; +} +std::string last_line(std::string_view text) { + auto lines = split_lines(text); + while (!lines.empty() && lines.back().empty()) lines.pop_back(); + return lines.empty() ? std::string{} : std::string(lines.back()); +} +std::string last_starting(std::string_view text, std::string_view prefix) { + std::string found; + for (auto l : split_lines(text)) + if (l.starts_with(prefix)) found = std::string(l); + return found; +} +std::size_t count_matching(std::string_view text, std::string_view pattern) { + const std::regex re{std::string(pattern)}; + return static_cast(std::ranges::count_if(split_lines(text), [&](auto l) { return std::regex_search(l.begin(), l.end(), re); })); +} std::size_t count_lines(std::string_view text, std::string_view needle) { return static_cast(std::ranges::count_if(split_lines(text), [&](auto l) { return l.contains(needle); })); } diff --git a/tests/support/src/support.cppm b/tests/support/src/support.cppm index 6756b9628..20212bbd5 100644 --- a/tests/support/src/support.cppm +++ b/tests/support/src/support.cppm @@ -43,12 +43,39 @@ bool has(std::string_view capability); // Ends the case as skipped ("missing capability: ") unless this machine has every word. void needs(std::initializer_list capabilities); +// The newest version (by `sort -V`) of payload `package` (`xim-x-musl-gcc`) for which +// `/` (a glob) exists, in the shared home's registry or, failing that, the user's +// xlings (tests/e2e/_toolchain_env.sh). +std::optional newest_payload(std::string_view package, std::string_view probe = ""); +// tests/e2e/_toolchain_env.sh: the version of a toolchain family the case uses, the variable +// `override` when set, else the newest numeric version installed, else `fallback`. +std::string family_version(std::string_view package, std::string_view override, std::string_view fallback); +inline std::string llvm_version() { return family_version("xim-x-llvm", "MCPP_E2E_LLVM_VERSION", "23.1.3"); } +inline std::string gcc_version() { return family_version("xim-x-gcc", "MCPP_E2E_GCC_VERSION", "16.1.0"); } +inline std::string musl_gcc_version() { return family_version("xim-x-musl-gcc", "MCPP_E2E_MUSL_GCC_VERSION", "15.1.0"); } +inline std::string mingw_cross_version() { return family_version("xim-x-mingw-cross-gcc", "MCPP_E2E_MINGW_CROSS_VERSION", "16.1.0"); } +// The directory of that payload's version. +fs::path payload_root(std::string_view package, std::string_view version); + +// The root of the mcpp repository these cases belong to (its examples, its fixtures). +fs::path repository(); + // ---- mcpp ---------------------------------------------------------------------------------------- // The mcpp under test. std::string mcpp_path(); -enum class home { shared, fresh }; +// shared: the machine's home. fresh: an empty home of the case. linked: a home of the case whose +// `registry` is a link to the shared home's (its toolchains, not its build cache or state). +// inherited: tests/e2e/_inherit_toolchain.sh's home, a home of the case holding links to each +// payload of the user's homes, their indices, `subos` and `bin`, and a copy of `config.toml`. +// payloads: inherited without config.toml and subos (MCPP_INHERIT_CONFIG=0 MCPP_INHERIT_SUBOS=0). +enum class home { shared, fresh, linked, inherited, payloads }; + +// The home of the case (fresh, linked, inherited): one directory per case, made on first use. +fs::path case_home(home kind = home::fresh); +// The shared home: `MCPP_HOME`, else `~/.mcpp`. +fs::path shared_home(); struct options { home where = home::shared; @@ -109,6 +136,15 @@ std::vector find_files(const fs::path& root, std::string_view pattern) // component matches any run of characters and, as in a shell, no leading dot: // glob(p.at("target"), "*/*/build.ninja"). std::vector glob(const fs::path& root, std::string_view pattern); +// Every path under `root`, relative and sorted (`find | sort`): a tree to compare later. +std::vector listing(const fs::path& root); +// The most recently written regular file under `root` named `name` (`ls -t $(find ...) | head -1`). +std::optional newest_file(const fs::path& root, std::string_view name); +// `content` written to `file` (its directories made), executable by its owner (a fake tool). +void write_executable(const fs::path& file, std::string_view content); +// A program the case built or wrote, run in `cwd` (a minute at most). +e2e::outcome run(const fs::path& program, std::vector args = {}, const fs::path& cwd = {}, + std::vector>> env = {}); // ---- readings -------------------------------------------------------------------------------------- @@ -131,8 +167,16 @@ mcpplibs::mtest::comparison no_line(std::string_view text, std::string_view patt mcpplibs::mtest::comparison lacks(std::string_view text, std::string_view needle); // `text` contains `needle`, ignoring ASCII case (`grep -i`). mcpplibs::mtest::comparison contains_nocase(std::string_view text, std::string_view needle); +// some line of `text` matches `pattern`, ignoring ASCII case (`grep -iE`). +mcpplibs::mtest::comparison has_line_nocase(std::string_view text, std::string_view pattern); +// The last line of `text` that is not empty (`| tail -1` of a program's output). +std::string last_line(std::string_view text); +// The last line of `text` that starts with `prefix` (`grep '^' | tail -1`), or "". +std::string last_starting(std::string_view text, std::string_view prefix); // The number of lines of `text` that contain `needle` (`grep -c`). std::size_t count_lines(std::string_view text, std::string_view needle); +// The number of lines of `text` that match `pattern` (`grep -cE`). +std::size_t count_matching(std::string_view text, std::string_view pattern); // The outputs `.ninja_log` records, one per edge run (its fourth column), in order. std::vector ninja_log_outputs(const fs::path& build_dir); From b4c815182912df97d28a837814ac45de0e7e8624 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 13 Nov 2026 18:30:13 +0900 Subject: [PATCH 006/115] e2e: the deps subsystem as C++ cases (49 scripts) The 49 merge scripts MIGRATION.tsv assigns to `deps` (path, git and index dependencies, how a dependency is named and resolved, mcpp.lock, tool packages a graph declares twice, floors a dependency states, patches and `embed`, entry targets, the advice an edge states) are 49 cases of the program e2e/deps (tests/e2e/deps/*.cpp, 12 files: advice, edges, entry, floors, git, graph, identity, lock, names, path, resolve, tools). Each case is named by its script's sentence and keeps its number; its `# requires:` words are its tags. Scripts: 09 12 24 27 31 32 33 61 62 63 79 161 162 163 165 196 203 326 603 627 628 629 661 679 682 713 816 821 822 844 939 943 944 945 946 948 980 990 1001 1002 1018 1027 1032 1046 1048 1060 1066 1071 1074. No workflow runs any of them by path; all 49 are removed. 23_remove_update.sh is a sink row and stays for the sink step. Old run (the scripts, against this branch's mcpp, Linux x86_64): 49 passed. New run (`mcpp test e2e/deps`): 49 passed, 0 failed, 0 skipped. Every assertion is kept. Where a reading changed: - python3 read JSON or rewrote a lock for 679, 682, 844, 1002, 1027 and 1071; the cases do it in C++ (nlohmann::json, the FNV-1a digest, the lock entry's lines) and 844, 939 (which did not use it) and 1071 no longer require python3. 844's os.utime is std::filesystem::last_write_time. - 628 read `mcpp why toolchain --format json` with jq only when jq was on PATH; the case always reads it. - `ls -1 | wc -l` (627) counts the store's own entries; `touch -d '30 days ago'` (945) sets the time 30 days back. - 822 took its own release from `mcpp --version | awk '{print $2}'`, the case from the second word of that line. - 816's `LC_ALL=C grep $'\r'` and `$'\x1b'` are byte searches of the output. - 713 skips when git is missing, as the script did (exit 77); the other git cases skip the same way. - Diagnostics the scripts printed on failure (`<(find ...)`, `diff`) are not kept; the failing comparison shows both sides instead. - Homes the scripts made with `MCPP_HOME=$TMP/home` (627, 628, 629) are fresh case homes; `_inherit_toolchain.sh` homes are `home::inherited`. docs/specs and docs/06 still name 163, 603, 627, 628 and 679 as the tests of their rules; this change does not touch docs/. --- tests/e2e/09_path_dependency.sh | 111 ----- .../1001_a_dependency_carries_its_patches.sh | 104 ---- tests/e2e/1002_a_dependency_is_embedded.sh | 120 ----- ...amespaced_package_of_one_name_both_link.sh | 28 -- ...oup_keeps_the_other_groups_lock_entries.sh | 108 ----- ...ted_dotted_dependency_key_is_deprecated.sh | 52 -- ...t_diff_from_the_package_root_is_applied.sh | 49 -- ...it_reference_follows_the_default_branch.sh | 102 ---- ...mber_of_a_repository_keeps_its_siblings.sh | 70 --- ...a_merged_version_withdraws_its_requests.sh | 71 --- ...rrides_a_dependency_for_the_whole_graph.sh | 92 ---- ...mespace_package_named_like_a_spec_field.sh | 46 -- tests/e2e/12_add_command.sh | 295 ------------ tests/e2e/161_xpkg_name_form.sh | 115 ----- tests/e2e/162_bare_name_namespace_scope.sh | 183 ------- tests/e2e/163_identity_first_resolution.sh | 150 ------ ..._bare_name_cross_namespace_wire_address.sh | 114 ----- tests/e2e/196_version_identity_and_lock.sh | 231 --------- .../e2e/203_exact_selector_lock_migration.sh | 111 ----- tests/e2e/24_git_dependency.sh | 231 --------- tests/e2e/27_namespace_dependencies.sh | 115 ----- tests/e2e/31_transitive_deps.sh | 124 ----- tests/e2e/326_dependency_standard_floor.sh | 88 ---- tests/e2e/32_semver_merge.sh | 107 ----- tests/e2e/33_multi_version_mangling.sh | 169 ------- tests/e2e/603_version_floor.sh | 79 --- tests/e2e/61_dependency_visibility.sh | 114 ----- tests/e2e/627_one_package_one_version.sh | 127 ----- ...8_a_pin_below_a_stated_floor_is_refused.sh | 117 ----- ...a_version_range_is_answered_by_xpkg_dir.sh | 140 ------ .../62_dotted_dependency_selector_priority.sh | 96 ---- .../63_bare_dependency_peer_root_priority.sh | 99 ---- .../e2e/661_one_identity_two_declarations.sh | 448 ------------------ ...dependency_takes_its_manifests_identity.sh | 139 ------ .../682_why_deps_prints_the_resolved_graph.sh | 49 -- ..._dependency_selects_a_repository_member.sh | 172 ------- .../e2e/79_gtest_regular_dep_feature_main.sh | 59 --- .../816_a_git_dependency_reports_its_fetch.sh | 50 -- ...an_edge_states_its_advice_on_both_paths.sh | 94 ---- ...kage_states_the_oldest_mcpp_it_supports.sh | 75 --- ...build_keeps_one_lock_entry_per_identity.sh | 182 ------- ...ck_written_meanwhile_is_not_overwritten.sh | 100 ---- .../943_a_transitive_git_branch_is_locked.sh | 104 ---- ...ckout_answers_a_version_the_index_lacks.sh | 100 ---- ...nywhere_in_the_graph_asks_for_a_refresh.sh | 90 ---- ..._a_program_begins_in_a_dependency_entry.sh | 204 -------- ...dependency_is_restaged_at_the_same_size.sh | 125 ----- ...mbiguous_dependency_name_is_no_packages.sh | 91 ---- ...does_not_depend_on_the_selected_members.sh | 124 ----- tests/e2e/MIGRATION.tsv | 98 ++-- tests/e2e/deps/advice.cpp | 68 +++ tests/e2e/deps/edges.cpp | 237 +++++++++ tests/e2e/deps/entry.cpp | 139 ++++++ tests/e2e/deps/floors.cpp | 134 ++++++ tests/e2e/deps/git.cpp | 443 +++++++++++++++++ tests/e2e/deps/graph.cpp | 260 ++++++++++ tests/e2e/deps/identity.cpp | 125 +++++ tests/e2e/deps/lock.cpp | 362 ++++++++++++++ tests/e2e/deps/names.cpp | 243 ++++++++++ tests/e2e/deps/path.cpp | 222 +++++++++ tests/e2e/deps/resolve.cpp | 179 +++++++ tests/e2e/deps/tools.cpp | 129 +++++ 62 files changed, 2590 insertions(+), 6013 deletions(-) delete mode 100755 tests/e2e/09_path_dependency.sh delete mode 100755 tests/e2e/1001_a_dependency_carries_its_patches.sh delete mode 100755 tests/e2e/1002_a_dependency_is_embedded.sh delete mode 100755 tests/e2e/1018_a_package_and_a_namespaced_package_of_one_name_both_link.sh delete mode 100755 tests/e2e/1027_a_configuration_group_keeps_the_other_groups_lock_entries.sh delete mode 100755 tests/e2e/1032_a_quoted_dotted_dependency_key_is_deprecated.sh delete mode 100755 tests/e2e/1046_a_patch_made_with_git_diff_from_the_package_root_is_applied.sh delete mode 100755 tests/e2e/1048_a_git_reference_follows_the_default_branch.sh delete mode 100755 tests/e2e/1060_a_patched_member_of_a_repository_keeps_its_siblings.sh delete mode 100755 tests/e2e/1066_a_merged_version_withdraws_its_requests.sh delete mode 100755 tests/e2e/1071_the_root_overrides_a_dependency_for_the_whole_graph.sh delete mode 100755 tests/e2e/1074_a_namespace_package_named_like_a_spec_field.sh delete mode 100755 tests/e2e/12_add_command.sh delete mode 100755 tests/e2e/161_xpkg_name_form.sh delete mode 100755 tests/e2e/162_bare_name_namespace_scope.sh delete mode 100755 tests/e2e/163_identity_first_resolution.sh delete mode 100755 tests/e2e/165_bare_name_cross_namespace_wire_address.sh delete mode 100755 tests/e2e/196_version_identity_and_lock.sh delete mode 100755 tests/e2e/203_exact_selector_lock_migration.sh delete mode 100755 tests/e2e/24_git_dependency.sh delete mode 100755 tests/e2e/27_namespace_dependencies.sh delete mode 100755 tests/e2e/31_transitive_deps.sh delete mode 100755 tests/e2e/326_dependency_standard_floor.sh delete mode 100755 tests/e2e/32_semver_merge.sh delete mode 100755 tests/e2e/33_multi_version_mangling.sh delete mode 100755 tests/e2e/603_version_floor.sh delete mode 100644 tests/e2e/61_dependency_visibility.sh delete mode 100755 tests/e2e/627_one_package_one_version.sh delete mode 100755 tests/e2e/628_a_pin_below_a_stated_floor_is_refused.sh delete mode 100755 tests/e2e/629_a_version_range_is_answered_by_xpkg_dir.sh delete mode 100644 tests/e2e/62_dotted_dependency_selector_priority.sh delete mode 100755 tests/e2e/63_bare_dependency_peer_root_priority.sh delete mode 100755 tests/e2e/661_one_identity_two_declarations.sh delete mode 100755 tests/e2e/679_a_path_dependency_takes_its_manifests_identity.sh delete mode 100755 tests/e2e/682_why_deps_prints_the_resolved_graph.sh delete mode 100755 tests/e2e/713_a_git_dependency_selects_a_repository_member.sh delete mode 100755 tests/e2e/79_gtest_regular_dep_feature_main.sh delete mode 100755 tests/e2e/816_a_git_dependency_reports_its_fetch.sh delete mode 100755 tests/e2e/821_an_edge_states_its_advice_on_both_paths.sh delete mode 100755 tests/e2e/822_a_package_states_the_oldest_mcpp_it_supports.sh delete mode 100644 tests/e2e/844_a_planned_build_keeps_one_lock_entry_per_identity.sh delete mode 100644 tests/e2e/939_a_lock_written_meanwhile_is_not_overwritten.sh delete mode 100755 tests/e2e/943_a_transitive_git_branch_is_locked.sh delete mode 100755 tests/e2e/944_a_root_checkout_answers_a_version_the_index_lacks.sh delete mode 100755 tests/e2e/945_a_miss_anywhere_in_the_graph_asks_for_a_refresh.sh delete mode 100755 tests/e2e/946_a_program_begins_in_a_dependency_entry.sh delete mode 100755 tests/e2e/948_an_upgraded_dependency_is_restaged_at_the_same_size.sh delete mode 100755 tests/e2e/980_an_ambiguous_dependency_name_is_no_packages.sh delete mode 100755 tests/e2e/990_a_lock_key_does_not_depend_on_the_selected_members.sh create mode 100644 tests/e2e/deps/advice.cpp create mode 100644 tests/e2e/deps/edges.cpp create mode 100644 tests/e2e/deps/entry.cpp create mode 100644 tests/e2e/deps/floors.cpp create mode 100644 tests/e2e/deps/git.cpp create mode 100644 tests/e2e/deps/graph.cpp create mode 100644 tests/e2e/deps/identity.cpp create mode 100644 tests/e2e/deps/lock.cpp create mode 100644 tests/e2e/deps/names.cpp create mode 100644 tests/e2e/deps/path.cpp create mode 100644 tests/e2e/deps/resolve.cpp create mode 100644 tests/e2e/deps/tools.cpp diff --git a/tests/e2e/09_path_dependency.sh b/tests/e2e/09_path_dependency.sh deleted file mode 100755 index 59f818d35..000000000 --- a/tests/e2e/09_path_dependency.sh +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/env bash -# requires: elf gcc -# Path-based dependency: package B imports modules from package A via -# [dependencies.A] path = "../A" -# Verifies the multi-package scanner + linker pipeline. -set -e -source "$(dirname "$0")/_host_path.sh" - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT - -cd "$TMP" - -# Package A — provides module mylibA.greet -"$MCPP" new mylibA > /dev/null -cd mylibA -cat > src/greet.cppm <<'EOF' -export module mylibA.greet; -import std; -export auto greet(std::string_view who) -> void { - std::println("Hello {} from mylibA!", who); -} -EOF -rm src/main.cpp - -cat > mcpp.toml <<'EOF' -[package] -name = "mylibA" -version = "0.1.0" -[language] -standard = "c++23" -modules = true -import_std = true -[modules] -sources = ["src/**/*.cppm"] -[targets.mylibA] -kind = "lib" -EOF -cd .. - -# Package B — depends on A via path -"$MCPP" new myappB > /dev/null -cd myappB - -cat > src/main.cpp <<'EOF' -import std; -import mylibA.greet; -int main() { - greet("modular C++23"); - std::println("path dep build OK"); - return 0; -} -EOF - -cat > mcpp.toml <<'EOF' -[package] -name = "myappB" -version = "0.1.0" -[language] -standard = "c++23" -modules = true -import_std = true -[modules] -sources = ["src/**/*.cppm", "src/**/*.cpp"] -[targets.myappB] -kind = "bin" -main = "src/main.cpp" -[dependencies.mylibA] -path = "../mylibA" -EOF - -"$MCPP" build > build.log 2>&1 -out=$("$MCPP" run 2>&1) -[[ "$out" == *"Hello modular C++23 from mylibA"* ]] || { echo "dep module not invoked: $out"; exit 1; } -[[ "$out" == *"path dep build OK"* ]] || { echo "main println missing: $out"; exit 1; } - -# build.ninja should compile sources from BOTH packages -ninja_file="$(find target -name build.ninja -not -path '*/.build-mcpp/*')" -grep -q 'mylibA.*greet.cppm' "$ninja_file" || { echo "ninja missing dep package source"; exit 1; } -# A dependency's BMI lies below its package's directory (pack drive and -# selection design 2026-10-01, B1); the root's would lie at its name. -grep -q 'gcm.cache/[^ /]*mylibA/mylibA.greet.gcm' "$ninja_file" || { - echo "ninja missing dep BMI"; exit 1; } - -# Path-resolution error reporting: declared name mismatch -TMP2=$(mktemp -d) -TMP2_HOST="$(host_path "$TMP2")" -cp -r "$TMP/mylibA" "$TMP2/wrongname" -sed -i.bak 's/name = "mylibA"/name = "differentname"/' "$TMP2/wrongname/mcpp.toml" && rm -f "$TMP2/wrongname/mcpp.toml.bak" -cat > mcpp.toml <&1) && { echo "expected name-mismatch error"; exit 1; } -[[ "$err" == *"mismatch with declared name"* ]] || { echo "wrong error: $err"; exit 1; } - -rm -rf "$TMP2" -echo "OK" diff --git a/tests/e2e/1001_a_dependency_carries_its_patches.sh b/tests/e2e/1001_a_dependency_carries_its_patches.sh deleted file mode 100755 index 3b20c47ff..000000000 --- a/tests/e2e/1001_a_dependency_carries_its_patches.sh +++ /dev/null @@ -1,104 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 1001 -- a dependency edge carries patches (design 2026-11-02 part 3 §14.1). A -# patched library is another library: the verified source is copied to the -# variant's own tree under the mcpp home and patched there, in order, exactly; -# the lock names the variant and each patch with its SHA-256; and a patch that -# does not apply is refused naming the patch and its hunk. -# -# Criteria: -# A. The program sees the patched header; the package's own installed source -# is untouched, and the variant's tree is beside the other variants. -# B. mcpp.lock records the variant, and each patch by its path and SHA-256; -# `mcpp sbom` states the pedigree. -# C. A changed patch is another variant: the build uses the new tree. -# D. A patch that does not apply is refused with the patch's name and the -# hunk, and leaves no variant behind. -# E. `patches` on a `path` dependency is refused. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -export MCPP_HOME="$TMP/mcpp-home" -source "$(dirname "$0")/_inherit_toolchain.sh" -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } - -cd "$TMP" -mkdir -p app/src app/patches && cd app -printf '%s\n' '--- a/zlib-1.3.2/zlib.h' '+++ b/zlib-1.3.2/zlib.h' '@@ -31,4 +31,6 @@' \ - ' #ifndef ZLIB_H' ' #define ZLIB_H' ' ' '+#define MCPP_PATCHED_ZLIB 1' '+' ' #ifdef ZLIB_BUILD' \ - > patches/0001-mark.patch -cat > mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" - -[dependencies] -"compat.zlib" = { version = "1.3.2", patches = ["patches/0001-mark.patch"] } -EOF -cat > src/main.cpp <<'EOF' -#include -#include -int main() { -#ifdef MCPP_PATCHED_ZLIB - std::printf("patched %d %s\n", MCPP_PATCHED_ZLIB, zlibVersion()); - return 0; -#else - std::printf("unpatched\n"); - return 1; -#endif -} -EOF - -# A -"$MCPP" run > a.log 2>&1 || fail "the patched build did not run" a.log -grep -q '^patched 1 1.3.2' a.log || fail "the program did not see the patched header" a.log -variant=$(ls -d "$MCPP_HOME"/cache/src/compat.zlib/1.3.2+p* 2>/dev/null | head -1) -[ -n "$variant" ] && [ -f "$variant/.mcpp-variant" ] || fail "no variant tree under the mcpp home" <(find "$MCPP_HOME/cache" -maxdepth 3) -grep -q 'MCPP_PATCHED_ZLIB' "$variant/zlib-1.3.2/zlib.h" || fail "the variant's header is not patched" "$variant/zlib-1.3.2/zlib.h" -installed=$(ls -d "$MCPP_HOME"/registry/data/xpkgs/compat-x-zlib/1.3.2 2>/dev/null | head -1) -if [ -n "$installed" ]; then - ! grep -q 'MCPP_PATCHED_ZLIB' "$installed"/zlib-1.3.2/zlib.h || fail "the installed source was patched in place" -fi - -# B -grep -q '^variant = ' mcpp.lock || fail "the lock names no variant" mcpp.lock -grep -q 'patches/0001-mark.patch#sha256:[0-9a-f]\{64\}' mcpp.lock || fail "the lock does not name the patch and its digest" mcpp.lock -"$MCPP" emit sbom > sbom.json 2>sbom.err || fail "sbom failed" sbom.err -grep -q '"pedigree"' sbom.json && grep -q '0001-mark.patch' sbom.json || fail "the bill of materials states no pedigree" sbom.json - -# C -sed -i 's/MCPP_PATCHED_ZLIB 1/MCPP_PATCHED_ZLIB 2/' patches/0001-mark.patch -"$MCPP" run > c.log 2>&1 || fail "the build with a changed patch failed" c.log -grep -q '^patched 2 ' c.log || fail "a changed patch did not make another variant" c.log -[ "$(ls -d "$MCPP_HOME"/cache/src/compat.zlib/1.3.2+p* | wc -l)" -ge 2 ] || fail "the changed patch did not get its own tree" <(ls "$MCPP_HOME/cache/src/compat.zlib") - -# D -cat > patches/0001-mark.patch <<'EOF' ---- a/zlib-1.3.2/zlib.h -+++ b/zlib-1.3.2/zlib.h -@@ -31,1 +31,1 @@ --#ifndef NOT_THE_LINE -+#ifndef ZLIB_H -EOF -before=$(ls -d "$MCPP_HOME"/cache/src/compat.zlib/* | wc -l) -if "$MCPP" build > d.log 2>&1; then fail "a patch that does not apply was accepted" d.log; fi -grep -q "0001-mark.patch" d.log && grep -q "hunk 1" d.log || fail "the refusal does not name the patch and the hunk" d.log -[ "$(ls -d "$MCPP_HOME"/cache/src/compat.zlib/* | wc -l)" = "$before" ] || fail "a failed patching left a tree" <(ls "$MCPP_HOME/cache/src/compat.zlib") - -# E -mkdir -p ../lib/src -printf '[package]\nname = "lib"\nversion = "0.1.0"\n' > ../lib/mcpp.toml -printf 'int lib_one() { return 1; }\n' > ../lib/src/lib.cpp -cat > mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" - -[dependencies] -lib = { path = "../lib", patches = ["patches/0001-mark.patch"] } -EOF -if "$MCPP" build > e.log 2>&1; then fail "patches on a path dependency were accepted" e.log; fi -grep -q "is a path and carries patches" e.log || fail "the refusal does not say why" e.log - -echo "OK" diff --git a/tests/e2e/1002_a_dependency_is_embedded.sh b/tests/e2e/1002_a_dependency_is_embedded.sh deleted file mode 100755 index ee3967875..000000000 --- a/tests/e2e/1002_a_dependency_is_embedded.sh +++ /dev/null @@ -1,120 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 1002 -- `embed = true` compiles a dependency into its consumer (design -# 2026-11-02 part 3 §14.1): the dependency's sources, less `exclude`, are the -# consumer's, compiled with the consumer's flags and the dependency's include -# directories and macros; the dependency builds no library of its own, and a -# program that reaches it holds its embedder or is refused. -# -# Criteria: -# A. The program links and runs with zlib embedded in its library package; -# no library of compat.zlib is built, and an excluded file is not compiled. -# B. The dependency's own compile flags reach its files (its `-include` of a -# generated header) and not the consumer's own. -# C. `exclude` without `embed` is refused at the manifest. -# D. A program that reaches the embedded package without its embedder is -# refused, naming both. -# E. An embedder with a build program builds without a warning about the -# flags `embed' gives the dependency's files: the build program's own plan -# compiles none of them, and the entry is the engine's, not a typo. -# F. The embedded package's link flags and libraries are not the program's: -# a dependency naming libraries no system has links when embedded. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -export MCPP_HOME="$TMP/mcpp-home" -source "$(dirname "$0")/_inherit_toolchain.sh" -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } - -cd "$TMP" -mkdir -p zip/src app/src -cat > zip/mcpp.toml <<'EOF' -[package] -name = "zip" -version = "0.1.0" - -[dependencies] -"compat.zlib" = { version = "1.3.2", embed = true, exclude = ["*/uncompr.c"] } -EOF -cat > zip/src/zip.cppm <<'EOF' -module; -#include -export module zip; -export int zip_bound(int n) { return static_cast(compressBound(static_cast(n))); } -export const char* zip_version() { return zlibVersion(); } -EOF -cat > app/mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" - -[dependencies] -zip = { path = "../zip" } -EOF -cat > app/src/main.cpp <<'EOF' -import std; -import zip; -int main() { std::println("{} {}", zip_version(), zip_bound(100) > 100); } -EOF - -cat > zip/build.mcpp <<'EOF' -import std; -int main() { return 0; } -EOF - -# A -cd app -"$MCPP" run > a.log 2>&1 || fail "the program with an embedded zlib did not run" a.log - -# E -! grep -q 'matched no source file' a.log || fail "the flags embed gives were reported as a glob that matched nothing" a.log -grep -q '^1.3.2 true' a.log || fail "the program did not call the embedded zlib" a.log -dir=$(ls -d target/*/*/ | grep -E '/[0-9a-f]{16}/$' | head -1) -db=$(find "$dir" -name compile_commands.json | head -1) -[ -n "$db" ] || db=compile_commands.json -grep -q 'deflate.c' "$db" || fail "zlib's sources were not compiled" "$db" -! grep -q 'uncompr.c' "$db" || fail "an excluded file was compiled" "$db" -! find "$dir" -name '*zlib*.a' -o -name '*zlib*.lib' | grep -q . || fail "a library of the embedded package was built" <(find "$dir" -name '*.a') - -# B -grep -q 'mcpp_zlib_config.h' "$db" || fail "the dependency's own flags did not reach its files" "$db" -python3 - "$db" <<'PY' || fail "the dependency's flags reached the consumer's own unit" "$db" -import json, sys -for e in json.load(open(sys.argv[1])): - if e["file"].endswith("zip.cppm") and "mcpp_zlib_config.h" in " ".join(e.get("arguments", [])) + e.get("command", ""): - sys.exit(1) -PY -cd .. - -# C -mkdir -p bad/src && cd bad -printf '[package]\nname = "bad"\nversion = "0.1.0"\n\n[dependencies]\n"compat.zlib" = { version = "1.3.2", exclude = ["*/gzwrite.c"] }\n' > mcpp.toml -printf 'int main() {}\n' > src/main.cpp -if "$MCPP" build > c.log 2>&1; then fail "exclude without embed was accepted" c.log; fi -grep -q "exclude is allowed only with embed" c.log || fail "the refusal does not say why" c.log -cd .. - -# D -- one configuration of a workspace: `app` holds the embedder, `plain` -# reaches compat.zlib directly. The embedded package compiles nothing of its -# own there, so `plain` would link its declarations and no definitions. -mkdir -p ws/plain/src && cd ws -cp -r ../app ../zip . -rm -rf app/target -printf '[workspace]\nmembers = ["app", "plain", "zip"]\n' > mcpp.toml -printf '[package]\nname = "plain"\nversion = "0.1.0"\n\n[dependencies]\n"compat.zlib" = "1.3.2"\n' > plain/mcpp.toml -printf '#include \nint main() { return zlibVersion()[0] == 0; }\n' > plain/src/main.cpp -if "$MCPP" build --workspace > d.log 2>&1; then fail "a program without the embedder was accepted" d.log; fi -grep -q "which 'zip' embeds" d.log || fail "the refusal does not name the embedder" d.log -cd .. - -# F -mkdir -p lk/src lkapp/src && cd lkapp -printf '[package]\nname = "lk"\nversion = "0.1.0"\n\n[build]\nldflags = ["-lmcpp-no-such-library"]\n\n[runtime]\nlibraries = ["mcpp-no-such-runtime-library"]\n' > ../lk/mcpp.toml -printf 'int lk_answer(void) { return 42; }\n' > ../lk/src/lk.c -printf '[package]\nname = "lkapp"\nversion = "0.1.0"\n\n[dependencies]\nlk = { path = "../lk", embed = true }\n' > mcpp.toml -printf 'extern "C" int lk_answer();\nint main() { return lk_answer() == 42 ? 0 : 1; }\n' > src/main.cpp -"$MCPP" run > f.log 2>&1 || fail "the embedded package's link flags or libraries reached the program's link" f.log -cd .. - -echo "OK" diff --git a/tests/e2e/1018_a_package_and_a_namespaced_package_of_one_name_both_link.sh b/tests/e2e/1018_a_package_and_a_namespaced_package_of_one_name_both_link.sh deleted file mode 100755 index 59ddadbd2..000000000 --- a/tests/e2e/1018_a_package_and_a_namespaced_package_of_one_name_both_link.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -# A PACKAGE AND A NAMESPACED PACKAGE OF ONE NAME BOTH LINK (mcpp#896). -# `foo` and `ns.foo` are two packages. The plan found a package by its bare -# name too, and the bare `foo` of `ns.foo` replaced `foo`'s, so a member that -# depended on both reached `ns.foo` twice and its link left `foo`'s objects -# out (`undefined reference to foo::one`). Each package is now reached by the -# edge that names it. -set -e -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } -MCPP="${MCPP:-mcpp}" - -cd "$TMP" -mkdir -p foo/src nsfoo/src app/src -printf '[workspace]\nmembers = ["foo", "nsfoo", "app"]\n' > mcpp.toml -printf '[package]\nname = "foo"\nversion = "0.1.0"\n[targets.foo]\nkind = "lib"\n[build]\nsources = ["src/*.cppm", "src/*.cpp"]\n' > foo/mcpp.toml -printf '[package]\nnamespace = "ns"\nname = "foo"\nversion = "0.1.0"\n[targets.ns-foo]\nkind = "lib"\n[build]\nsources = ["src/*.cppm", "src/*.cpp"]\n' > nsfoo/mcpp.toml -printf '[package]\nname = "app"\nversion = "0.1.0"\n[targets.app]\nkind = "bin"\nmain = "src/main.cpp"\n[dependencies]\nfoo = { path = "../foo" }\nns.foo = { path = "../nsfoo" }\n' > app/mcpp.toml -printf 'export module foo;\nexport namespace foo { int one(); }\n' > foo/src/foo.cppm -printf 'module foo;\nnamespace foo { int one() { return 1; } }\n' > foo/src/foo.cpp -printf 'export module ns.foo;\nexport namespace ns::foo { int two(); }\n' > nsfoo/src/foo.cppm -printf 'module ns.foo;\nnamespace ns::foo { int two() { return 2; } }\n' > nsfoo/src/foo.cpp -printf 'import std;\nimport foo;\nimport ns.foo;\nint main() { std::println("sum {}", foo::one() + ns::foo::two()); }\n' > app/src/main.cpp - -"$MCPP" run -p app > run.log 2>&1 || fail "the member reaching foo and ns.foo did not link and run" run.log -grep -q "sum 3" run.log || fail "the program's output" run.log -echo "PASS: 1018 a package and a namespaced package of one name both link" diff --git a/tests/e2e/1027_a_configuration_group_keeps_the_other_groups_lock_entries.sh b/tests/e2e/1027_a_configuration_group_keeps_the_other_groups_lock_entries.sh deleted file mode 100755 index 36267beaf..000000000 --- a/tests/e2e/1027_a_configuration_group_keeps_the_other_groups_lock_entries.sh +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc fresh-sandbox -# 1027 -- a configuration group keeps the lock entries of the other groups -# (mcpp#903). -# -# A command over a workspace whose members take two configurations plans one -# group at a time, and each plan writes the lock. A plan updates the entries it -# resolves and keeps the others; "the others" were taken from the members the -# COMMAND asked for, so the group that resolves no entry of a package took the -# package from this configuration: `mcpp test --workspace` removed the native -# target from a dependency only the other group's member uses. -# -# Criteria: after a build of the whole workspace, the entry of the package one -# group uses keeps both the configuration it was recorded for and the one this -# build resolved it in. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } - -export MCPP_HOME="$TMP/mcpp-home" -source "$(dirname "$0")/_inherit_toolchain.sh" - -mkdir -p "$TMP/ws" -cd "$TMP/ws" - -mkdir -p local-index/pkgs/w .mcpp/.xlings/data/xpkgs/mcpplibs.widget3/1.0.0/src -cat > local-index/pkgs/w/widget3.lua <<'LUA' -package = { - spec = "1", - namespace = "mcpplibs", - name = "widget3", - description = "a package of the default namespace from a local index", - licenses = {"MIT"}, - type = "package", - xpm = { - linux = { ["1.0.0"] = { url = "https://example.invalid/w.tar.gz" } }, - macosx = { ["1.0.0"] = { url = "https://example.invalid/w.tar.gz" } }, - windows = { ["1.0.0"] = { url = "https://example.invalid/w.zip" } }, - }, - mcpp = { - language = "c++23", - import_std = false, - sources = { "src/widget3.cppm" }, - targets = { ["widget3"] = { kind = "lib" } }, - deps = {}, - }, -} -LUA -printf 'export module widget3;\nexport int widget3_value() { return 36; }\n' \ - > .mcpp/.xlings/data/xpkgs/mcpplibs.widget3/1.0.0/src/widget3.cppm - -cat > mcpp.toml <<'TOML' -[workspace] -members = ["a", "b"] - -[workspace.indices] -mcpplibs = { path = "local-index" } -TOML - -# Two configurations: `b` is another C++ standard (833 G4). -mkdir -p a/src b/src -cat > a/mcpp.toml <<'TOML' -[package] -name = "a" -version = "0.1.0" - -[dependencies] -mcpplibs.widget3 = "1.0.0" - -[targets.a] -kind = "bin" -main = "src/main.cpp" -TOML -printf 'import widget3;\nint main() { return widget3_value() == 36 ? 0 : 1; }\n' > a/src/main.cpp -cat > b/mcpp.toml <<'TOML' -[package] -name = "b" -version = "0.1.0" -standard = "c++26" - -[targets.b] -kind = "bin" -main = "src/main.cpp" -TOML -printf 'int main() { return 0; }\n' > b/src/main.cpp - -"$MCPP" build --workspace --configure-only > first.log 2>&1 || fail "the first configure failed" first.log -grep -q 'widget3' mcpp.lock || fail "the lock records no widget3 entry" mcpp.lock - -# The entry as another configuration recorded it as well. -python3 - <<'PY' -import re -text = open("mcpp.lock").read() -head = re.search(r'(\[package\."[^"]*widget3"\]\n(?:(?!\[)[^\n]*\n)*)', text) -block = head.group(1) -block2 = re.sub(r'^targets = .*\n', '', block, flags=re.M) + 'targets = ["x86_64-openkal"]\n' -open("mcpp.lock", "w").write(text.replace(block, block2)) -PY - -"$MCPP" build --workspace --configure-only > second.log 2>&1 || fail "the second configure failed" second.log -line=$(awk '/widget3"\]/{f=1} f && /^targets/{print; exit}' mcpp.lock) -[[ "$line" == *x86_64-openkal* ]] || fail "the other configuration's target was dropped: $line" mcpp.lock second.log -[[ "$line" == *,* ]] || fail "this configuration's target is missing: $line" mcpp.lock second.log -echo "ok: $line" - -echo "PASS: 1027 a configuration group keeps the other groups' lock entries" diff --git a/tests/e2e/1032_a_quoted_dotted_dependency_key_is_deprecated.sh b/tests/e2e/1032_a_quoted_dotted_dependency_key_is_deprecated.sh deleted file mode 100755 index 6d02a82e2..000000000 --- a/tests/e2e/1032_a_quoted_dotted_dependency_key_is_deprecated.sh +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env bash -# 1032 -- a quoted dotted dependency key is deprecated, and the warning names the unquoted key. -# -# `"acme.lib" = ...` is one TOML key whose name contains a dot; mcpp reads it as -# the selector `acme.lib`, as it did before, and said nothing, so a project -# never learned the recommended spelling (mcpp#904). It is now the deprecated -# reading `quoted-dotted-dependency-key` (docs/51). -# -# Criterion: the quoted key still resolves and builds; the build warns, naming -# the key, the releases and the unquoted spelling; `mcpp why compat` -# lists the use; the unquoted key builds with no such warning; a quoted key that is the only -# spelling of a package beside its parent (`acme.lib` and `acme.lib.extra` in one table) builds with -# no such warning either. -set -e -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } - -mkdir -p "$TMP/lib/src" "$TMP/app/src" -cat > "$TMP/lib/mcpp.toml" <<'TOML' -[package] -namespace = "acme" -name = "lib" -version = "0.1.0" -TOML -printf 'export module acme.lib;\nexport int answer() { return 42; }\n' > "$TMP/lib/src/lib.cppm" -printf 'import acme.lib;\nint main() { return answer() == 42 ? 0 : 1; }\n' > "$TMP/app/src/main.cpp" - -cd "$TMP/app" -printf '[package]\nname = "app"\nversion = "0.1.0"\n\n[dependencies]\n"acme.lib" = { path = "../lib" }\n' > mcpp.toml -"$MCPP" build > quoted.log 2>&1 || fail "the quoted key no longer builds" quoted.log -grep -q '"acme.lib" is deprecated since mcpp 2026.11.8.1 and removed from mcpp 2027.2' quoted.log \ - || fail "no deprecation warning for the quoted key" quoted.log -grep -q 'Write the key unquoted, `acme.lib = ...` instead' quoted.log || fail "the warning does not show the unquoted spelling of its own key" quoted.log -"$MCPP" why compat > why.log 2>&1 || true -grep -q 'quoted-dotted-dependency-key: \[dependencies\] "acme.lib"' why.log || fail "mcpp why compat does not list the use" why.log - -printf '[package]\nname = "app"\nversion = "0.1.0"\n\n[dependencies]\nacme.lib = { path = "../lib" }\n' > mcpp.toml -"$MCPP" build > unquoted.log 2>&1 || fail "the unquoted key does not build" unquoted.log -grep -q 'quoted-dotted-dependency-key\|is deprecated since' unquoted.log && fail "the unquoted key is warned about" unquoted.log -mkdir -p "$TMP/extra/src" -cat > "$TMP/extra/mcpp.toml" <<'TOML' -[package] -namespace = "acme.lib" -name = "extra" -version = "0.1.0" -TOML -printf 'export module acme.lib.extra;\nexport int more() { return 1; }\n' > "$TMP/extra/src/extra.cppm" -printf '[package]\nname = "app"\nversion = "0.1.0"\n\n[dependencies]\nacme.lib = { path = "../lib" }\n"acme.lib.extra" = { path = "../extra" }\n' > mcpp.toml -"$MCPP" build > pair.log 2>&1 || fail "a package beside its parent does not build" pair.log -grep -q 'quoted-dotted-dependency-key\|is deprecated since' pair.log && fail "the only spelling of the pair is warned about" pair.log -echo "PASS: 1032 a quoted dotted dependency key is deprecated" diff --git a/tests/e2e/1046_a_patch_made_with_git_diff_from_the_package_root_is_applied.sh b/tests/e2e/1046_a_patch_made_with_git_diff_from_the_package_root_is_applied.sh deleted file mode 100755 index 019f5a1b1..000000000 --- a/tests/e2e/1046_a_patch_made_with_git_diff_from_the_package_root_is_applied.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env bash -# requires: elf gcc -source "$(dirname "${BASH_SOURCE[0]}")/_toolchain_env.sh" -# 1046 -- a patch made with `git diff` from the package's root is applied (mcpp#911 G8; docs/05). -# -# A patch names files from the package's root, the directory of its resolved manifest, which -# `mcpp why deps` reports. A copy of that tree under git, edited, gives the patch with -# `git diff`; the edge's `patches` lists it, and the build reads the patched variant, whose root -# `mcpp why deps` then reports. -# -# Criteria: `mcpp why deps` reports the dependency's root; the patch git writes from a copy of it -# applies; the variant holds the change and is the root reported afterwards. -set -e -t=$(mktemp -d); trap 'rm -rf "$t"' EXIT -fail() { echo "FAIL: $1"; [ -n "${2:-}" ] && printf '%s\n' "$2" | tail -20; exit 1; } -mkdir -p "$t/app/src" -cd "$t/app" -cat > mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" - -[dependencies] -mcpplibs.cmdline = "0.0.1" -EOF -echo 'int main() { return 0; }' > src/main.cpp - -out=$("$MCPP" why deps 2>&1) || fail "why deps failed" "$out" -root=$(sed -n '/^ mcpplibs.cmdline[@+]/,/^ [^ ]/s/^ root: //p' <<<"$out" | head -1) -[ -n "$root" ] && [ -d "$root" ] || fail "the dependency's root is not reported" "$out" -echo " ok why deps reports the root" - -cp -R "$root" "$t/cmdline" -file=$(cd "$t/cmdline" && find . -name parse.cppm | head -1); file=${file#./} -[ -n "$file" ] || fail "the root holds no parse.cppm" "$(ls -R "$t/cmdline" | head)" -(cd "$t/cmdline" && git init -q && git add -A && git -c user.name=t -c user.email=t@t commit -qm base \ - && echo '// changed for the patch' >> "$file") -mkdir -p patches -(cd "$t/cmdline" && git diff) > patches/0001-change.patch -grep -q -- "--- a/$file" patches/0001-change.patch || fail "git did not name the file from the root" "$(head -3 patches/0001-change.patch)" -sed -i.bak 's|^mcpplibs.cmdline = "0.0.1"$|mcpplibs.cmdline = { version = "0.0.1", patches = ["patches/0001-change.patch"] }|' mcpp.toml - -out=$("$MCPP" build 2>&1) || fail "the patched build failed" "$out" -out=$("$MCPP" why deps 2>&1) || fail "why deps failed after the patch" "$out" -variant=$(sed -n '/^ mcpplibs.cmdline[@+]/,/^ [^ ]/s/^ root: //p' <<<"$out" | head -1) -[ -n "$variant" ] && [ "$variant" != "$root" ] || fail "the variant is not the reported root" "$out" -grep -q 'changed for the patch' "$variant/$file" || fail "the variant does not hold the change" -echo " ok the patch applies and the variant is the root the build reads" -echo "PASS: 1046 a patch made with git diff from the package root is applied" diff --git a/tests/e2e/1048_a_git_reference_follows_the_default_branch.sh b/tests/e2e/1048_a_git_reference_follows_the_default_branch.sh deleted file mode 100755 index 30319b0a5..000000000 --- a/tests/e2e/1048_a_git_reference_follows_the_default_branch.sh +++ /dev/null @@ -1,102 +0,0 @@ -#!/usr/bin/env bash -# requires: elf llvm -source "$(dirname "${BASH_SOURCE[0]}")/_toolchain_env.sh" -# 1048 -- a git reference with no tag, rev or branch follows the remote's default branch; a -# reference on the command line takes the same forms (SPEC-001 §5; SPEC-016 §8). -# -# The default branch is the one the remote announces (`git ls-remote --symref`), here `trunk`; the -# lock records it and its commit, and an offline build reads them. A dependency that names two of -# rev / tag / branch is refused. `--platform git+#tag=` selects a platform from a -# repository as a manifest's `{ git = ..., tag = ... }` does. -# -# Criteria: the build reports the default branch and runs; mcpp.lock names `#branch=trunk@`; -# an offline rebuild succeeds; two refs are refused; the platform from git builds its row. -set -e -t=$(mktemp -d); trap 'rm -rf "$t"' EXIT -fail() { echo "FAIL: $1"; [ -n "${2:-}" ] && printf '%s\n' "$2" | tail -20; exit 1; } -g() { git -c user.name=e2e -c user.email=e2e@example.invalid -c init.defaultBranch=trunk "$@"; } -mkdir -p "$t/lib/src" "$t/app/src" "$t/toy-platform" "$t/bare/src" -cat > "$t/lib/mcpp.toml" <<'EOF' -[package] -name = "gitlib" -version = "0.1.0" - -[targets.gitlib] -kind = "lib" -EOF -echo 'int gitlib_value() { return 9; }' > "$t/lib/src/lib.cpp" -(cd "$t/lib" && g init -q . && g add -A && g commit -qm init) -cat > "$t/app/mcpp.toml" < "$t/app/src/main.cpp" -cd "$t/app" - -out=$("$MCPP" build 2>&1) || fail "the build failed" "$out" -grep -q "the default branch of .* is trunk" <<<"$out" || fail "the default branch is not reported" "$out" -set +e; "$(find target -path '*/bin/app' -type f | head -1)"; rc=$?; set -e -[ "$rc" = 9 ] || fail "the program returned $rc, not 9" -grep -Eq 'source = "git\+.*#branch=trunk@[0-9a-f]{40}"' mcpp.lock || fail "the lock does not record the branch and its commit" "$(cat mcpp.lock)" -rm -rf target -out=$("$MCPP" build --offline 2>&1) || fail "the offline build did not read the lock" "$out" -echo " ok the default branch is followed, locked, and read offline" - -sed -i "s|gitlib = { git = \"$t/lib\" }|gitlib = { git = \"$t/lib\", tag = \"v1\", branch = \"trunk\" }|" mcpp.toml -out=$("$MCPP" build 2>&1 || true) -grep -q "names 2 of rev / tag / branch; a git dependency names at most one" <<<"$out" || fail "two refs were not refused" "$out" -echo " ok a dependency that names two refs is refused" - -cat > "$t/toy-platform/mcpp.toml" < "$t/bare/mcpp.toml" <<'EOF' -[package] -name = "bare" -version = "0.1.0" - -[build] -sources = ["src/*.c"] -ldflags = ["-nostdlib", "-static", "-no-pie"] - -[targets.bare] -kind = "bin" -main = "src/main.c" -EOF -printf 'void _start(void) { __asm__ volatile("mov $60, %%eax\\n mov $7, %%edi\\n syscall"); }\n' > "$t/bare/src/main.c" -cd "$t/bare" -out=$("$MCPP" build --target x86_64-toyos --platform "git+$t/toy-platform#tag=v1" 2>&1) || fail "the platform from git did not build its row" "$out" -grep -q "Platform toy-platform 0.1.0" <<<"$out" || fail "the platform is not reported" "$out" -echo " ok --platform git+#tag= selects the platform from its repository" -echo "PASS: 1048 a git reference follows the default branch" diff --git a/tests/e2e/1060_a_patched_member_of_a_repository_keeps_its_siblings.sh b/tests/e2e/1060_a_patched_member_of_a_repository_keeps_its_siblings.sh deleted file mode 100755 index e31b9c1d5..000000000 --- a/tests/e2e/1060_a_patched_member_of_a_repository_keeps_its_siblings.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc unix-shell -source "$(dirname "${BASH_SOURCE[0]}")/_toolchain_env.sh" -# 1060 -- a patched member of a git repository keeps its siblings (design 2026-11-10 E6; mcpp#911 -# N5; docs/05). -# -# A repository's root manifest lists members; one member reaches another by `path = "../core"`. -# Patching the first through a git dependency used to copy the member alone, so its `path` edge -# named a directory the copy did not have. The variant is now a copy of the repository, the patch -# names files from the member's root, and the sibling edge holds. -# -# Criteria: the build with the patch succeeds; the program prints the patched value (2) beside -# the sibling's (40). -set -e -t=$(mktemp -d); trap 'rm -rf "$t"' EXIT -fail() { echo "FAIL: $1"; [ -n "${2:-}" ] && printf '%s\n' "$2" | tail -20; exit 1; } -repo="$t/repo"; mkdir -p "$repo/core/src" "$repo/front/src" "$t/app/src" "$t/app/patches" -cat > "$repo/mcpp.toml" <<'TOML' -[workspace] -members = ["core", "front"] -TOML -cat > "$repo/core/mcpp.toml" <<'TOML' -[package] -name = "toy-core" -version = "0.1.0" - -[targets.toy-core] -kind = "lib" -TOML -echo 'int core_value(void) { return 40; }' > "$repo/core/src/core.c" -cat > "$repo/front/mcpp.toml" <<'TOML' -[package] -name = "toy-front" -version = "0.1.0" - -[dependencies] -toy-core = { path = "../core" } - -[targets.toy-front] -kind = "lib" -TOML -echo 'int front_value(void) { return 1; }' > "$repo/front/src/front.c" -(cd "$repo" && git init -q && git add -A && git -c user.name=t -c user.email=t@t commit -qm base && git tag v1) -# The patch, from the member's root: front_value returns 2. -cp -R "$repo/front" "$t/front-edit" -(cd "$t/front-edit" && rm -rf .git && git init -q && git add -A && git -c user.name=t -c user.email=t@t commit -qm b \ - && sed -i 's/return 1;/return 2;/' src/front.c && git diff) > "$t/app/patches/0001-front.patch" -grep -q -- '--- a/src/front.c' "$t/app/patches/0001-front.patch" || fail "git wrote no member-rooted patch" -cat > "$t/app/mcpp.toml" < "$t/app/src/main.c" <<'C' -#include -int front_value(void); int core_value(void); -int main(void) { printf("%d %d\n", front_value(), core_value()); return 0; } -C -cd "$t/app" -out=$("$MCPP" build 2>&1) || fail "the patched member did not build" "$out" -got=$("$(find target -path '*/bin/app' -type f | head -1)") -[ "$got" = "2 40" ] || fail "the program printed '$got', not '2 40'" -echo "PASS: 1060 a patched member of a repository keeps its siblings" diff --git a/tests/e2e/1066_a_merged_version_withdraws_its_requests.sh b/tests/e2e/1066_a_merged_version_withdraws_its_requests.sh deleted file mode 100755 index 7d60c6dc7..000000000 --- a/tests/e2e/1066_a_merged_version_withdraws_its_requests.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bash -# requires: -# 1066 — a version a merge replaces takes its requests out of the graph. -# -# The app asks for mcpplibs.os ^0.4.1, which resolves to the newest release -# (0.4.4, over the openkal 0.25 family); a path library pins os = 0.4.1, over -# the openkal 0.24 family. The merge takes os down to 0.4.1, and the requests -# 0.4.4 made -- openkal-linux 0.26.0, and through it openkal 0.25.0 -- leave -# with it (on Windows and macOS, the 0.25 family's implementation of that -# system). The library is named to be walked first, so that the pin is met -# before the newer release's own requests are. Before, they stayed: the graph -# was refused with "irreconcilable versions" between openkal 0.24.0 and a -# request of openkal-linux 0.26.0, a version no longer in it (the mtest -# workspace, measured). -set -e -source "$(dirname "$0")/_host_path.sh" - -TMP=$(mktemp -d) -TMP_HOST="$(host_path "$TMP")" -trap "rm -rf $TMP" EXIT - -mkdir -p "$TMP/anchor/src" "$TMP/app/src" -cat > "$TMP/anchor/mcpp.toml" <<'EOF' -[package] -name = "anchor" -version = "0.1.0" -[targets.anchor] -kind = "lib" - -[dependencies] -mcpplibs.os = "0.4.1" -EOF -cat > "$TMP/anchor/src/anchor.cppm" <<'EOF' -export module anchor; -export int anchor_answer() { return 1; } -EOF - -cat > "$TMP/app/mcpp.toml" < "$TMP/app/src/main.cpp" <<'EOF' -import anchor; -int main() { return anchor_answer() == 1 ? 0 : 1; } -EOF - -cd "$TMP/app" -"$MCPP" build --configure-only > plan.log 2>&1 || { - cat plan.log; echo "FAIL: the merged graph was refused"; exit 1; } -if grep -q 'irreconcilable' plan.log; then - cat plan.log; echo "FAIL: a request of a replaced version was kept"; exit 1 -fi -grep -qE 'Merged mcpplibs\.os 0\.4\.[2-9] ⨯ 0\.4\.1 → v0\.4\.1' plan.log || { - cat plan.log; echo "FAIL: os was not merged down to the pinned version"; exit 1; } -grep -A2 '^\[package\."openkal"\]' mcpp.lock | grep -q 'version = "0.24.0"' || { - cat mcpp.lock; echo "FAIL: openkal is not the 0.24 the pinned os asks for"; exit 1; } -# The system's implementation is the one of the 0.24 family, whichever system -# this is: none of the 0.25 family's (linux 0.26, macos 0.23, windows 0.21). -for member in 'openkal-linux" 0.26' 'openkal-macos" 0.23' 'openkal-windows" 0.21'; do - name=${member%% *}; series=${member##* } - if grep -A2 "^\[package\.\"$name\]" mcpp.lock | grep -q "version = \"$series\."; then - cat mcpp.lock; echo "FAIL: ${name%\"} $series of the replaced version is in the graph"; exit 1 - fi -done - -echo "PASS: 1066 a merged version withdraws its requests" diff --git a/tests/e2e/1071_the_root_overrides_a_dependency_for_the_whole_graph.sh b/tests/e2e/1071_the_root_overrides_a_dependency_for_the_whole_graph.sh deleted file mode 100755 index 7bfe20be7..000000000 --- a/tests/e2e/1071_the_root_overrides_a_dependency_for_the_whole_graph.sh +++ /dev/null @@ -1,92 +0,0 @@ -#!/usr/bin/env bash -# requires: python3 -# 1071 — the root overrides a dependency for the whole graph (design -# 2026-11-10 supplement, T2 and T3; docs/04 `override`). -# -# A. Two requests no merge reconciles (a library pins mcpplibs.os 0.4.1, the -# root asks for ^0.4.4) are refused, and the refusal names the line that -# chooses one version for the whole graph. -# B. With `override = true` on the root's declaration, every request takes -# the root's version; the replaced request is named once ("Overrode"), -# and mcpp.lock records the override. -# C. `--locked` reads the override as part of the recorded resolution: the -# same version reached without it is drift. -# D. A library's `override` is ignored, and said to be. -set -e -source "$(dirname "$0")/_host_path.sh" - -TMP=$(mktemp -d) -TMP_HOST="$(host_path "$TMP")" -trap 'rm -rf "$TMP"' EXIT -PY=python3; command -v python3 >/dev/null 2>&1 || PY=python -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f"; cat "$f"; done; exit 1; } - -mkdir -p "$TMP/anchor/src" "$TMP/app/src" -cat > "$TMP/anchor/mcpp.toml" <<'EOF' -[package] -name = "anchor" -version = "0.1.0" -[targets.anchor] -kind = "lib" - -[dependencies] -mcpplibs.os = "0.4.1" -EOF -cat > "$TMP/anchor/src/anchor.cppm" <<'EOF' -export module anchor; -export int anchor_answer() { return 1; } -EOF -write_app() { # write_app OS-DECLARATION - cat > "$TMP/app/mcpp.toml" < "$TMP/app/src/main.cpp" <<'EOF' -import anchor; -int main() { return anchor_answer() == 1 ? 0 : 1; } -EOF -locked_os() { $PY - <<'PY' -import re -text = open("mcpp.lock").read() -m = re.search(r'\[package\."mcpplibs\.os"\]([^\[]*)', text) -body = m.group(1) if m else "" -v = re.search(r'version\s*=\s*"([^"]+)"', body) -print((v.group(1) if v else "") + (" override" if re.search(r'^override\s*=\s*true', body, re.M) else "")) -PY -} -cd "$TMP/app" - -# A -write_app '"^0.4.4"' -"$MCPP" build --configure-only > a.log 2>&1 && fail "A: two requests no merge reconciles were accepted" a.log -grep -q 'irreconcilable versions' a.log || fail "A: the refusal does not name the conflict" a.log -grep -q 'mcpplibs.os = { version = "[0-9.]*", override = true }' a.log || fail "A: the refusal does not name the override" a.log - -# B -write_app '{ version = "0.4.4", override = true }' -"$MCPP" build --configure-only > b.log 2>&1 || fail "B: the override was refused" b.log -grep -qE 'Overrode +mcpplibs\.os 0\.4\.1 \(requested by [^)]*anchor[^)]*\) -> 0\.4\.4 \(\[dependencies\] of app\)' b.log \ - || fail "B: the replaced request is not named" b.log -[ "$(grep -c 'Overrode' b.log)" = 1 ] || fail "B: the replaced request is named more than once" b.log -[ "$(locked_os)" = "0.4.4 override" ] || fail "B: mcpp.lock does not record the override ($(locked_os))" mcpp.lock - -# C -write_app '{ version = "0.4.4", override = true }' -"$MCPP" build --configure-only > c2.log 2>&1 || fail "C: the override was refused" c2.log -sed -i.bak 's/^override = true$//' mcpp.lock -"$MCPP" build --configure-only --locked > c3.log 2>&1 && fail "C: --locked accepted a lock without the override" c3.log -grep -q 'mcpplibs.os 0.4.4 -> 0.4.4 (override)' c3.log || fail "C: the drift does not name the override" c3.log - -# D -sed -i.bak 's/^mcpplibs.os = "0.4.1"$/mcpplibs.os = { version = "0.4.1", override = true }/' "$TMP/anchor/mcpp.toml" -"$MCPP" build --configure-only > d.log 2>&1 || fail "D: the build failed" d.log -grep -q "\`override\` on 'mcpplibs.os' is ignored" d.log || fail "D: a library's override is not reported" d.log -sed -i.bak 's/^mcpplibs.os = { version = "0.4.1", override = true }$/mcpplibs.os = "0.4.1"/' "$TMP/anchor/mcpp.toml" - -echo "PASS: 1071 the root overrides a dependency for the whole graph" diff --git a/tests/e2e/1074_a_namespace_package_named_like_a_spec_field.sh b/tests/e2e/1074_a_namespace_package_named_like_a_spec_field.sh deleted file mode 100755 index cb6fcf126..000000000 --- a/tests/e2e/1074_a_namespace_package_named_like_a_spec_field.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash -# 1074 -- a namespace's package may be named like a dependency specification's -# field (#918; docs/06). -# -# `gpp.version = { path = "../version" }` is TOML for the table `gpp` holding the -# key `version`, whose value is a table: the package `version` in the namespace -# `gpp`. A specification's `path`, `version` and `git` are strings and its -# `workspace` a boolean, so only such a value makes a table one specification. -# -# Criteria: a project depending on `gpp.version` by path builds and runs it, and -# `mcpp emit build-database` describes it. -set -e -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; tail -30 "$f" 2>/dev/null; done; exit 1; } - -mkdir -p "$TMP/version/src" "$TMP/app/src" -cat > "$TMP/version/mcpp.toml" <<'T' -[package] -namespace = "gpp" -name = "version" -version = "0.1.0" -T -cat > "$TMP/version/src/version.cppm" <<'T' -export module gpp.version; -export int gpp_version() { return 7; } -T -cat > "$TMP/app/mcpp.toml" <<'T' -[package] -name = "app" -version = "0.1.0" - -[dependencies] -gpp.version = { path = "../version" } -T -cat > "$TMP/app/src/main.cpp" <<'T' -import gpp.version; -int main() { return gpp_version() == 7 ? 0 : 1; } -T - -cd "$TMP/app" -"$MCPP" run > run.log 2>&1 || fail "the package gpp.version was not read as a package" run.log -grep -q 'must specify' run.log && fail "the namespace was read as one specification" run.log -"$MCPP" emit build-database --format json > db.json 2> db.err || fail "the build database was not described" db.err -grep -q 'version.cppm' db.json || fail "the build database does not hold gpp.version" db.json -echo "OK" diff --git a/tests/e2e/12_add_command.sh b/tests/e2e/12_add_command.sh deleted file mode 100755 index 231347718..000000000 --- a/tests/e2e/12_add_command.sh +++ /dev/null @@ -1,295 +0,0 @@ -#!/usr/bin/env bash -# requires: -# `mcpp add` modifies mcpp.toml [dependencies]. Default-namespace packages land -# as bare keys under [dependencies]; canonical `ns.name` inputs use a subtable; -# the deprecated `ns:name` spelling is accepted only as a migration alias. A -# package no readable index can serve is -# refused before mcpp.toml is touched (#305) — but only where absence is -# provable, so the gate resolves the same candidates the manifest parser derives -# and reads through the same index routing `mcpp build` resolves dependencies -# with. -# -# Every package here is served by a project-local `[indices] path = …`, so the -# assertions depend on neither the network nor the state of the shared registry. -# MCPP_HOME is deliberately NOT isolated: an empty home would force a full -# sandbox bootstrap (xlings, index fetch, patchelf, ninja) on a test that is -# otherwise pure local file manipulation. -set -e -source "$(dirname "$0")/_host_path.sh" - -TMP=$(mktemp -d) -TMP_HOST="$(host_path "$TMP")" -trap "rm -rf $TMP" EXIT - -cd "$TMP" -"$MCPP" new myapp > /dev/null -cd myapp - -# A local index carrying five packages: two in the DEFAULT namespace, one under -# `acme`, and a same-short-name pair under `capi` / `mcpplibs.capi` that proves -# exact dotted selection cannot be stolen by the old prefixed candidate. -# Descriptors are filed under the first letter of their FILENAME, and publish -# every OS so the version assertions below hold on all three CI platforms. -mkdir -p index/pkgs/w index/pkgs/g index/pkgs/a index/pkgs/c index/pkgs/m -cat > index/pkgs/w/widget.lua <<'EOF' -package = { - spec = "1", - namespace = "mcpplibs", - name = "widget", - description = "default-namespace package served by a local index", - licenses = {"MIT"}, - type = "package", - xpm = { - linux = { ["0.1.0"] = { url = "https://example.invalid/w.tar.gz" } }, - macosx = { ["0.1.0"] = { url = "https://example.invalid/w.tar.gz" } }, - windows = { ["0.1.0"] = { url = "https://example.invalid/w.zip" } }, - }, -} -EOF -cat > index/pkgs/g/gadget.lua <<'EOF' -package = { - spec = "1", - namespace = "mcpplibs", - name = "gadget", - description = "second default-namespace package", - licenses = {"MIT"}, - type = "package", - xpm = { - linux = { ["0.2.0"] = { url = "https://example.invalid/g.tar.gz" } }, - macosx = { ["0.2.0"] = { url = "https://example.invalid/g.tar.gz" } }, - windows = { ["0.2.0"] = { url = "https://example.invalid/g.zip" } }, - }, -} -EOF -cat > index/pkgs/a/acme.util.lua <<'EOF' -package = { - spec = "1", - namespace = "acme", - name = "util", - description = "nested-namespace package served by a local index", - licenses = {"MIT"}, - type = "package", - xpm = { - linux = { ["2.0.0"] = { url = "https://example.invalid/u.tar.gz" } }, - macosx = { ["2.0.0"] = { url = "https://example.invalid/u.tar.gz" } }, - windows = { ["2.0.0"] = { url = "https://example.invalid/u.zip" } }, - }, -} -EOF -cat > index/pkgs/c/capi.lua.lua <<'EOF' -package = { - spec = "1", - namespace = "capi", - name = "lua", - description = "new exact dotted identity", - licenses = {"MIT"}, - type = "package", - xpm = { - linux = { ["5.4.7"] = { url = "https://example.invalid/lua.tar.gz" } }, - macosx = { ["5.4.7"] = { url = "https://example.invalid/lua.tar.gz" } }, - windows = { ["5.4.7"] = { url = "https://example.invalid/lua.zip" } }, - }, -} -EOF -cat > index/pkgs/m/mcpplibs.capi.lua.lua <<'EOF' -package = { - spec = "1", - namespace = "mcpplibs.capi", - name = "lua", - description = "old prefixed dotted candidate", - licenses = {"MIT"}, - type = "package", - xpm = { - linux = { ["9.9.9"] = { url = "https://example.invalid/old.tar.gz" } }, - macosx = { ["9.9.9"] = { url = "https://example.invalid/old.tar.gz" } }, - windows = { ["9.9.9"] = { url = "https://example.invalid/old.zip" } }, - }, -} -EOF -cat >> mcpp.toml <<'EOF' - -[indices] -default = { path = "index" } -acme = { path = "index" } -capi = { path = "index" } -EOF - -# (1) Default-namespace dep: bare name → unquoted key under [dependencies]. -"$MCPP" add widget@0.1.0 > /dev/null -grep -qE '^\[dependencies\]' mcpp.toml || { cat mcpp.toml; echo "no [dependencies] section"; exit 1; } -grep -qE '^widget = "0\.1\.0"$' mcpp.toml || { cat mcpp.toml; echo "widget entry missing or quoted"; exit 1; } -grep -qE '^"widget"' mcpp.toml && { cat mcpp.toml; echo "default-ns key should not be quoted"; exit 1; } - -# (2) `.` where ns IS the default (mcpplibs) — still a bare key under -# [dependencies], NOT [dependencies.mcpplibs]. Appends without duplicating the -# section header. -"$MCPP" add mcpplibs.gadget@0.2.0 > /dev/null -header_count=$(grep -cE '^\[dependencies\]$' mcpp.toml) -[[ "$header_count" == "1" ]] || { cat mcpp.toml; echo "[dependencies] header duplicated"; exit 1; } -grep -qE '^gadget = "0\.2\.0"$' mcpp.toml || { cat mcpp.toml; echo "gadget not set"; exit 1; } - -# (3) A dotted selector is one exact identity and is stored canonically as a -# namespace subtable. It must never become an ordered fallback search. -"$MCPP" add acme.util@2.0.0 > /dev/null -grep -qE '^\[dependencies\.acme\]$' mcpp.toml || { cat mcpp.toml; echo "missing [dependencies.acme] section"; exit 1; } -grep -qE '^util = "2\.0\.0"$' mcpp.toml || { cat mcpp.toml; echo "util entry missing"; exit 1; } -! grep -qE '^acme\.util = ' mcpp.toml || { cat mcpp.toml; echo "dotted selector must not be stored as an ambiguous flat key"; exit 1; } - -# (4) The old colon spelling remains a one-release migration alias, gives a -# copyable canonical replacement, and writes the same canonical shape. -alias_out=$("$MCPP" add acme:util@2.0.0 2>&1) -[[ "$alias_out" == *"deprecated"* ]] || { echo "missing migration warning: $alias_out"; exit 1; } -[[ "$alias_out" == *"acme.util@2.0.0"* ]] || { echo "warning lacks canonical replacement: $alias_out"; exit 1; } - -# (5) `capi.lua` is exactly (capi,lua), even though the old first candidate -# (mcpplibs.capi,lua) exists with the requested 9.9.9 version. The version -# warning must come from capi:lua and the migration warning shows both complete -# selectors without silently selecting the sibling. -exact_out=$("$MCPP" add capi.lua@9.9.9 2>&1) -[[ "$exact_out" == *"available: 5.4.7"* ]] || { echo "sibling stole exact selector: $exact_out"; exit 1; } -[[ "$exact_out" == *"mcpplibs.capi.lua"* ]] || { echo "migration warning lacks old selector: $exact_out"; exit 1; } -[[ "$exact_out" == *"capi.lua"* ]] || { echo "migration warning lacks new selector: $exact_out"; exit 1; } -"$MCPP" add capi.lua@5.4.7 > /dev/null -grep -qE '^\[dependencies\.capi\]$' mcpp.toml || { cat mcpp.toml; echo "missing [dependencies.capi] section"; exit 1; } -grep -qE '^lua = "5\.4\.7"$' mcpp.toml || { cat mcpp.toml; echo "exact capi.lua entry missing"; exit 1; } - -# Nested namespaces use the same parser for add and remove. -"$MCPP" add mcpplibs.capi.lua@9.9.9 > /dev/null -grep -qE '^\[dependencies\.mcpplibs\.capi\]$' mcpp.toml || { cat mcpp.toml; echo "nested namespace section missing"; exit 1; } -"$MCPP" remove mcpplibs.capi.lua > /dev/null -! grep -qE '^lua = "9\.9\.9"$' mcpp.toml || { cat mcpp.toml; echo "nested exact remove failed"; exit 1; } - -# (6) Dotted remove can still clean the old subtable shape for compatibility. -cat >> mcpp.toml <<'EOF' - -[dependencies.legacy] -old = "0.1.0" -EOF -"$MCPP" remove legacy.old > /dev/null -! grep -qE '^old = "0\.1\.0"$' mcpp.toml || { cat mcpp.toml; echo "legacy.old was not removed"; exit 1; } - -# (7) Reject missing version. -err=$("$MCPP" add bareword 2>&1) && { echo "expected error for missing version"; exit 1; } -[[ "$err" == *"version required"* ]] || { echo "wrong error: $err"; exit 1; } - -# (8) Reject empty package name (e.g. `mcpp add :@1.0`). -err=$("$MCPP" add ":@1.0" 2>&1) && { echo "expected error for empty package name"; exit 1; } - -# (9) Reject malformed/unsafe selectors before any index lookup or manifest -# mutation. These spellings must not be normalized into a different identity. -cp mcpp.toml "$TMP/before-invalid" -for invalid in 'ocornut..imgui@1.0.0' 'acme/util@1.0.0' 'acme\util@1.0.0' 'acme.util@' 'acme.util@1.0@2.0' 'widget@1.0"bad'; do - err=$("$MCPP" add "$invalid" 2>&1) && { echo "expected error for invalid selector: $invalid"; exit 1; } - [[ "$err" == *"invalid"* || "$err" == *"version required"* ]] || { echo "wrong error for $invalid: $err"; exit 1; } - diff -q "$TMP/before-invalid" mcpp.toml || { cat mcpp.toml; echo "mcpp.toml mutated for invalid selector: $invalid"; exit 1; } -done - -# (10) A package the index does not carry is refused, and mcpp.toml is left -# exactly as it was. The error names the identities that were tried. -cp mcpp.toml "$TMP/before" -err=$("$MCPP" add definitely-not-a-real-package@9.9.9 2>&1) && { echo "expected error for missing package"; exit 1; } -[[ "$err" == *"not found"* ]] || { echo "wrong error: $err"; exit 1; } -[[ "$err" == *"tried:"* ]] || { echo "error should list the identities tried: $err"; exit 1; } -diff -q "$TMP/before" mcpp.toml || { cat mcpp.toml; echo "mcpp.toml mutated for missing package"; exit 1; } - -# (11) Same for an explicitly-namespaced miss in a readable index. -err=$("$MCPP" add acme.nope@1.0.0 2>&1) && { echo "expected error for missing acme package"; exit 1; } -[[ "$err" == *"(acme, nope)"* ]] || { echo "wrong error: $err"; exit 1; } -[[ "$err" == *"route: local index 'acme': root present, pkgs present"* ]] || { - echo "missing privacy-safe local-index route state: $err" - exit 1 -} -diff -q "$TMP/before" mcpp.toml || { cat mcpp.toml; echo "mcpp.toml mutated for missing package"; exit 1; } - -# (12) A namespace no readable index covers cannot be refuted, so the add goes -# through unverified rather than failing. Refusing here would be worse than the -# bug being fixed — `mcpp build` resolves such dependencies fine. -"$MCPP" add unknownidx.thing@1.0.0 > /dev/null -grep -qE '^\[dependencies\.unknownidx\]$' mcpp.toml || { cat mcpp.toml; echo "unverifiable namespace should still be added"; exit 1; } - -# (13) A real package asked for at a version it does not publish is a warning, -# not a refusal — version tables are per-OS, so "absent here" is not "absent". -out=$("$MCPP" add widget@9.9.9 2>&1) -[[ "$out" == *"available: 0.1.0"* ]] || { echo "expected available-versions warning: $out"; exit 1; } -grep -qE '^widget = "9\.9\.9"$' mcpp.toml || { cat mcpp.toml; echo "widget@9.9.9 should still be written"; exit 1; } - -# (14) A workspace MEMBER inherits the root's [indices], so adding inside it -# sees the same packages `mcpp build` would resolve from there (#224). -mkdir -p "$TMP/ws" -cd "$TMP/ws" -cp -r "$TMP/myapp/index" index -[[ -f index/pkgs/a/acme.util.lua ]] || { - echo "workspace index fixture copy is incomplete" - exit 1 -} -cat > mcpp.toml <<'EOF' -[workspace] -members = ["m1"] - -[workspace.indices] -acme = { path = "index" } -EOF -"$MCPP" new m1 > /dev/null -cd m1 -workspace_add=$("$MCPP" add acme.util@2.0.0 2>&1) || { - echo "$workspace_add" - echo "workspace member could not read its root-owned local index" - exit 1 -} -grep -qE '^util = "2\.0\.0"$' mcpp.toml || { cat mcpp.toml; echo "member should inherit workspace [indices]"; exit 1; } -err=$("$MCPP" add acme.nope@1.0.0 2>&1) && { echo "expected error inside workspace member"; exit 1; } -[[ "$err" == *"(acme, nope)"* ]] || { echo "wrong error: $err"; exit 1; } - -# (15) Adding an exact selector upserts an equivalent legacy flat spelling -# instead of leaving two source forms for one PackageId. -mkdir -p "$TMP/upsert" -cat > "$TMP/upsert/mcpp.toml" < /dev/null -! grep -qE '^"acme\.util" = ' mcpp.toml || { - cat mcpp.toml - echo "legacy flat selector was duplicated instead of migrated" - exit 1 -} -[[ $(grep -cE '^util = "2\.0\.0"$' mcpp.toml) == 1 ]] || { - cat mcpp.toml - echo "canonical exact selector was not upserted once" - exit 1 -} - -# (16) Exact removal is scoped to [dependencies]. A same-name dev dependency -# that appears earlier in the file must not be removed by a global text match. -cat > mcpp.toml <<'EOF' -[package] -name = "remove-scope" -version = "0.1.0" - -[dev-dependencies] -widget = "0.2.0" - -[dependencies] -widget = "0.1.0" -EOF -"$MCPP" remove widget > /dev/null -grep -qE '^widget = "0\.2\.0"$' mcpp.toml || { - cat mcpp.toml - echo "remove escaped [dependencies] and deleted the dev dependency" - exit 1 -} -! grep -qE '^widget = "0\.1\.0"$' mcpp.toml || { - cat mcpp.toml - echo "regular dependency was not removed" - exit 1 -} - -echo "OK" diff --git a/tests/e2e/161_xpkg_name_form.sh b/tests/e2e/161_xpkg_name_form.sh deleted file mode 100755 index 549061aca..000000000 --- a/tests/e2e/161_xpkg_name_form.sh +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env bash -# requires: -# 161_xpkg_name_form.sh — SPEC-001 §3.2: `package.name` is a SINGLE ATOMIC -# SEGMENT; all hierarchy belongs in `package.namespace`. -# -# accepted namespace = "chriskohlhoff", name = "asio" -# accepted namespace = "mcpplibs.capi", name = "lua" -# accepted namespace = "compat", name = "compat.zlib" (legacy FQN, kept working) -# refused namespace = "mcpplibs", name = "capi.lua" (short name has a dot) -# -# Why the last one is rejected rather than reinterpreted: identity is the pair -# (namespace, name). A `name` carrying dots the namespace does not account for -# describes a package whose namespace nobody declared. mcpp used to split such a -# name on its LAST dot and silently invent `(mcpplibs.capi, lua)` — a namespace -# absent from the descriptor. It now refuses to guess. -# -# This test was the inverse before mcpp 0.0.106 (it asserted that the short form -# was the violation). See docs/specs/package-identity.md §3.2 and mcpp#278. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -mk() { # mk - cat > "$1" < /dev/null - -# ── 2. hierarchical namespace with an atomic short name ───────────── -mk nested.lua "mcpplibs.capi" "lua" -"$MCPP" xpkg parse nested.lua > /dev/null - -# ── 3. legacy fully-qualified spelling still accepted ─────────────── -# Descriptors written before SPEC-001 repeat the namespace inside `name`. -# They resolve to the same identity and stay installable, so they must not -# become errors — that would break every currently published package. -mk legacy.lua "compat" "compat.zlib" -"$MCPP" xpkg parse legacy.lua > /dev/null - -# ── 4. non-atomic short name is rejected, with the fix spelled out ── -mk bad.lua "mcpplibs" "capi.lua" -if "$MCPP" xpkg parse bad.lua > /dev/null 2>&1; then - echo "FAIL: a short name containing '.' must be rejected"; exit 1 -fi -"$MCPP" xpkg parse bad.lua 2>&1 | tee bad.err -grep -q "single atomic segment" bad.err -# The diagnostic must name BOTH corrected fields, not just complain. -grep -q 'mcpplibs.capi' bad.err -grep -q '"lua"' bad.err - -# ── 5. dotted name with no namespace at all is rejected ───────────── -# It would resolve into a namespace written down nowhere. -mk nons.lua "" "mcpplibs.capi.lua" -if "$MCPP" xpkg parse nons.lua > /dev/null 2>&1; then - echo "FAIL: dotted name without a namespace must be rejected"; exit 1 -fi - -# ── 6. --json carries the violation machine-readably (index CI) ───── -if "$MCPP" xpkg parse --json bad.lua > bad.json 2>/dev/null; then - echo "FAIL: --json must still exit non-zero on violation"; exit 1 -fi -python3 - <<'PY' -import json -j = json.load(open("bad.json")) -assert "error" in j, j -assert "atomic" in j["error"], j -PY - -# ── 7. runtime fail-fast: a descriptor that skipped index CI ──────── -# A `[indices]` path index bypasses lint entirely, so the same predicate has -# to guard the install path — and must fire BEFORE any download. -mkdir -p idx/pkgs/d -cat > idx/pkgs/d/thing.lua <<'EOF' -package = { - spec = "1", namespace = "demo", name = "sub.thing", - xpm = { linux = { ["1.0.0"] = { url = "u", sha256 = "h" } }, - macosx = { ["1.0.0"] = { url = "u", sha256 = "h" } }, - windows = { ["1.0.0"] = { url = "u", sha256 = "h" } } }, - mcpp = { schema = "0.1", sources = { "*.cpp" } }, -} -EOF - -mkdir -p app/src && cd app -cat > mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" - -[indices] -demo = { path = "../idx" } - -[dependencies.demo] -"sub.thing" = "1.0.0" -EOF -echo 'int main() { return 0; }' > src/main.cpp - -if "$MCPP" build > build.out 2>&1; then - echo "FAIL: non-atomic short name must not build"; cat build.out; exit 1 -fi -grep -q "single atomic segment" build.out || { cat build.out; exit 1; } -if grep -q "Downloading" build.out; then - echo "FAIL: must fail before any download"; cat build.out; exit 1 -fi - -echo "PASS 161_xpkg_name_form" diff --git a/tests/e2e/162_bare_name_namespace_scope.sh b/tests/e2e/162_bare_name_namespace_scope.sh deleted file mode 100755 index 7bd3126fe..000000000 --- a/tests/e2e/162_bare_name_namespace_scope.sh +++ /dev/null @@ -1,183 +0,0 @@ -#!/usr/bin/env bash -# requires: -# 162_bare_name_namespace_scope.sh — INV-RESOLVE (#278): a bare dependency name -# resolves in exactly one place (mcpplibs); every other namespace must be -# written out. -# -# Before this, a bare name that matched nothing fell through to the FIRST -# candidate SILENTLY, so mcpp carried on with a namespace it had invented and -# the user met the failure much later, wrapped around that invented name. And -# the discovery rung `(no namespace, X)` accepted a descriptor from ANY -# namespace, which made resolution depend on which indices happened to be -# present — adding an index could silently retarget an existing dependency. -# -# Covered: the hard failure, the did-you-mean hint (a DIAGNOSTIC-only index -# scan that must never feed resolution), and that both documented spellings -# resolve the very same package the hint names. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -# A path index owning namespaces `acme` and `compat`. Note the acme descriptor -# is FQN-correct — this test is about the CONSUMER's spelling, not the -# descriptor's. -mkdir -p idx/pkgs/a idx/pkgs/c -cat > idx/pkgs/a/acme.widget.lua <<'EOF' -package = { - spec = "1", namespace = "acme", name = "acme.widget", - xpm = { linux = { ["1.0.0"] = { url = "u", sha256 = "h" } }, - macosx = { ["1.0.0"] = { url = "u", sha256 = "h" } }, - windows = { ["1.0.0"] = { url = "u", sha256 = "h" } } }, - mcpp = { schema = "0.1", sources = { "*.cpp" } }, -} -EOF -# The compat wrapper shape every published `compat.*` package uses. Served from -# the fixture rather than the shared registry so section 5 asserts the same -# property offline that it used to assert against whatever gtest happened to be -# installed. -cat > idx/pkgs/c/compat.gadget.lua <<'EOF' -package = { - spec = "1", namespace = "compat", name = "gadget", - xpm = { linux = { ["2.0.0"] = { url = "u", sha256 = "h" } }, - macosx = { ["2.0.0"] = { url = "u", sha256 = "h" } }, - windows = { ["2.0.0"] = { url = "u", sha256 = "h" } } }, - mcpp = { schema = "0.1", sources = { "*.cpp" } }, -} -EOF - -mkdir -p app/src && cd app -echo 'int main() { return 0; }' > src/main.cpp - -write_manifest() { - cat > mcpp.toml < bare.out 2>&1; then - echo "FAIL: a bare name must not reach the acme namespace"; cat bare.out; exit 1 -fi -grep -q "no package found for exact selector" bare.out || { - echo "FAIL: expected the explicit not-found error"; cat bare.out; exit 1; } -# The identities actually attempted must be listed — the old silent fallback -# reported a namespace the user never wrote. -grep -q "tried:" bare.out || { cat bare.out; exit 1; } - -# ── 2. did-you-mean names the real package and both spellings ─────── -grep -q "acme.widget" bare.out || { - echo "FAIL: expected did-you-mean to name acme.widget"; cat bare.out; exit 1; } -# #487: the hint must also carry what the suggested package publishes — -# the fixture's xpm table lists exactly one version per OS. -grep -q "acme.widget (1.0.0)" bare.out || { - echo "FAIL: expected did-you-mean to show acme.widget's version"; cat bare.out; exit 1; } -grep -q "\[dependencies.acme\]" bare.out || { - echo "FAIL: expected the sub-table spelling in the hint"; cat bare.out; exit 1; } - -# ── 3. the dotted spelling the hint suggests resolves ─────────────── -write_manifest '[dependencies] -"acme.widget" = "1.0.0"' -"$MCPP" build > dotted.out 2>&1 || true -# The asset URL is a sentinel, so the fetch fails — but resolution must have -# gotten far enough to ADDRESS the package, which is what this asserts. -if grep -q "no package found for exact selector" dotted.out; then - echo "FAIL: dotted selector must resolve to (acme, widget)"; cat dotted.out; exit 1 -fi - -# ── 4. the sub-table spelling resolves identically ────────────────── -write_manifest '[dependencies.acme] -widget = "1.0.0"' -"$MCPP" build > subtable.out 2>&1 || true -if grep -q "no package found for exact selector" subtable.out; then - echo "FAIL: sub-table form must resolve to (acme, widget)"; cat subtable.out; exit 1 -fi - -# ── 5. namespace omission means mcpplibs, with a one-release exit ramp ── -# -# Every published `compat.*` package and every manifest written before exact -# identity spells its dependency bare. Turning that into an immediate hard -# error would make an mcpp upgrade break builds against data that is already -# published and can no longer be edited — the mirror image of the rule that -# keeps a raised index floor from bricking older clients. -# -# So a bare name still REACHES compat for one release, but it can no longer do -# so quietly: that silence, not the reach, was the #278 defect. The hit is -# announced, it names the exact replacement, and the canonical identity is what -# travels downstream. -# -# Asserted on RESOLUTION, not on a successful build: whether the sentinel asset -# downloads is irrelevant to whether the bare name addressed the package. -write_manifest '[dependencies] -gadget = "2.0.0"' -"$MCPP" build > compat.out 2>&1 || true -if grep -q "no package found for exact selector" compat.out; then - echo "FAIL: bare gadget must still reach compat during the migration window" - cat compat.out; exit 1 -fi -grep -q "deprecated bare-name search" compat.out || { - echo "FAIL: the bare-name fallback must announce itself"; cat compat.out; exit 1; } -grep -q "compat.gadget" compat.out || { - echo "FAIL: the warning must name the exact replacement"; cat compat.out; exit 1; } -grep -q "\[dependencies.compat\]" compat.out || { - echo "FAIL: the warning must show the manifest edit"; cat compat.out; exit 1; } - -# The explicit spelling resolves the same package and says nothing: a user who -# has already migrated must not be nagged. -write_manifest '[dependencies.compat] -gadget = "2.0.0"' -"$MCPP" build > compat-explicit.out 2>&1 || true -if grep -q "no package found for exact selector" compat-explicit.out; then - echo "FAIL: explicit compat.gadget must resolve"; cat compat-explicit.out; exit 1 -fi -if grep -q "deprecated bare-name search" compat-explicit.out; then - echo "FAIL: an already-exact selector must not warn" - cat compat-explicit.out; exit 1 -fi - -# ── 6. the exit ramp is not a wildcard ────────────────────────────── -# The fallback reaches `compat` and the namespace-less rung. It must NOT reach -# a third-party namespace — section 1 already proved bare `widget` fails, and -# it must keep failing now that a fallback exists at all. Stating the exact -# namespace is likewise not eligible: it is an identity, not an omission. -write_manifest '[dependencies.mcpplibs] -gadget = "2.0.0"' -if "$MCPP" build > explicit-default.out 2>&1; then - echo "FAIL: mcpplibs.gadget states an identity and must not reach compat" - cat explicit-default.out; exit 1 -fi -grep -q "no package found for exact selector" explicit-default.out || { - echo "FAIL: expected the explicit not-found error" - cat explicit-default.out; exit 1; } - -# ── 7. `mcpp add` did-you-mean carries versions too (#487) ────────── -# The same suggestion text serves the build path (sections 1-2) and the add -# path. Requesting a gadget under `acme` — a readable index that does not -# serve it — must fail AND point at compat.gadget with its published version, -# straight from the fixture's xpm table. -write_manifest '' -if MCPP_OFFLINE=1 "$MCPP" add acme.gadget@9.9.9 > add-hint.out 2>&1; then - echo "FAIL: adding a package the acme index does not serve must fail" - cat add-hint.out; exit 1 -fi -grep -q "compat.gadget (2.0.0)" add-hint.out || { - echo "FAIL: add hint must name compat.gadget with its published version" - cat add-hint.out; exit 1; } -# And the manifest must NOT have been mutated by the failed add. -if grep -q "gadget" mcpp.toml; then - echo "FAIL: a rejected add must not write the dependency"; cat mcpp.toml; exit 1 -fi - -echo "PASS 162_bare_name_namespace_scope" diff --git a/tests/e2e/163_identity_first_resolution.sh b/tests/e2e/163_identity_first_resolution.sh deleted file mode 100755 index e1be88561..000000000 --- a/tests/e2e/163_identity_first_resolution.sh +++ /dev/null @@ -1,150 +0,0 @@ -#!/usr/bin/env bash -# requires: -# 163_identity_first_resolution.sh — SPEC-001 §3.4 / §6: a package is found by -# its DECLARED identity `(namespace, name)`, and addressed on the wire by the -# LITERAL `package.name`. -# -# Three properties, none of which held before mcpp 0.0.106: -# -# 1. A short-name descriptor (`namespace="acme", name="widget"`) installs. -# Previously mcpp re-derived the wire name as `.` and asked the -# index for `acme.widget`, which it is not keyed by → E_NOT_FOUND (#278). -# 2. The filename is only a hint. A descriptor called anything, in any letter -# directory, is still discovered by its declared identity. Previously -# discovery probed a fixed candidate-filename list, so such a file was -# invisible even though the identity gate would have accepted it. -# 3. Two packages sharing a short name under different namespaces coexist in -# ONE index and are each addressable. This needs xlings >= 0.4.69 -# (openxlings/xlings#381); mcpp's part is sending `:` -# rather than a reconstructed string. -# -# The legacy fully-qualified spelling must keep working throughout — every -# currently published descriptor uses it. -set -e -source "$(dirname "$0")/_host_path.sh" - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -# A real, downloadable payload so this exercises the whole install path rather -# than stopping at resolution. Reuse whatever asio the registry already has. -SRC="" -for c in "$MCPP_HOME/indices/mcpplibs/pkgs/c/chriskohlhoff.asio.lua"; do - [ -f "$c" ] && SRC="$c" && break -done -if [ -z "$SRC" ]; then - echo "SKIP 163_identity_first_resolution (no asio descriptor in registry)" - exit 77 -fi - -mkidx() { # mkidx - mkdir -p "$1/pkgs/$(dirname "$2")" - # Rewrite the identity fields by SHAPE, never by their current values. The - # first version of this matched the literal `name = "chriskohlhoff.asio"`, - # which silently stopped matching the day mcpp-index migrated that - # descriptor to its SPEC-001 short name. The fixture then carried the WRONG - # identity and this test failed on three platforms for a reason that had - # nothing to do with what it asserts. - sed -e "s/^\( *namespace *= *\)\"[^\"]*\"/\1\"$3\"/" \ - -e "s/^\( *name *= *\)\"[^\"]*\"/\1\"$4\"/" \ - "$SRC" > "$1/pkgs/$2" - # Fail loudly rather than go on to test a fixture that isn't what we asked - # for — a missed rewrite must never masquerade as a resolution bug. - grep -qE "^ *namespace *= *\"$3\"" "$1/pkgs/$2" || { - echo "FAIL: fixture namespace rewrite missed (upstream descriptor shape changed?)" - exit 1; } - grep -qE "^ *name *= *\"$4\"" "$1/pkgs/$2" || { - echo "FAIL: fixture name rewrite missed (upstream descriptor shape changed?)" - exit 1; } -} - -mkapp() { # mkapp - mkdir -p "$1/src" - local index_host - index_host="$(host_path "$3")" - cat > "$1/mcpp.toml" < "$1/src/main.cpp" -} - -# ── 1. short-name descriptor installs ─────────────────────────────── -mkidx idx1 a/acme.widget.lua acme widget -mkapp app1 acme ../idx1 acme widget -(cd app1 && "$MCPP" build -v > out.txt 2>&1) || { cat app1/out.txt; exit 1; } -# Either verb: what this test is about is that the descriptor was found BY -# IDENTITY and the package became part of the build. Whether its objects were -# compiled here or served from the global build cache is a different subsystem's -# business — and now that the cache actually works, a sibling app dir under the -# same MCPP_HOME (or a restored CI sandbox) can legitimately supply them. -grep -qE "(Compiling|Cached) acme\.widget" app1/out.txt || { - cat app1/out.txt - echo "FAIL: acme.widget was neither compiled nor served from cache" - exit 1 -} -# xlings names the dir {namespace}-x-{literal name} — short name → acme-x-widget -test -d "app1/.mcpp/.xlings/data/xpkgs/acme-x-widget" \ - || { echo "FAIL: expected store dir acme-x-widget"; ls -R app1/.mcpp/.xlings/data/xpkgs 2>/dev/null; exit 1; } - -# ── 2. legacy fully-qualified spelling still installs ─────────────── -mkidx idx2 a/acme.widget.lua acme acme.widget -mkapp app2 acme ../idx2 acme widget -(cd app2 && "$MCPP" build > out.txt 2>&1) || { cat app2/out.txt; exit 1; } -# ...and lands under the dir its LITERAL name implies -test -d "app2/.mcpp/.xlings/data/xpkgs/acme-x-acme.widget" \ - || { echo "FAIL: expected store dir acme-x-acme.widget"; exit 1; } - -# ── 3. filename is only a hint ────────────────────────────────────── -# Arbitrary basename, and deliberately the "wrong" letter directory. -mkidx idx3 z/totally-unrelated-name.lua acme widget -mkapp app3 acme ../idx3 acme widget -(cd app3 && "$MCPP" build > out.txt 2>&1) || { - echo "FAIL: descriptor must be discoverable by identity, not filename" - cat app3/out.txt; exit 1; } - -# ── 4. two namespaces, one short name, ONE index ──────────────────── -# Needs xlings >= 0.4.69 (openxlings/xlings#381): before it, one index repo -# keyed its table by the bare `package.name`, so the second package was -# unreachable even with an explicit namespace. -# -# The sandbox xlings comes from bootstrapping the PREVIOUS mcpp release, so -# until 0.0.106 ships (bundling 0.4.69) CI still runs 0.4.68 here. Skip this -# sub-case rather than assert a capability the sandbox cannot have yet — -# properties 1-3 above are the ones this change is responsible for. -XLBIN="${MCPP_HOME:-$HOME/.mcpp}/registry/bin/xlings" -XLVER="" -[ -x "$XLBIN" ] && XLVER=$("$XLBIN" --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1) -xl_ge_0469() { - [ -n "$XLVER" ] || return 1 - printf '%s\n0.4.69\n' "$XLVER" | sort -V | head -1 | grep -qx "0.4.69" -} -if ! xl_ge_0469; then - echo "PASS 163_identity_first_resolution (step 4 skipped: sandbox xlings ${XLVER:-unknown} < 0.4.69)" - exit 0 -fi - -mkidx idx4 a/alpha.widget.lua alpha widget -mkidx idx4 b/beta.widget.lua beta widget - -mkapp app4a alpha ../idx4 alpha widget -(cd app4a && "$MCPP" build > out.txt 2>&1) || { cat app4a/out.txt; exit 1; } -test -d "app4a/.mcpp/.xlings/data/xpkgs/alpha-x-widget" \ - || { echo "FAIL: alpha:widget should land in alpha-x-widget"; exit 1; } - -mkapp app4b beta ../idx4 beta widget -(cd app4b && "$MCPP" build > out.txt 2>&1) || { - echo "FAIL: beta:widget unreachable — needs xlings >= 0.4.69 (#381)" - cat app4b/out.txt; exit 1; } -test -d "app4b/.mcpp/.xlings/data/xpkgs/beta-x-widget" \ - || { echo "FAIL: beta:widget should land in beta-x-widget"; exit 1; } - -echo "PASS 163_identity_first_resolution" diff --git a/tests/e2e/165_bare_name_cross_namespace_wire_address.sh b/tests/e2e/165_bare_name_cross_namespace_wire_address.sh deleted file mode 100755 index 19e7cf16f..000000000 --- a/tests/e2e/165_bare_name_cross_namespace_wire_address.sh +++ /dev/null @@ -1,114 +0,0 @@ -#!/usr/bin/env bash -# requires: -# 165_bare_name_cross_namespace_wire_address.sh — SPEC-001 §6: the descriptor -# the identity gate accepted supplies BOTH halves of the wire address. -# -# THE INCIDENT THIS LOCKS DOWN (2026-07-25, 6 of 7 workflows red on main): -# -# An explicitly selected compat dependency must carry the descriptor's -# namespace into the xlings wire address. The historical bug mixed the request -# and descriptor halves and emitted `mcpplibs:gtest`. -# -# It passed for months by coincidence: the literal `package.name` used to read -# `compat.gtest`, so the hardcoded `compat.` retry caught it. When -# mcpp-index migrated to SPEC-001 short names (`name = "gtest"`), that -# coincidence vanished and every bare-name dependency served by a compat -# descriptor became uninstallable — 34 of the index's 48 packages. -# -# WHY THIS TEST IS HERMETIC. The index side validated that migration against -# its own examples, all of which spell the dependency `[dependencies.compat]` -# — the one form that was never broken. Verification matrix and consumption -# matrix were not the same shape, so 8 green checks meant nothing. This test -# builds its own index so it cannot drift with the live one, and it asserts on -# the address mcpp SENDS, not just on whether the install happened to work. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -# Reuse a real, downloadable descriptor so this exercises the whole install -# path rather than stopping at resolution. -SRC="$MCPP_HOME/indices/mcpplibs/pkgs/c/chriskohlhoff.asio.lua" -if [ ! -f "$SRC" ]; then - echo "SKIP 165_bare_name_cross_namespace_wire_address (no asio descriptor in registry)" - exit 77 -fi - -# A `compat`-namespaced package named by its SPEC-001 short name. The -# `compat..lua` filename mirrors the live index, but selection is by the -# descriptor's exact declared identity, never by a bare-name fallback. -mkdir -p idx/pkgs/c -sed -e 's/^\( *namespace *= *\)"[^"]*"/\1"compat"/' \ - -e 's/^\( *name *= *\)"[^"]*"/\1"widget"/' \ - "$SRC" > idx/pkgs/c/compat.widget.lua -grep -qE '^ *namespace *= *"compat"' idx/pkgs/c/compat.widget.lua || { - echo "FAIL: fixture namespace rewrite missed (upstream descriptor shape changed?)"; exit 1; } -grep -qE '^ *name *= *"widget"' idx/pkgs/c/compat.widget.lua || { - echo "FAIL: fixture name rewrite missed (upstream descriptor shape changed?)"; exit 1; } - -# The consumer writes the compat namespace explicitly and routes it to the -# fixture index, so the whole thing stays offline. -mkdir -p app/src -cat > app/mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" - -[indices] -compat = { path = "../idx" } - -[dependencies.compat] -widget = "1.38.1" -EOF -echo 'int main() { return 0; }' > app/src/main.cpp - -set +e -(cd app && MCPP_VERBOSE=1 "$MCPP" build > out.txt 2>&1) -build_rc=$? -set -e - -# ── The assertion that matters: which address went on the wire ────── -# -# Checked independently of whether the install then succeeded, so this keeps -# reporting the real defect even when the network or the sandbox xlings is the -# thing that failed. -# -# Read the `"targets":[...]` payload specifically rather than grepping the log -# as a whole — the human-facing "Downloading …" and error lines carry the -# resolved identity `mcpplibs.widget`, which is correct there and would make a -# looser assertion answer the wrong question. -# Un-escape first: the interface command is single-quoted on unix -# (--args '{"targets":[...]}') but double-quoted with backslash-escaped quotes -# on Windows (--args "{\"targets\":[...]}"). Normalising makes one pattern -# work on both instead of silently matching nothing on one of them. -sed 's/\\"/"/g' app/out.txt | grep -oE '"targets":\["[^"]*"\]' > targets.txt || true -test -s targets.txt || { - echo "FAIL: no install target was emitted at all"; cat app/out.txt; exit 1; } -if grep -q '"mcpplibs:widget@1\.38\.1"' targets.txt; then - echo "FAIL: wire address took its namespace from the REQUEST (mcpplibs:widget)" - echo " The descriptor declares namespace=\"compat\"; the address must be compat:widget." - cat targets.txt - exit 1 -fi -grep -q '"compat:widget@1\.38\.1"' targets.txt || { - echo "FAIL: expected the wire address compat:widget@1.38.1" - cat targets.txt - exit 1; } - -# ── And that the package actually installs under that identity ────── -if [ $build_rc -ne 0 ]; then - echo "FAIL: build failed even though the wire address was correct" - cat app/out.txt - exit 1 -fi - -# xlings names an xpkg directory {namespace}-x-{literal name}. Search for it by -# NAME rather than at a fixed path — the store root differs per platform, and -# the directory's name is the whole point of the assertion. -find app/.mcpp -type d -name "compat-x-widget" 2>/dev/null | grep -q . || { - echo "FAIL: expected a store dir named compat-x-widget" - find app/.mcpp -type d -name "*-x-*" 2>/dev/null | head -10 - exit 1; } - -echo "PASS 165_bare_name_cross_namespace_wire_address" diff --git a/tests/e2e/196_version_identity_and_lock.sh b/tests/e2e/196_version_identity_and_lock.sh deleted file mode 100755 index cf1790127..000000000 --- a/tests/e2e/196_version_identity_and_lock.sh +++ /dev/null @@ -1,231 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc fresh-sandbox -# mcpp#363 — a resolved version must be an index KEY, and mcpp.lock must record -# what was resolved. -# -# Before this, pm/resolver.cppm parsed the index's literal version keys, threw -# them away, and re-rendered an address from the parsed numbers. Anything the -# renderer could not reproduce became an address that does not exist: -# -# 1.92.8-docking pre-release, truncated to 1.92.8 (and so indistinguishable -# from the non-docking release — a different tarball) -# 25.0.4.7.1 five segments, truncated to 25.0.4.7 -# b10069 not a number at all: skipped, then reported as -# "no valid versions in index" -# -# Every shape below is one the real xim-pkgindex publishes today (compat.imgui, -# jdk-corretto, jdk-temurin, khistory). The index here is a local path index so -# nothing is downloaded; the payload for the version that must win is pre-seeded, -# and the others deliberately have none — a build that resolves the wrong key -# fails to install, which is itself an assertion. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT - -export MCPP_HOME="$TMP/mcpp-home" -source "$(dirname "$0")/_inherit_toolchain.sh" - -mkdir -p "$TMP/proj/src" "$TMP/proj/local-index/pkgs/a" -cd "$TMP/proj" - -# ── One descriptor per upstream shape ───────────────────────────────────── -mk_pkg() { # $1 = short name, $2 = version-table body, $3 = module name (default gadget) - MOD="${3:-gadget}" - cat > "local-index/pkgs/a/acme.$1.lua" < ".mcpp/.xlings/data/xpkgs/acme.$1/$2/src/$MOD.cppm" -} -seed im 1.92.8 -seed jdk 25.0.4.7.1 -seed dv 1.0.0 devkit - -printf 'import gadget;\nint main(){ return gadget_value() == 42 ? 0 : 1; }\n' > src/main.cpp - -use_dep() { # $1 = short name, $2 = constraint - cat > mcpp.toml </dev/null; done; exit 1; } - -# ── 1. A range must not reach into a pre-release ────────────────────────── -use_dep im '^1.92.8' -"$MCPP" build > b1.log 2>&1 || fail "^1.92.8 did not build" b1.log -grep -q '^\s*Resolved acme.im .* → v1\.92\.8$' b1.log \ - || fail "^1.92.8 must resolve to 1.92.8, never the -docking branch" b1.log - -# ── 2. ...but naming it exactly still addresses it ──────────────────────── -# Only 1.92.8 is seeded, so this must fail AT INSTALL with the docking address — -# proving the wire address carried the literal key rather than a truncation. -use_dep im '1.92.8-docking' -"$MCPP" build > b2.log 2>&1 && fail "expected the unseeded -docking payload to fail" b2.log -grep -q '1\.92\.8-docking' b2.log \ - || fail "exact pre-release must be addressed literally, not truncated" b2.log - -# ── 3. Five segments survive, and an alias is not a candidate ───────────── -# `^25.0` used to render 25.0.4.7 (a key that does not exist) or pick the alias. -use_dep jdk '^25.0' -"$MCPP" build > b3.log 2>&1 || fail "^25.0 did not build" b3.log -grep -q '^\s*Resolved acme.jdk .* → v25\.0\.4\.7\.1$' b3.log \ - || fail "^25.0 must resolve to the real five-segment key, not an alias or a truncation" b3.log - -# ── 4. An unorderable key is named, not blamed on the index ─────────────── -use_dep kh '*' -"$MCPP" build > b4.log 2>&1 && fail "a range over unorderable keys must not succeed" b4.log -grep -q 'not ordered versions' b4.log \ - || fail "expected an error naming the unorderable keys" b4.log -grep -q 'acme.kh = "pre-v0.0.5"' b4.log \ - || fail "the error must show the exact pin that works" b4.log -grep -q 'no valid versions in index' b4.log \ - && fail "the old message blames the index for a package it publishes fine" b4.log - -# ...and pinning it exactly is accepted (fails later, at install, for want of a -# payload — which is proof that resolution let it through). -use_dep kh 'pre-v0.0.5' -"$MCPP" build > b5.log 2>&1 && fail "expected the unseeded payload to fail" b5.log -grep -q 'not ordered versions' b5.log \ - && fail "an exact unorderable key must resolve, not be rejected" b5.log - -# The `=` form must work too — it is exactly what the error above tells the user -# to write, and it is what try_merge_semver produces internally from a literal -# pin. Routing it through the SemVer grammar would reject the remedy. -use_dep kh '=pre-v0.0.5' -"$MCPP" build > b5b.log 2>&1 && fail "expected the unseeded payload to fail" b5b.log -grep -qE 'invalid version constraint|not ordered versions' b5b.log \ - && fail "'=' must resolve to that key" b5b.log - -# ── 5. A genuine precedence tie is refused, not guessed ─────────────────── -use_dep tie '^1.0' -"$MCPP" build > b6.log 2>&1 && fail "a tie must not silently pick one" b6.log -grep -q 'compare EQUAL' b6.log || fail "expected the tie to be named" b6.log -grep -q '1\.0\.0+a' b6.log && grep -q '1\.0\.0+b' b6.log \ - || fail "both tied keys must be listed" b6.log - -# ...while naming one exactly is not ambiguous at all: the ORDER cannot separate -# them (SemVer excludes build metadata from precedence), the LITERALS can. -use_dep tie '=1.0.0+b' -"$MCPP" build > b6b.log 2>&1 && fail "expected the unseeded payload to fail" b6b.log -grep -q 'compare EQUAL' b6b.log \ - && fail "an exact build-metadata pin must not be reported as a tie" b6b.log -grep -q '1\.0\.0+b' b6b.log || fail "the exact key must be addressed literally" b6b.log - -# ── 6. mcpp.lock records the resolution, not the constraint ─────────────── -use_dep im '^1.92.8' -"$MCPP" build -v > b7.log 2>&1 || fail "rebuild failed" b7.log -grep -q 'version = "1.92.8"' mcpp.lock \ - || fail "the lock must record the resolved version" mcpp.lock -grep -q '\^' mcpp.lock \ - && fail "a lock that records a range locks nothing" mcpp.lock -# The banner and the lock read the same data, so they cannot disagree. (The dep -# announces itself as Compiling or Cached depending on the build cache; both go -# through the same version string, which is the point.) -grep -qE '(Compiling|Cached) +acme\.im v1\.92\.8' b7.log \ - || fail "the dependency banner must announce the resolved version" b7.log -grep -q 'acme\.im v\^' b7.log \ - && fail "the banner must never print a constraint as a version" b7.log - -# Honest about what it is not: the lock is written but not yet read back for -# index deps. DELETE this assertion in the same change that makes it -# authoritative — it exists so that removal cannot be forgotten. -grep -q 'does not yet pin future builds' mcpp.lock \ - || fail "the lock must state that it does not pin yet" mcpp.lock - -# Idempotent: a second build must not churn the file. -cp mcpp.lock lock.first -"$MCPP" build > b8.log 2>&1 || fail "second build failed" b8.log -cmp -s mcpp.lock lock.first || fail "mcpp.lock is not stable across builds" mcpp.lock - -# ── 7. ...and stable across COMMANDS, which is the harder half ──────────── -# -# The lock is written from the resolution result, and `mcpp test` resolves -# dev-dependencies while `mcpp build` does not. Recording them therefore made a -# VCS-committed file depend on which command ran last: build, test, build wrote -# three different files. A lock has to be a function of the MANIFEST. -mkdir -p tests -printf 'import devkit;\nint main(){ return devkit_value()==42?0:1; }\n' > tests/t_dev.cpp -cat > mcpp.toml <<'EOF' -[package] -name = "proj" -version = "0.1.0" - -[indices] -acme = { path = "local-index" } - -[dependencies.acme] -im = "^1.92.8" - -[dev-dependencies.acme] -dv = "^1.0" - -[targets.proj] -kind = "bin" -main = "src/main.cpp" -EOF -rm -f mcpp.lock -"$MCPP" build > c1.log 2>&1 || fail "build with a dev-dep failed" c1.log -cp mcpp.lock lock.build -"$MCPP" test > c2.log 2>&1 || fail "test with a dev-dep failed" c2.log -# `mcpp test` announces the resolved version too, as the build's banner does. -grep -q 'acme\.im v\^' c2.log \ - && fail "mcpp test announced a constraint as a version" c2.log -cmp -s mcpp.lock lock.build \ - || { diff lock.build mcpp.lock || true - fail "mcpp test rewrote mcpp.lock — the lock must not depend on the command"; } -"$MCPP" build > c3.log 2>&1 || fail "build after test failed" c3.log -cmp -s mcpp.lock lock.build \ - || fail "mcpp build rewrote mcpp.lock after mcpp test" mcpp.lock -# The dev-dep really was resolved (so this is not passing by never reaching it)… -grep -q 'Resolved acme.dv' c2.log \ - || fail "the dev-dependency was never resolved — the check above is vacuous" c2.log -# …and it is deliberately absent from the file. -grep -q 'acme\.dv' mcpp.lock \ - && fail "dev-dependencies must not be recorded in mcpp.lock" mcpp.lock -grep -q 'dev-dependencies are excluded' mcpp.lock \ - || fail "the lock must state that dev-dependencies are excluded" mcpp.lock - -echo "OK" diff --git a/tests/e2e/203_exact_selector_lock_migration.sh b/tests/e2e/203_exact_selector_lock_migration.sh deleted file mode 100755 index b264192e1..000000000 --- a/tests/e2e/203_exact_selector_lock_migration.sh +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc fresh-sandbox -# A compact dotted selector used to try mcpplibs. first. During the one -# migration release, an existing lock keeps that already-selected identity; -# without the lock, the selector is exact. Both identities exist here so index -# order cannot accidentally make the assertion pass. -set -e -source "$(dirname "$0")/_host_path.sh" - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT - -export MCPP_HOME="$TMP/mcpp-home" -source "$(dirname "$0")/_inherit_toolchain.sh" - -INDEX_DIR="$TMP/index" -INDEX_DIR_HOST="$(host_path "$INDEX_DIR")" -APP="$TMP/app" -mkdir -p "$INDEX_DIR/pkgs/c" "$INDEX_DIR/pkgs/m" "$APP/src" - -descriptor() { - local ns=$1 - local desc=$2 - cat < "$INDEX_DIR/pkgs/c/capi.lua.lua" -descriptor mcpplibs.capi "old prefixed identity" \ - > "$INDEX_DIR/pkgs/m/mcpplibs.capi.lua.lua" - -OLD_ROOT="$APP/.mcpp/.xlings/data/xpkgs/mcpplibs.capi-x-lua/1.0.0" -NEW_ROOT="$APP/.mcpp/.xlings/data/xpkgs/capi-x-lua/1.0.0" -mkdir -p "$OLD_ROOT/src" "$NEW_ROOT/src" -cat > "$OLD_ROOT/src/lua.cppm" <<'EOF' -export module selected.lua; -export int selected_value() { return 41; } -EOF -cat > "$NEW_ROOT/src/lua.cppm" <<'EOF' -export module selected.lua; -export int selected_value() { return 42; } -EOF -printf 'ok\n' > "$OLD_ROOT/.mcpp_ok" -printf 'ok\n' > "$NEW_ROOT/.mcpp_ok" - -cat > "$APP/src/main.cpp" <<'EOF' -import selected.lua; -int main() { return selected_value() == 41 ? 0 : 1; } -EOF -cat > "$APP/mcpp.toml" < "$APP/mcpp.lock" <<'EOF' -# Existing pre-exact-selector resolution anchor. -version = 2 - -[package."capi.lua"] -namespace = "mcpplibs.capi" -version = "1.0.0" -source = "index+mcpplibs.capi@1.0.0" -hash = "fnv1a:migration-fixture" -EOF - -cd "$APP" -"$MCPP" build > build.log 2>&1 || { cat build.log; exit 1; } -grep -q "keeping the locked identity" build.log || { - cat build.log - echo "expected selector migration warning" - exit 1 -} -grep -q "mcpplibs.capi.lua" build.log || { - cat build.log - echo "warning must include the locked selector" - exit 1 -} -"$MCPP" run > run.log 2>&1 || { cat run.log; exit 1; } -grep -q 'namespace = "mcpplibs.capi"' mcpp.lock || { - cat mcpp.lock - echo "lock identity changed during migration" - exit 1 -} - -echo "OK" diff --git a/tests/e2e/24_git_dependency.sh b/tests/e2e/24_git_dependency.sh deleted file mode 100755 index bfbce6d4d..000000000 --- a/tests/e2e/24_git_dependency.sh +++ /dev/null @@ -1,231 +0,0 @@ -#!/usr/bin/env bash -# requires: fresh-sandbox -# 24_git_dependency.sh — M4 #5: git-based dep clones to ~/.mcpp/git// -# and is treated as a path dep. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -export MCPP_HOME="$TMP/mcpp-home" -# Inherit toolchain from user's installed mcpp (avoids reinstalling gcc). -source "$(dirname "$0")/_inherit_toolchain.sh" -# Now override HOME — but inheritance must happen first while $HOME points -# at the user's real home. -export HOME="$TMP/home" # so default_cache_root + git cache resolve here -mkdir -p "$HOME" - -# 1. Build a tiny mcpp lib in a real local git repo (acts as origin). -mkdir "$TMP/origin" && cd "$TMP/origin" -git init --quiet -git config user.email "test@local" -git config user.name "test" -"$MCPP" new mylibA >/dev/null -mv mylibA/* mylibA/.??* . 2>/dev/null || true -rmdir mylibA 2>/dev/null || true -cat > src/greet.cppm <<'EOF' -export module mylibA.greet; -import std; -export auto greet() -> void { std::println("hello from git dep"); } -EOF -rm src/main.cpp -cat > mcpp.toml <<'EOF' -[package] -name = "mylibA" -version = "0.1.0" -[language] -standard = "c++23" -modules = true -import_std = true -[modules] -sources = ["src/**/*.cppm"] -[targets.mylibA] -kind = "lib" -EOF -git add -A >/dev/null -git commit --quiet -m "init" -HEAD_REV=$(git rev-parse HEAD) - -# 2. Consumer references the git repo + rev. -cd "$TMP" -"$MCPP" new myapp >/dev/null -cd myapp -cat > src/main.cpp <<'EOF' -import std; -import mylibA.greet; -int main() { greet(); return 0; } -EOF -cat > mcpp.toml < build.log 2>&1 -triple=$(ls -d target/*/ | head -1) -fp_dir=$(ls "$triple") -out=$(${triple}${fp_dir}/bin/myapp) -[[ "$out" == *"hello from git dep"* ]] || { - echo "FAIL: git dep module not invoked: $out" - cat build.log; exit 1; } - -# Verify we cached the clone under MCPP_HOME/git -[[ -d "$MCPP_HOME/git" ]] || { echo "FAIL: \$MCPP_HOME/git not created"; ls "$MCPP_HOME"; exit 1; } -test -n "$(ls -A "$MCPP_HOME/git" 2>/dev/null)" || { echo "FAIL: git cache empty"; exit 1; } - -# Reuse the same dep — second build should NOT re-clone. -build2=$("$MCPP" build 2>&1) -echo "$build2" | grep -q 'Cloning' && { echo "FAIL: re-cloned on rebuild"; exit 1; } || true - -# 3. Branch refs should update when the user asks for mcpp update. -mkdir "$TMP/branch-origin" && cd "$TMP/branch-origin" -git init --quiet -git checkout -B main --quiet -git config user.email "test@local" -git config user.name "test" -"$MCPP" new branchlib >/dev/null -mv branchlib/* branchlib/.??* . 2>/dev/null || true -rmdir branchlib 2>/dev/null || true -cat > src/greet.cppm <<'EOF' -export module branchlib.greet; -import std; -export auto greet() -> void { std::println("branch dep v1"); } -EOF -rm src/main.cpp -cat > mcpp.toml <<'EOF' -[package] -name = "branchlib" -version = "0.1.0" -[language] -standard = "c++23" -modules = true -import_std = true -[modules] -sources = ["src/**/*.cppm"] -[targets.branchlib] -kind = "lib" -EOF -git add -A >/dev/null -git commit --quiet -m "v1" - -cd "$TMP" -"$MCPP" new branchapp >/dev/null -cd branchapp -cat > src/main.cpp <<'EOF' -import std; -import branchlib.greet; -int main() { greet(); return 0; } -EOF -cat > mcpp.toml < branch-v1.log 2>&1 -triple=$(ls -d target/*/ | head -1) -fp_dir=$(ls "$triple") -out=$(${triple}${fp_dir}/bin/branchapp) -[[ "$out" == *"branch dep v1"* ]] || { - echo "FAIL: branch dep v1 not invoked: $out" - cat branch-v1.log; exit 1; } - -# A rebuild with the lock in place must resolve the branch dep from the -# recorded commit rather than calling `git ls-remote`. -# -# `mcpp clean` first: a bare `mcpp build` here would take the fast path in -# cmd_build (build.ninja is newer than every input → just run ninja), so -# prepare_build never runs and neither the anchor nor `ls-remote` would be -# exercised — the assertion would pass identically without the feature. clean -# wipes only `target/`, so mcpp.lock and the git cache both survive. -# -# Run under MCPP_OFFLINE as well: with the commit in the lock and the clone on -# disk there is nothing left to fetch, so this must hold with the network -# refused. It also pins the local-remote rule — this fixture's `git =` is a -# directory path, which costs no network and must never be refused offline. -"$MCPP" clean >/dev/null -build2=$(MCPP_OFFLINE=1 "$MCPP" build 2>&1) || { - echo "FAIL: offline rebuild with lock + cache present failed" - echo "--- build2 ---"; cat <<<"$build2"; exit 1; } -echo "$build2" | grep -q 'from mcpp.lock' || { echo "FAIL: branch dep not resolved from mcpp.lock on rebuild"; echo "--- build2 ---"; cat <<<"$build2"; exit 1; } -echo "$build2" | grep -q 'Cloning' && { echo "FAIL: branch dep re-cloned on rebuild"; exit 1; } || true - -grep -q 'source = "git+' mcpp.lock || { - echo "FAIL: git dep lock source is not marked as git" - cat mcpp.lock; exit 1; } -grep -q 'branch=main' mcpp.lock || { - echo "FAIL: git dep lock source does not record branch ref" - cat mcpp.lock; exit 1; } -! grep -q 'source = "index+' mcpp.lock || { - echo "FAIL: git dep lock source incorrectly uses index source" - cat mcpp.lock; exit 1; } - -cd "$TMP/branch-origin" -cat > src/greet.cppm <<'EOF' -export module branchlib.greet; -import std; -export auto greet() -> void { std::println("branch dep v2"); } -EOF -git add -A >/dev/null -git commit --quiet -m "v2" - -cd "$TMP/branchapp" - -# mcpp.lock is authoritative, not a hint the cache has to confirm. The branch -# now points at v2 and the git cache is gone, so the only surviving record of -# what this project builds is the commit in mcpp.lock — and the build must -# still produce v1 from it. If the lock were merely an optimisation over the -# cache, evicting ~/.mcpp/git would silently advance the build to v2. -rm -rf "$MCPP_HOME/git" -"$MCPP" clean >/dev/null -"$MCPP" build > branch-evicted.log 2>&1 || { - echo "FAIL: rebuild after git-cache eviction" - cat branch-evicted.log; exit 1; } -triple=$(ls -d target/*/ | head -1) -fp_dir=$(ls "$triple") -out=$(${triple}${fp_dir}/bin/branchapp) -[[ "$out" == *"branch dep v1"* ]] || { - echo "FAIL: git-cache eviction silently advanced the locked branch: $out" - echo "--- branch-evicted.log ---"; cat branch-evicted.log - echo "--- mcpp.lock ---"; cat mcpp.lock; exit 1; } - -"$MCPP" update branchlib >/dev/null -"$MCPP" clean >/dev/null -"$MCPP" build > branch-v2.log 2>&1 -triple=$(ls -d target/*/ | head -1) -fp_dir=$(ls "$triple") -out=$(${triple}${fp_dir}/bin/branchapp) -[[ "$out" == *"branch dep v2"* ]] || { - echo "FAIL: branch dep did not refresh after mcpp update: $out" - echo "--- branch-v2.log ---" - cat branch-v2.log - echo "--- mcpp.lock ---" - cat mcpp.lock - exit 1 -} - -# Cleanup so trap doesn't blow up if any subprocess holds files. -echo "OK" diff --git a/tests/e2e/27_namespace_dependencies.sh b/tests/e2e/27_namespace_dependencies.sh deleted file mode 100755 index b2039cca2..000000000 --- a/tests/e2e/27_namespace_dependencies.sh +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env bash -# requires: fresh-sandbox -# Namespaced dependencies: `[dependencies.] name = { path = "..." }` -# is parsed correctly and the dep is actually picked up by the build. -# Also verifies that the legacy `"." = "..."` quoted form still -# round-trips through the manifest parser. -set -e -source "$(dirname "$0")/_host_path.sh" - -TMP=$(mktemp -d) -TMP_HOST="$(host_path "$TMP")" -trap "rm -rf $TMP" EXIT - -export MCPP_HOME="$TMP/mcpp-home" -source "$(dirname "$0")/_inherit_toolchain.sh" - -# ── 1. Sibling lib package (acme:util). Pure-modular C++23. ───────────── -mkdir -p "$TMP/util-pkg" -cd "$TMP/util-pkg" -"$MCPP" new util > /dev/null -cd util -rm -f src/main.cpp -cat > src/util.cppm <<'EOF' -export module acme.util; -import std; -export int answer() { return 42; } -EOF -cat > mcpp.toml <<'EOF' -[package] -name = "util" -version = "0.1.0" -[modules] -sources = ["src/**/*.cppm"] -[targets.util] -kind = "lib" -EOF - -# ── 2. Consumer that pulls util via the new namespaced subtable form. ─── -mkdir -p "$TMP/app" -cd "$TMP/app" -"$MCPP" new app > /dev/null -cd app -cat > src/main.cpp <<'EOF' -import std; -import acme.util; -int main() { std::println("answer = {}", answer()); return answer() == 42 ? 0 : 1; } -EOF -cat > mcpp.toml < build.log 2>&1 || { cat build.log; echo "build failed"; exit 1; } -out=$("$MCPP" run 2>&1 | tail -1) -[[ "$out" == "answer = 42" ]] || { echo "unexpected output: $out"; exit 1; } - -# ── 3. Same consumer, legacy quoted-dotted form. Must still parse. ────── -cat > mcpp.toml < build-legacy.log 2>&1 || { cat build-legacy.log; echo "legacy form build failed"; exit 1; } -out=$("$MCPP" run 2>&1 | tail -1) -[[ "$out" == "answer = 42" ]] || { echo "legacy form unexpected output: $out"; exit 1; } - -# ── 4. Default-namespace flat form keeps working with no quotes. ──────── -mkdir -p "$TMP/util2-pkg" -cd "$TMP/util2-pkg" -"$MCPP" new util2 > /dev/null -cd util2 -rm -f src/main.cpp -cat > src/util2.cppm <<'EOF' -export module util2; -import std; -export int two() { return 2; } -EOF -cat > mcpp.toml <<'EOF' -[package] -name = "util2" -version = "0.1.0" -[modules] -sources = ["src/**/*.cppm"] -[targets.util2] -kind = "lib" -EOF - -cd "$TMP/app/app" -cat > src/main.cpp <<'EOF' -import std; -import util2; -int main() { std::println("two = {}", two()); return two() == 2 ? 0 : 1; } -EOF -cat > mcpp.toml < build-flat.log 2>&1 || { cat build-flat.log; echo "flat form build failed"; exit 1; } -out=$("$MCPP" run 2>&1 | tail -1) -[[ "$out" == "two = 2" ]] || { echo "flat form unexpected output: $out"; exit 1; } - -echo "OK" diff --git a/tests/e2e/31_transitive_deps.sh b/tests/e2e/31_transitive_deps.sh deleted file mode 100755 index 7690f6073..000000000 --- a/tests/e2e/31_transitive_deps.sh +++ /dev/null @@ -1,124 +0,0 @@ -#!/usr/bin/env bash -# requires: musl -# 31_transitive_deps.sh — transitive dependency walker: -# * a path-dep that itself declares a path-dep is fully resolved -# (consumer doesn't need to list the grandchild explicitly) -# * the grandchild's [build].include_dirs propagate so its headers -# are visible while compiling its parent. -set -e -source "$(dirname "$0")/_host_path.sh" - -TMP=$(mktemp -d) -TMP_HOST="$(host_path "$TMP")" -trap "rm -rf $TMP" EXIT -export MCPP_HOME="$TMP/mcpp-home" -MCPP_INHERIT_CONFIG=0 MCPP_INHERIT_SUBOS=0 source "$(dirname "$0")/_inherit_toolchain.sh" - -# The musl payload is the one linked from the developer's home, whichever -# release that is. The probe named 15.1.0 until 2026-11-12, the runners -# installed a later one, and the script skipped itself on every runner. -source "$(dirname "$0")/_toolchain_env.sh" -MUSL_PAYLOAD="$MUSL_GCC_ROOT/bin/x86_64-linux-musl-g++" -if [[ ! -x "$MUSL_PAYLOAD" ]]; then - echo "SKIP: no reusable musl-gcc xpkg payload" - echo "OK" - exit 77 -fi - -# ── 1. Grandchild: a header-providing C lib whose `[build].include_dirs` -# is what consumers care about. Plays the role of mbedtls in the -# llmapi → tinyhttps → mbedtls chain. -mkdir -p "$TMP/grandchild" && cd "$TMP/grandchild" -"$MCPP" new gc > /dev/null -cd gc -mkdir -p include/gc -cat > include/gc/gc.h <<'EOF' -#pragma once -inline int gc_answer(void) { return 42; } -EOF -rm -f src/main.cpp -cat > src/gc.cppm <<'EOF' -export module gc; -EOF -cat > mcpp.toml <<'EOF' -[package] -name = "gc" -version = "0.1.0" -[build] -include_dirs = ["include"] -[targets.gc] -kind = "lib" -EOF - -# ── 2. Child: depends on grandchild via path; its own .cppm pulls -# , which can only work if gc's include_dirs reach the -# child's compile rule (transitive include propagation). -mkdir -p "$TMP/child" && cd "$TMP/child" -"$MCPP" new ch > /dev/null -cd ch -rm -f src/main.cpp -cat > src/ch.cppm <<'EOF' -module; -#include -export module ch; -export int ch_answer() { return gc_answer(); } -EOF -cat > mcpp.toml < /dev/null -cd top -cat > src/main.cpp <<'EOF' -import std; -import ch; -int main() { - std::println("answer={}", ch_answer()); - return ch_answer() == 42 ? 0 : 1; -} -EOF -cat > mcpp.toml < build.log 2>&1 || { cat build.log; echo "transitive build failed"; exit 1; } -out="$("$MCPP" run 2>&1 | tail -1)" -[[ "$out" == "answer=42" ]] || { echo "unexpected output: $out"; exit 1; } - -# ── 4. Same dep referenced through two parallel paths is allowed -# (no version conflict — same path, same package). -mkdir -p "$TMP/top2" && cd "$TMP/top2" -"$MCPP" new top2 > /dev/null -cd top2 -cat > src/main.cpp <<'EOF' -import std; -import ch; -int main() { std::println("answer={}", ch_answer()); return ch_answer() == 42 ? 0 : 1; } -EOF -cat > mcpp.toml < build-top2.log 2>&1 || { cat build-top2.log; echo "duplicate-but-consistent dep failed"; exit 1; } - -echo "OK" diff --git a/tests/e2e/326_dependency_standard_floor.sh b/tests/e2e/326_dependency_standard_floor.sh deleted file mode 100755 index ba317a4b9..000000000 --- a/tests/e2e/326_dependency_standard_floor.sh +++ /dev/null @@ -1,88 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 326_dependency_standard_floor.sh — a dependency that DECLARED a higher -# standard than the graph is built at (#527 RFC 3). -# -# A C++ module graph has one standard: cross-level BMIs are hard incompatible, -# so the root's level is imposed graph-wide and a dependency's `standard` is -# parsed and then discarded. That is correct. The defect was the silence — a -# package declaring c++26 because it needs c++26 is compiled at whatever the -# consumer says, and fails, if it fails at all, inside a translation unit the -# user does not own. -# -# SCOPED TO AUTHOR-OWNED MANIFESTS, AND THE NEGATIVE IS THE HARD HALF. -# `standardDeclared` says the key was present, not that a human meant it: every -# index descriptor with an mcpp segment declares `language` (measured: 782 of -# 782 in the local registry, 756 of those 774 packages being C libraries with -# `import_std = false` carrying a boilerplate "c++23"). A check that trusted -# declaredness everywhere would fire against the whole index for any root at -# c++20 — which is exactly why the cpp20 design doc deferred it. -# -# DEGRADED, NOT AN ERROR: a package declaring c++26 compiles fine at c++23 -# whenever it does not use a C++26 construct, and that is a green build today. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -mkdir -p lib/src app/src -printf '[package]\nname = "flib"\nversion = "0.1.0"\nstandard = "c++26"\n[targets.flib]\nkind = "lib"\n' > lib/mcpp.toml -printf 'export module flib;\nexport int f(){ return 1; }\n' > lib/src/flib.cppm -printf '[package]\nname = "fapp"\nversion = "0.1.0"\nstandard = "c++23"\n[dependencies]\nflib = { path = "../lib" }\n' > app/mcpp.toml -printf 'import flib;\nint main(){ return f()==1 ? 0 : 1; }\n' > app/src/main.cpp - -cd app - -# ── it fires, and the build still succeeds ────────────────────────────────── -"$MCPP" build > warn.log 2>&1 || { - echo "FAIL: the floor check must be degraded, not an error" - cat warn.log; exit 1 -} -grep -q 'flib' warn.log && grep -q 'c++26' warn.log || { - echo "FAIL: the diagnostic does not name the package and the level" - cat warn.log; exit 1 -} -grep -q 'workspace.package' warn.log || { - echo "FAIL: the hint does not name the one-place fix" - cat warn.log; exit 1 -} - -# ── --strict promotes it, in the one place that policy lives ──────────────── -rm -rf target -if "$MCPP" build --strict > strict.log 2>&1; then - echo "FAIL: --strict did not promote the degradation" - cat strict.log; exit 1 -fi - -# ── SILENT when the dependency did not declare one ────────────────────────── -# -# THE DENOMINATOR for "declared". Same graph, same levels, the key removed. -printf '[package]\nname = "flib"\nversion = "0.1.0"\n[targets.flib]\nkind = "lib"\n' > ../lib/mcpp.toml -rm -rf target -"$MCPP" build > silent.log 2>&1 || { cat silent.log; exit 1; } -grep -q 'declares standard' silent.log && { - echo "FAIL: fired for a dependency that declared nothing" - cat silent.log; exit 1 -} - -# ── SILENT when the dependency declares a level at or below the graph's ───── -printf '[package]\nname = "flib"\nversion = "0.1.0"\nstandard = "c++23"\n[targets.flib]\nkind = "lib"\n' > ../lib/mcpp.toml -rm -rf target -"$MCPP" build > equal.log 2>&1 || { cat equal.log; exit 1; } -grep -q 'declares standard' equal.log && { - echo "FAIL: fired for a dependency at the graph's own level" - cat equal.log; exit 1 -} - -# ── and raising the consumer silences it, which is what the hint promises ─── -printf '[package]\nname = "flib"\nversion = "0.1.0"\nstandard = "c++26"\n[targets.flib]\nkind = "lib"\n' > ../lib/mcpp.toml -printf '[package]\nname = "fapp"\nversion = "0.1.0"\nstandard = "c++26"\n[dependencies]\nflib = { path = "../lib" }\n' > mcpp.toml -rm -rf target -"$MCPP" build > raised.log 2>&1 || { cat raised.log; exit 1; } -grep -q 'declares standard' raised.log && { - echo "FAIL: the documented fix does not silence the diagnostic" - cat raised.log; exit 1 -} - -echo "PASS: 326_dependency_standard_floor" diff --git a/tests/e2e/32_semver_merge.sh b/tests/e2e/32_semver_merge.sh deleted file mode 100755 index bd4f1364a..000000000 --- a/tests/e2e/32_semver_merge.sh +++ /dev/null @@ -1,107 +0,0 @@ -#!/usr/bin/env bash -# requires: fresh-sandbox -# 32_semver_merge.sh — SemVer merge in the transitive walker: -# * Two consumers of the same package with overlapping constraints -# (one exact, one range) merge to a single satisfying version -# instead of erroring out. -# * Non-overlapping pins still hard-error (Level-1 mangling fallback -# is a follow-up). -set -e -source "$(dirname "$0")/_host_path.sh" - -TMP=$(mktemp -d) -TMP_HOST="$(host_path "$TMP")" -trap "rm -rf $TMP" EXIT -export MCPP_HOME="$TMP/mcpp-home" -source "$(dirname "$0")/_inherit_toolchain.sh" - -# Index is needed since we exercise version-source deps. -mkdir -p "$MCPP_HOME/registry/data" "$MCPP_HOME/indices" -# Link the pre-cached mcpplibs index: where mcpp keeps it (indices/), or where -# an earlier mcpp kept it, under the old or the new name. -for src in "$HOME/.mcpp/indices/mcpplibs" "$HOME/.mcpp/registry/data/mcpplibs" "$HOME/.mcpp/registry/data/mcpp-index"; do - if [[ -d "$src" ]]; then - ln -sf "$src" "$MCPP_HOME/indices/mcpplibs" 2>/dev/null \ - || cp -r "$src" "$MCPP_HOME/indices/mcpplibs" - break - fi -done -# Pre-cached xpkg downloads so the test doesn't re-fetch the world. -if [[ -d "$HOME/.mcpp/registry/data/xpkgs" ]]; then - [[ -e "$MCPP_HOME/registry/data/xpkgs" ]] \ - || ln -sf "$HOME/.mcpp/registry/data/xpkgs" \ - "$MCPP_HOME/registry/data/xpkgs" 2>/dev/null \ - || cp -r "$HOME/.mcpp/registry/data/xpkgs" \ - "$MCPP_HOME/registry/data/xpkgs" -fi - -# ── 1. Compatible-merge case ──────────────────────────────────────────── -# mylib (path) pins cmdline to =0.0.1 -# app (root) pins cmdline to >=0.0.1,<1 (resolves to 0.0.2) -# The walker first sees app's broader range (→ 0.0.2), then mylib's -# exact (=0.0.1) — strict equality would error; the merger AND-combines -# the two constraints, picks 0.0.1, re-fetches that version, and -# replaces the previously-pinned slot. - -mkdir -p "$TMP/mylib" && cd "$TMP/mylib" -"$MCPP" new mylib > /dev/null -cd mylib -rm -f src/main.cpp -cat > src/mylib.cppm <<'EOF' -export module mylib; -export int mylib_answer() { return 1; } -EOF -cat > mcpp.toml <<'EOF' -[package] -name = "mylib" -version = "0.1.0" -[targets.mylib] -kind = "lib" - -[dependencies.mcpplibs] -cmdline = "=0.0.1" -EOF - -mkdir -p "$TMP/app" && cd "$TMP/app" -"$MCPP" new app > /dev/null -cd app -cat > src/main.cpp <<'EOF' -import std; -import mylib; -int main() { - std::println("ok={}", mylib_answer()); - return mylib_answer() == 1 ? 0 : 1; -} -EOF -cat > mcpp.toml < build.log 2>&1 || { - cat build.log - echo "compatible merge failed"; exit 1; } - -# The resolver should announce the merge step so future debugging is -# obvious. Either trace style is fine — we just want proof the merger -# (not the strict-equality fallback) handled the conflict. -grep -qE 'Merged.*cmdline.*0\.0\.1|→ v0\.0\.1' build.log || { - cat build.log - echo "no merge trace in build log"; exit 1; } - -out="$("$MCPP" run 2>&1 | tail -1)" -[[ "$out" == "ok=1" ]] || { echo "unexpected output: $out"; exit 1; } - -# Note: the previously-irreconcilable case (=0.0.1 ⨯ =0.0.2 across two -# consumers) used to hard-error here. Level 1 (multi-version mangling) -# now resolves it instead — see tests/e2e/33_multi_version_mangling.sh -# for the cross-major coexistence path. - -echo "OK" diff --git a/tests/e2e/33_multi_version_mangling.sh b/tests/e2e/33_multi_version_mangling.sh deleted file mode 100755 index bfe80e8f6..000000000 --- a/tests/e2e/33_multi_version_mangling.sh +++ /dev/null @@ -1,169 +0,0 @@ -#!/usr/bin/env bash -# requires: elf gcc -# 33_multi_version_mangling.sh — Level 1 of dep resolution: when two -# transitive consumers want incompatible (non-overlapping) versions of -# the same package, the secondary copy is rewritten to use a mangled -# module name so both BMIs coexist in the build graph. -# -# Setup: libA pinned to cmdline 0.0.1, libB pinned to cmdline 0.0.2. -# Both live as path-deps of `app`. The SemVer merger has nothing to -# combine (=0.0.1 ⨯ =0.0.2 has no satisfying version), so the resolver -# stages cmdline 0.0.2 + libB under `target/.mangled/` with rewritten -# `module/import` declarations and the build proceeds. -set -e -source "$(dirname "$0")/_host_path.sh" - -TMP=$(mktemp -d) -TMP_HOST="$(host_path "$TMP")" -trap "rm -rf $TMP" EXIT -export MCPP_HOME="$TMP/mcpp-home" -source "$(dirname "$0")/_inherit_toolchain.sh" - -# Index + xpkgs need to be visible since we exercise version-source deps. -mkdir -p "$MCPP_HOME/registry/data" "$MCPP_HOME/indices" -# Link the pre-cached mcpplibs index: where mcpp keeps it (indices/), or where -# an earlier mcpp kept it, under the old or the new name. -for src in "$HOME/.mcpp/indices/mcpplibs" "$HOME/.mcpp/registry/data/mcpplibs" "$HOME/.mcpp/registry/data/mcpp-index"; do - if [[ -d "$src" ]]; then - ln -sf "$src" "$MCPP_HOME/indices/mcpplibs" 2>/dev/null \ - || cp -r "$src" "$MCPP_HOME/indices/mcpplibs" - break - fi -done -if [[ -d "$HOME/.mcpp/registry/data/xpkgs" ]]; then - [[ -e "$MCPP_HOME/registry/data/xpkgs" ]] \ - || ln -sf "$HOME/.mcpp/registry/data/xpkgs" \ - "$MCPP_HOME/registry/data/xpkgs" -fi - -# ── libA: pinned to cmdline 0.0.1 ────────────────────────────────────── -mkdir -p "$TMP/libA" && cd "$TMP/libA" -"$MCPP" new libA > /dev/null -cd libA -rm -f src/main.cpp -cat > src/libA.cppm <<'EOF' -export module libA; -import mcpplibs.cmdline; // pulls cmdline 0.0.1 (primary) -import std; -export int libA_v() { return 1; } -EOF -cat > mcpp.toml <<'EOF' -[package] -name = "libA" -version = "0.1.0" -[targets.libA] -kind = "lib" - -[dependencies.mcpplibs] -cmdline = "=0.0.1" -EOF - -# ── libB: pinned to cmdline 0.0.2 (incompatible with libA) ───────────── -mkdir -p "$TMP/libB" && cd "$TMP/libB" -"$MCPP" new libB > /dev/null -cd libB -rm -f src/main.cpp -cat > src/libB.cppm <<'EOF' -module; -// Declared in the global module fragment, so it has the same linkage as its -// definition in libB_impl.cpp — a non-module translation unit. Declaring it -// after `export module` would attach it to libB and leave the definition -// unattached, which is a mistake in a fixture rather than in what it tests. -int libB_impl_v(); -export module libB; -import mcpplibs.cmdline; // resolver rewrites this to the mangled secondary -import std; -export int libB_v() { return libB_impl_v(); } -EOF -# A PRIVATE HEADER BESIDE A SOURCE, WHICH IS WHAT STAGING USED TO DROP. -# -# libB is the copy the resolver stages under `target/.mangled/`, and a quoted -# include is resolved relative to the directory of the file holding it — so -# moving the source moves the search. No `include_dirs` entry is involved and -# absolutizing one cannot help: a package with its header beside its source -# never declared a path because it never needed one. -# -# Measured before the fix, on this fixture and on a real package alike: -# -# .mangled/libB/__self__/src/libB_impl.cpp:1:10: -# fatal error: libB_detail.h: No such file or directory -# -# The failure names a staging directory the author never wrote, for a header -# sitting exactly where the source expects it. -cat > src/libB_detail.h <<'EOF' -#pragma once -inline int libB_detail_v() { return 2; } -EOF -cat > src/libB_impl.cpp <<'EOF' -#include "libB_detail.h" -int libB_impl_v() { return libB_detail_v(); } -EOF -cat > mcpp.toml <<'EOF' -[package] -name = "libB" -version = "0.1.0" -[targets.libB] -kind = "lib" - -[dependencies.mcpplibs] -cmdline = "=0.0.2" -EOF - -# ── app: pulls both libs ─────────────────────────────────────────────── -mkdir -p "$TMP/app" && cd "$TMP/app" -"$MCPP" new app > /dev/null -cd app -cat > src/main.cpp <<'EOF' -import std; -import libA; -import libB; -int main() { - std::println("a={} b={}", libA_v(), libB_v()); - return libA_v() + libB_v() == 3 ? 0 : 1; -} -EOF -cat > mcpp.toml < build.log 2>&1 || { - cat build.log - echo "mangling build failed"; exit 1; } - -# Sanity: the resolver should announce the mangling step. Either -# `Mangled` (our trace) or the mangled module name landing in the -# ninja file is fine. -grep -q 'Mangled.*cmdline' build.log || { - cat build.log - echo "expected 'Mangled' trace in build log"; exit 1; } - -# Look for the mangled BMI on disk (proof both versions actually built). -find target -name 'mcpplibs.cmdline__v0_0_2__mcpp.gcm' | grep -q . || { - echo "mangled BMI mcpplibs.cmdline__v0_0_2__mcpp.gcm not found" - find target -name '*.gcm' | head -10 - exit 1; } -find target -name 'mcpplibs.cmdline.gcm' | grep -q . || { - echo "primary BMI mcpplibs.cmdline.gcm not found" - find target -name '*.gcm' | head -10 - exit 1; } - -# The staged copy carries the header beside the staged source. Asserted on -# disk as well as through the build, because a build that happened to find the -# header through some other path would pass while leaving this broken. -staged="$(find target/.mangled -name 'libB_detail.h' | head -1)" -[ -n "$staged" ] || { - echo "the header beside the staged source was not staged with it" - find target/.mangled -type f | head -20 - exit 1; } -echo " ok the header beside the staged source came with it ($staged)" - -out="$("$MCPP" run 2>&1 | tail -1)" -[[ "$out" == "a=1 b=2" ]] || { echo "unexpected output: $out"; exit 1; } - -echo "OK" diff --git a/tests/e2e/603_version_floor.sh b/tests/e2e/603_version_floor.sh deleted file mode 100755 index 985942cc7..000000000 --- a/tests/e2e/603_version_floor.sh +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env bash -# A package needing more of the machine than it has is refused before compiling. -# -# The failure this prevents is not visible at build time on its own: a program -# built against a runtime newer than the driver it will meet compiles cleanly, -# links cleanly, and fails at first use with a message naming neither side. -# Measured on the development machine, a CUDA 13.3 build against a driver -# serving 12.4 does exactly that. -# -# Nothing here mentions a vendor. The engine reads a name, a relation and a -# version; `cuda.driver` is data passing through. That is asserted directly in -# the third case with a name no backend uses. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -# A package that states a fact about this machine, the way a package that -# probed at install time would. -mkfact() { # $1 name $2 "cuda.driver=12.4" - mkdir -p "$1/src" - printf '[package]\nname = "%s"\nversion = "0.1.0"\n[language]\nstandard = "c++23"\n[runtime]\nprovides = ["%s"]\n[targets.%s]\nkind = "lib"\n' "$1" "$2" "$1" > "$1/mcpp.toml" - echo "int ${1//-/_}_anchor(void){return 0;}" > "$1/src/$1.c" -} - -# A package that needs something of the machine. -mkneed() { # $1 name $2 "cuda.driver >= 13.0" - mkdir -p "$1/src" - { - printf '[package]\nname = "%s"\nversion = "0.1.0"\n[language]\nstandard = "c++23"\n' "$1" - printf '[[runtime.requirements]]\nkind = "version-floor"\nvalue = "%s"\n' "$2" - printf '[targets.%s]\nkind = "lib"\n' "$1" - } > "$1/mcpp.toml" - echo "int ${1//-/_}_anchor(void){return 0;}" > "$1/src/$1.c" -} - -consumer() { - rm -rf app; "$MCPP" new app > /dev/null; cd app - { printf '[package]\nname="app"\nversion="0.1.0"\n[language]\nstandard="c++23"\n[dependencies]\n' - for d in "$@"; do echo "$d = { path = \"../$d\" }"; done; } > mcpp.toml - rm -f src/*.cppm - echo 'int main(){return 0;}' > src/main.cpp -} - -# ── One: the floor is above the fact ─────────────────────────────────────── -mkfact driverfact "cuda.driver=12.4" -mkneed toolkitnew "cuda.driver >= 13.0" -consumer driverfact toolkitnew - -if "$MCPP" build > out.log 2>&1; then - cat out.log; echo "FAIL: a floor above the stated fact was accepted"; exit 1 -fi -grep -q "cuda.driver" out.log || { cat out.log; echo "FAIL: refusal does not name what is short"; exit 1; } -grep -q "13.0" out.log || { cat out.log; echo "FAIL: refusal does not say what was needed"; exit 1; } -grep -q "12.4" out.log || { cat out.log; echo "FAIL: refusal does not say what is there"; exit 1; } -grep -q "driverfact" out.log || { cat out.log; echo "FAIL: refusal does not say who stated the fact"; exit 1; } -echo "PASS: refused, naming the requirement, both versions and who stated the fact" - -# ── Two: the floor is met ────────────────────────────────────────────────── -cd "$TMP" -mkneed toolkitok "cuda.driver >= 12.0" -consumer driverfact toolkitok -"$MCPP" build > ok.log 2>&1 || { cat ok.log; echo "FAIL: a met floor was refused"; exit 1; } -echo "PASS: a met floor builds" - -# ── Three: a floor with NO fact is silent, and the name is just data ─────── -# -# This is the control that matters. Without it the first case would also pass -# against an engine that refused every version-floor requirement, which would -# turn "we do not know" into "no" — the exact failure mode this mechanism was -# built to avoid. -cd "$TMP" -mkneed futureneed "some.future.thing >= 4.2.1" -consumer futureneed -"$MCPP" build > quiet.log 2>&1 || { cat quiet.log; echo "FAIL: a floor nobody answered was refused"; exit 1; } -echo "PASS: a floor with no stated fact is silent, for a name no backend uses" - -echo "PASS: version floor" diff --git a/tests/e2e/61_dependency_visibility.sh b/tests/e2e/61_dependency_visibility.sh deleted file mode 100644 index ec6a0ec55..000000000 --- a/tests/e2e/61_dependency_visibility.sh +++ /dev/null @@ -1,114 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# Dependency visibility: private deps must be available while compiling the -# dependent package, but their include dirs must not leak to consumers. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -export MCPP_HOME="$TMP/mcpp-home" -source "$(dirname "$0")/_inherit_toolchain.sh" - -cd "$TMP" -mkdir -p hidden/include/hidden hidden/src facade/src app/src - -cat > hidden/include/hidden/value.hpp <<'EOF' -#pragma once -inline int hidden_value() { return 7; } -EOF - -cat > hidden/src/hidden.cppm <<'EOF' -module; -#include -export module hidden; -export int hidden_answer() { return hidden_value(); } -EOF - -cat > hidden/mcpp.toml <<'EOF' -[package] -name = "hidden" -version = "0.1.0" - -[build] -include_dirs = ["include"] - -[modules] -sources = ["src/*.cppm"] - -[targets.hidden] -kind = "lib" -EOF - -cat > facade/src/facade.cppm <<'EOF' -export module facade; -import hidden; -export int facade_answer() { return hidden_answer(); } -EOF - -cat > facade/mcpp.toml <<'EOF' -[package] -name = "facade" -version = "0.1.0" - -[modules] -sources = ["src/*.cppm"] - -[targets.facade] -kind = "lib" - -[dependencies] -hidden = { path = "../hidden", visibility = "private" } -EOF - -cat > app/src/main.cpp <<'EOF' -import facade; -int main() { - return facade_answer() == 7 ? 0 : 1; -} -EOF - -cat > app/mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" - -[targets.app] -kind = "bin" -main = "src/main.cpp" - -[dependencies] -facade = { path = "../facade" } -EOF - -cd app -"$MCPP" build > build.log 2>&1 || { - cat build.log - echo "private dep should compile its direct consumer" - exit 1 -} -"$MCPP" run > run.log 2>&1 || { - cat run.log - echo "app run failed" - exit 1 -} - -cat > src/main.cpp <<'EOF' -#include -import facade; -int main() { - return hidden_value() == facade_answer() ? 0 : 1; -} -EOF - -if "$MCPP" build > leak.log 2>&1; then - cat leak.log - echo "private dependency include dir leaked to consumer" - exit 1 -fi -grep -q "hidden/value.hpp" leak.log || { - cat leak.log - echo "consumer failed for an unexpected reason" - exit 1 -} - -echo "OK" diff --git a/tests/e2e/627_one_package_one_version.sh b/tests/e2e/627_one_package_one_version.sh deleted file mode 100755 index 65dbe5a87..000000000 --- a/tests/e2e/627_one_package_one_version.sh +++ /dev/null @@ -1,127 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# ONE PACKAGE, ONE VERSION. -# -# A project and one of its dependencies both name `xim:zoxide`, at different -# versions. mcpp had two definitions of when two addresses name one package: the -# conditional merge compared the PACKAGE, the graph split compared the whole -# STRING. Under the second, these were two packages -- both installed, and the -# `xpkg_dir` answer decided by whichever "keep the first value for a name" rule -# a later pass happened to reach. Installed twice, answered once, said nothing. -# -# THE CRITERION IS BOTH HALVES AT ONCE. Asserting only "one directory exists" -# would pass on an engine that installs one and answers the other; asserting -# only the answer would pass on one that installs two. The pair is what -# identifies the defect. -# -# WHY zoxide: three versions in the index for all three host platforms, about a -# megabyte each. The tool is never run -- only its payload directory is asked -# about -- so the test measures resolution, not the tool. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -TOOL=zoxide -PROJECT_PIN=0.9.9 -DEP_PIN=0.9.7 - -mkdir -p dep/src -cat > dep/src/lib.cpp <<'EOF' -int dep_touch() { return 1; } -EOF -# THE ANSWER IS RECORDED BY THE DEPENDENCY'S OWN BUILD PROGRAM, which is the -# reader that has to agree with the installer. To a file, because mcpp prints a -# build program's output only when it fails. -cat > dep/build.mcpp < -#include -import mcpp; -int main() { - const char* d = mcpp::xpkg_dir("xim", "$TOOL"); - std::string out = std::string(mcpp::manifest_dir()) + "/answered.txt"; - std::FILE* f = std::fopen(out.c_str(), "w"); - if (f == nullptr) return 3; - std::fprintf(f, "%s\n", d == nullptr ? "" : d); - std::fclose(f); - return 0; -} -EOF -cat > dep/mcpp.toml < app/src/main.cpp <<'EOF' -int main() { return 0; } -EOF -cat > app/mcpp.toml <build.log 2>&1; then - echo "FAIL: the consumer did not build" - tail -20 build.log - exit 1 -fi - -STORE="$MCPP_HOME/registry/data/xpkgs/xim-x-$TOOL" -[ -d "$STORE" ] || { echo "FAIL: $TOOL was never installed ($STORE)"; tail -20 build.log; exit 1; } -installed=$(ls -1 "$STORE" | sort | tr '\n' ' ') -count=$(ls -1 "$STORE" | wc -l | tr -d ' ') -echo "installed versions: $installed" -[ "$count" -eq 1 ] || { - echo "FAIL: two declarations of one package installed $count versions: $installed" - exit 1 -} -[ -d "$STORE/$PROJECT_PIN" ] || { - echo "FAIL: the declaration nearer the artifact did not win: $installed" - exit 1 -} - -answered="$TMP/dep/answered.txt" -[ -s "$answered" ] || { echo "FAIL: the dependency's build program recorded nothing"; exit 1; } -echo "xpkg_dir answered: $(cat "$answered")" -grep -q "$PROJECT_PIN" "$answered" || { - echo "FAIL: installed $PROJECT_PIN and answered $(cat "$answered")" - exit 1 -} - -# AND IT IS SAID OUT LOUD. An override visible only as "two versions were -# declared and one directory exists" is a fact the reader has to reconstruct -# from the filesystem. -grep -q "declared at two versions" build.log || { - echo "FAIL: the override was not reported" - grep -i 'zoxide' build.log | head -5 - exit 1 -} -grep -q "toolowner" build.log || { - echo "FAIL: the report does not name the dependency that lost" - exit 1 -} - -echo "PASS: one package, one version -- installed, answered and reported" diff --git a/tests/e2e/628_a_pin_below_a_stated_floor_is_refused.sh b/tests/e2e/628_a_pin_below_a_stated_floor_is_refused.sh deleted file mode 100755 index d33f2d984..000000000 --- a/tests/e2e/628_a_pin_below_a_stated_floor_is_refused.sh +++ /dev/null @@ -1,117 +0,0 @@ -#!/usr/bin/env bash -# requires: -# NO CAPABILITY GATE, unlike its two neighbours. They need a dependency's -# build program to run, and `gcc` is how this runner spells "a toolchain that -# compiles one"; this test's first leg refuses before anything is compiled and -# its second only builds an ordinary library. The rule it checks is -# platform-independent, so it should be checked on every platform -- a Windows -# runner is where a path or a quoting assumption would surface. -# A PIN THAT DOES NOT SATISFY A STATED FLOOR IS A REFUSAL, NOT A SECOND INSTALL. -# -# Once one package means one version, the two declarations have to be -# compatible. A rule package writes `>=0.9.9` because that is the version its -# rule needs; a project that pins 0.9.7 has stated something that cannot hold at -# the same time. Installing both -- what the engine used to do -- postponed the -# contradiction to whatever the rule failed to find, or to nothing at all. -# -# THIS IS A COMPARISON, NOT A SEARCH. The version is chosen by adjudication (the -# declaration nearer the artifact wins) and then checked against the -# requirements that lost. Nothing here asks the index which versions exist, so -# there is no constraint solver in the engine and none is needed for this. -# -# THE MESSAGE MUST NAME BOTH SIDES. "cannot be satisfied" without saying who -# asked for what leaves the author to find the second declaration by grep, and -# it is usually in a package they do not own. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -TOOL=zoxide - -mkdir -p dep/src -cat > dep/src/lib.cpp <<'EOF' -int dep_touch() { return 1; } -EOF -cat > dep/mcpp.toml < app/src/main.cpp <<'EOF' -int main() { return 0; } -EOF -cat > app/mcpp.toml <build.log 2>&1; then - echo "FAIL: a pin below the dependency's floor was accepted" - grep -iE "$TOOL" build.log | head -5 - exit 1 -fi -out=$(cat build.log) - -for needle in "xim:$TOOL" "0.9.7" ">=0.9.9" "toolowner" "this project"; do - echo "$out" | grep -q -- "$needle" || { - echo "FAIL: the refusal does not mention '$needle'" - echo "$out" | tail -10 - exit 1 - } -done -echo "$out" | grep -q "drop the pin" || { - echo "FAIL: the refusal does not state the way out" - echo "$out" | tail -10 - exit 1 -} - -# AND THE REFUSAL HAS A TOKEN. A machine consumer classifying this outcome must -# not be reduced to matching the prose -- which is what docs/11 exists to -# promise, and what a reworded sentence silently breaks. -if command -v jq >/dev/null 2>&1; then - "$MCPP" why toolchain --format json >why.json 2>/dev/null || true - reason=$(jq -r '.data.reason // empty' why.json 2>/dev/null || true) - [ "$reason" = "tool-version-conflict" ] || { - echo "FAIL: reason was '$reason', expected tool-version-conflict" - head -c 400 why.json - exit 1 - } - echo "machine-readable reason: $reason" -fi - -# …AND RAISING THE PIN CLEARS IT. Without this leg the test would also pass on -# an engine that refuses every project declaring a tool its dependency also -# declares. -cd "$TMP" -sed -i.bak "s/\"0.9.7\"/\"0.9.9\"/" app/mcpp.toml -rm -f app/mcpp.toml.bak -cd app -"$MCPP" build >build2.log 2>&1 || { - echo "FAIL: a pin that DOES satisfy the floor was still refused" - tail -20 build2.log - exit 1 -} - -echo "PASS: a pin below a stated floor is refused, and one that satisfies it is not" diff --git a/tests/e2e/629_a_version_range_is_answered_by_xpkg_dir.sh b/tests/e2e/629_a_version_range_is_answered_by_xpkg_dir.sh deleted file mode 100755 index 28d709610..000000000 --- a/tests/e2e/629_a_version_range_is_answered_by_xpkg_dir.sh +++ /dev/null @@ -1,140 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# A RANGE IN THE VERSION POSITION IS A CONSTRAINT, AND IT MUST BE ANSWERABLE. -# -# The version position of an xlings address has accepted range expressions all -# along and xlings resolves one when it installs. `xpkg_dir` did not: it treated -# the whole position as a directory name, so `>=0.9.9` installed a payload and -# then reported that nothing was installed. A rule package could therefore state -# a floor and could not then find what the floor had brought in -- which is why -# every project using a rule repeated the rule's own package list with exact -# pins. -# -# THIS IS THE ARRANGEMENT THE RANGE EXISTS FOR: the rule owns "which package, -# and no older than what", the project owns "and exactly this one" -- and writes -# nothing at all when it has no opinion, which is the common case. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -TOOL=zoxide - -mkdir -p dep/src -cat > dep/src/lib.cpp <<'EOF' -int dep_touch() { return 1; } -EOF -cat > dep/build.mcpp < -#include -import mcpp; -int main() { - const char* d = mcpp::xpkg_dir("xim", "$TOOL"); - if (d == nullptr || d[0] == '\0') { - std::fprintf(stderr, - "a [feature-xlings] entry stated as a range was installed and then " - "answered as absent\n"); - return 2; - } - std::string out = std::string(mcpp::manifest_dir()) + "/answered.txt"; - std::FILE* f = std::fopen(out.c_str(), "w"); - if (f == nullptr) return 3; - std::fprintf(f, "%s\n", d); - std::fclose(f); - return 0; -} -EOF -# UNDER A FEATURE, which is the shape a rule package uses: the tool arrives only -# for the consumers that asked for the rule. -cat > dep/mcpp.toml < app/src/main.cpp <<'EOF' -int main() { return 0; } -EOF -# THE PROJECT DECLARES NO TOOL AT ALL. That is the whole claim: one edge, and -# the environment the rule needs comes with it. -cat > app/mcpp.toml <<'EOF' -[package] -name = "consumer" -version = "0.1.0" -[dependencies] -toolowner = { path = "../dep", features = ["usestool"] } -[targets.consumer] -kind = "bin" -main = "src/main.cpp" -EOF - -export MCPP_HOME="$TMP/home" -mkdir -p "$MCPP_HOME" - -cd app -if ! "$MCPP" build >build.log 2>&1; then - echo "FAIL: the consumer did not build" - grep -iE 'answered as absent|error' build.log | head -5 - tail -10 build.log - exit 1 -fi - -answered="$TMP/dep/answered.txt" -[ -s "$answered" ] || { - echo "FAIL: the build program recorded nothing" - tail -10 build.log - exit 1 -} -echo "xpkg_dir answered: $(cat "$answered")" -grep -q "xim-x-$TOOL" "$answered" || { - echo "FAIL: the recorded path does not name $TOOL: $(cat "$answered")" - exit 1 -} -# THE HIGHEST INSTALLED VERSION SATISFYING THE RANGE, not merely some version. -# 0.9.7 is in the index and does not satisfy `>=0.9.9`; answering it would be a -# floor silently lowered. -grep -qE '/(0\.9\.9|0\.10\.[0-9]+)$' "$answered" || { - echo "FAIL: the answer does not satisfy >=0.9.9: $(cat "$answered")" - exit 1 -} - -# ── the other direction ───────────────────────────────────────────────────── -# -# AN IMPLEMENTATION THAT PASSED THE RANGE THROUGH AS A LITERAL WOULD ALSO PASS -# EVERYTHING ABOVE. What separates "the range was solved" from "the string -# happened to name something" is a range that nothing can satisfy: it has to be -# refused, and refused for that reason. -cd "$TMP" -sed 's/>=0.9.9/>=99.0.0/' dep/mcpp.toml > dep/mcpp.toml.new -mv dep/mcpp.toml.new dep/mcpp.toml -rm -rf app/target dep/answered.txt -cd app -if "$MCPP" build >unsat.log 2>&1; then - echo "FAIL: an unsatisfiable range was accepted" - grep -i "$TOOL" unsat.log | head -3 - exit 1 -fi -grep -q 'not found in the synced index' unsat.log || { - echo "FAIL: refused, but not because the range is unsatisfiable" - grep -i error unsat.log | head -3 - exit 1 -} -grep -q ">=99.0.0" unsat.log || { - echo "FAIL: the refusal does not quote the range that could not be satisfied" - grep -i error unsat.log | head -3 - exit 1 -} -echo "an unsatisfiable range is refused, naming it" - -echo "PASS: a range is installed and answered in one direction, and refused in the other" diff --git a/tests/e2e/62_dotted_dependency_selector_priority.sh b/tests/e2e/62_dotted_dependency_selector_priority.sh deleted file mode 100644 index 8fd267ccc..000000000 --- a/tests/e2e/62_dotted_dependency_selector_priority.sh +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc fresh-sandbox -# Dotted selectors in a single [dependencies] table are exact: -# imgui.core -> (imgui, core), and never mcpplibs.imgui/core. -# This test provides that exact package and verifies the build stays offline. -set -e -source "$(dirname "$0")/_host_path.sh" - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT - -export MCPP_HOME="$TMP/mcpp-home" -source "$(dirname "$0")/_inherit_toolchain.sh" - -INDEX_DIR="$TMP/imgui-index" -INDEX_DIR_HOST="$(host_path "$INDEX_DIR")" -mkdir -p "$INDEX_DIR/pkgs/i" -cat > "$INDEX_DIR/pkgs/i/imgui.core.lua" <<'EOF' -package = { - spec = "1", - namespace = "imgui", - name = "imgui.core", - description = "Exact dotted selector test package", - licenses = {"MIT"}, - type = "package", - xpm = { - linux = { - ["1.0.0"] = { - url = "https://example.invalid/imgui-core-1.0.0.tar.gz", - sha256 = "0000000000000000000000000000000000000000000000000000000000000000", - }, - }, - }, - mcpp = { - language = "c++23", - import_std = false, - sources = { "src/core.cppm" }, - targets = { ["core"] = { kind = "lib" } }, - deps = {}, - }, -} -EOF - -mkdir -p "$TMP/project/app/src" \ - "$TMP/project/app/.mcpp/.xlings/data/xpkgs/imgui-x-imgui.core/1.0.0/src" -cd "$TMP/project/app" - -cat > .mcpp/.xlings/data/xpkgs/imgui-x-imgui.core/1.0.0/src/core.cppm <<'EOF' -export module imgui.core; - -export int imgui_core_value() { - return 42; -} -EOF - -cat > src/main.cpp <<'EOF' -import imgui.core; - -int main() { - return imgui_core_value() == 42 ? 0 : 1; -} -EOF - -cat > mcpp.toml < build.log 2>&1 || { - cat build.log - exit 1 -} - -"$MCPP" run > run.log 2>&1 || { - cat run.log - exit 1 -} - -grep -q 'namespace = "imgui"' mcpp.lock || { - cat mcpp.lock - echo "expected lockfile to record resolved peer namespace imgui" - exit 1 -} - -echo "OK" diff --git a/tests/e2e/63_bare_dependency_peer_root_priority.sh b/tests/e2e/63_bare_dependency_peer_root_priority.sh deleted file mode 100755 index 6117200bd..000000000 --- a/tests/e2e/63_bare_dependency_peer_root_priority.sh +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc fresh-sandbox -# Bare selectors mean exactly the default mcpplibs namespace. This test -# provides mcpplibs:imgui and verifies `imgui = "1.0.0"` records that identity. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT - -export MCPP_HOME="$TMP/mcpp-home" -source "$(dirname "$0")/_inherit_toolchain.sh" - -INDEX_DIR="$MCPP_HOME/indices/mcpplibs" -PKG_ROOT="$MCPP_HOME/registry/data/xpkgs/mcpplibs-x-imgui/1.0.0" -mkdir -p "$INDEX_DIR/pkgs/i" "$PKG_ROOT/src" -printf 'ok\n' > "$INDEX_DIR/.mcpp-index-updated" - -cat > "$INDEX_DIR/pkgs/i/imgui.lua" <<'EOF' -package = { - spec = "1", - namespace = "mcpplibs", - name = "imgui", - description = "Default-namespace exact selector test package", - licenses = {"MIT"}, - type = "package", - xpm = { - linux = { - ["1.0.0"] = { - url = "https://example.invalid/imgui-1.0.0.tar.gz", - sha256 = "0000000000000000000000000000000000000000000000000000000000000000", - }, - }, - }, - mcpp = { - language = "c++23", - import_std = false, - sources = { "src/imgui.cppm" }, - targets = { ["imgui"] = { kind = "lib" } }, - deps = {}, - }, -} -EOF - -cat > "$PKG_ROOT/src/imgui.cppm" <<'EOF' -export module imgui; - -export int imgui_value() { - return 42; -} -EOF -printf 'ok\n' > "$PKG_ROOT/.mcpp_ok" - -mkdir -p "$TMP/project/app/src" -cd "$TMP/project/app" - -cat > src/main.cpp <<'EOF' -import imgui; - -int main() { - return imgui_value() == 42 ? 0 : 1; -} -EOF - -cat > mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" - -[dependencies] -imgui = "1.0.0" - -[targets.app] -kind = "bin" -main = "src/main.cpp" -EOF - -"$MCPP" build > build.log 2>&1 || { - cat build.log - exit 1 -} - -"$MCPP" run > run.log 2>&1 || { - cat run.log - exit 1 -} - -grep -q '\[package."imgui"\]' mcpp.lock || { - cat mcpp.lock - echo "expected imgui package lock entry" - exit 1 -} - -if ! grep -q 'namespace = "mcpplibs"' mcpp.lock; then - cat mcpp.lock - echo "bare package must lock as the default mcpplibs namespace" - exit 1 -fi - -echo "OK" diff --git a/tests/e2e/661_one_identity_two_declarations.sh b/tests/e2e/661_one_identity_two_declarations.sh deleted file mode 100755 index 5766086c2..000000000 --- a/tests/e2e/661_one_identity_two_declarations.sh +++ /dev/null @@ -1,448 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 661_one_identity_two_declarations.sh -- two declarations of the SAME -# dependency identity (T3, 2026-09-13-630 record §2.2). Before this, a -# second `git`/`path` declaration of an already-resolved identity carried -# no comparable reference at all -- `ResolvedRecord` held no path and no -# `gitRev` -- so the winner was silently whichever request the FIFO -# worklist happened to dequeue first, and a root/dependency KIND clash -# (`path` vs `git`) was an unconditional refusal even when the root itself -# was a party. Six cases: -# -# 1. root `git rev=A`, a library `git rev=B` -> root wins, warned -# 2. root `path` (dirty working tree), library `git` -> root wins, warned -# 3. no root declaration, two libraries at B and C -> first wins, warned -# 4. two libraries, one `path` one `git` -> refused, "Pick one" -# 5. root pins a checkout below a library's SemVer floor -> refused -# 6. the SAME reference declared twice -> silent, --strict OK -set -e -source "$(dirname "$0")/_host_path.sh" - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT - -fail() { - local msg="$1"; shift - echo "FAIL: $msg" - for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done - exit 1 -} - -# ── one shared git origin for "framework": commits A, B, C, then a dirty -# working tree. A `git clone` of a commit never sees the dirty edit made -# afterwards, so cases 1/3/4/6 (which clone a fixed rev) and case 2 (which -# reads the live directory as a `path` dep) can share this one repository. ── -FW_GIT="$TMP/framework-git" -FW_GIT_HOST="$(host_path "$FW_GIT")" -mkdir -p "$FW_GIT/src" -git init --quiet "$FW_GIT" -git -C "$FW_GIT" config user.email "test@local" -git -C "$FW_GIT" config user.name "test" -cat > "$FW_GIT/mcpp.toml" <<'EOF' -[package] -name = "framework" -version = "0.1.0" - -[build] -sources = ["src/*.c"] - -[targets.framework] -kind = "lib" -EOF - -write_marker() { - printf 'int framework_marker(void) { return %s; }\n' "$1" > "$FW_GIT/src/framework.c" -} - -write_marker 101 -git -C "$FW_GIT" add -A >/dev/null -git -C "$FW_GIT" commit --quiet -m "A" -REV_A=$(git -C "$FW_GIT" rev-parse HEAD) - -write_marker 102 -git -C "$FW_GIT" add -A >/dev/null -git -C "$FW_GIT" commit --quiet -m "B" -REV_B=$(git -C "$FW_GIT" rev-parse HEAD) - -write_marker 103 -git -C "$FW_GIT" add -A >/dev/null -git -C "$FW_GIT" commit --quiet -m "C" -REV_C=$(git -C "$FW_GIT" rev-parse HEAD) - -# The dirty edit for case 2 -- never committed, so REV_A/B/C above are -# unaffected by it. -write_marker 199 - -# A second "framework", pinned by a `path` dep and carrying its own -# `[package] version` -- used by case 5's SemVer-against-checkout check. -FW_V020="$TMP/framework-v020" -mkdir -p "$FW_V020/src" -cat > "$FW_V020/mcpp.toml" <<'EOF' -[package] -name = "framework" -version = "0.2.0" - -[build] -sources = ["src/*.c"] - -[targets.framework] -kind = "lib" -EOF -printf 'int framework_marker(void) { return 20; }\n' > "$FW_V020/src/framework.c" - -# make_lib DIR NAME EXTRA_MANIFEST_LINES -- a tiny library whose one -# function calls framework_marker() and returns it, so a consumer can -# observe WHICH framework instance the library actually built against. -make_lib() { - local dir="$1" name="$2" extra="$3" - mkdir -p "$dir/src" - { - printf '[package]\nname = "%s"\nversion = "0.1.0"\n\n' "$name" - printf '[build]\nsources = ["src/*.c"]\n\n' - printf '[targets.%s]\nkind = "lib"\n\n' "$name" - printf '%s\n' "$extra" - } > "$dir/mcpp.toml" - printf 'extern int framework_marker(void);\nint %s_marker(void) { return framework_marker(); }\n' \ - "$name" > "$dir/src/$name.c" -} - -# ═══════════════════════════════════════════════════════════════════════ -# Case 1: root `git rev=A`, a library `git rev=B` -- the root wins. -# ═══════════════════════════════════════════════════════════════════════ -C1="$TMP/case1" -make_lib "$C1/libb" "libb" "$(cat < "$C1/app/mcpp.toml" < "$C1/app/src/main.cpp" <<'EOF' -#include -extern "C" int libb_marker(void); -int main() { - std::printf("libb=%d\n", libb_marker()); - return 0; -} -EOF -( - cd "$C1/app" - ec=0 - "$MCPP" run > run.log 2>&1 || ec=$? - [[ $ec -eq 0 ]] || fail "case 1: exit $ec" run.log - grep -q "app" run.log || fail "case 1: warning does not name the root" run.log - grep -q "libb" run.log || fail "case 1: warning does not name the library" run.log - grep -q "$REV_A" run.log || fail "case 1: warning does not name rev A" run.log - grep -q "$REV_B" run.log || fail "case 1: warning does not name rev B" run.log - grep -q "libb=101" run.log || fail "case 1: program did not print A's marker" run.log - grep -q "$REV_A" mcpp.lock || fail "case 1: mcpp.lock does not record A" mcpp.lock -) -echo "ok: case 1 -- root git rev wins over a library's, warned, locked, built against A" - -# ═══════════════════════════════════════════════════════════════════════ -# Case 2: root `path` (dirty working tree), a library `git` -- root wins, -# and what the library actually sees is the WORKING TREE, not a commit. -# ═══════════════════════════════════════════════════════════════════════ -C2="$TMP/case2" -make_lib "$C2/libg" "libg" "$(cat < "$C2/app/mcpp.toml" < "$C2/app/src/main.cpp" <<'EOF' -#include -extern "C" int libg_marker(void); -int main() { - std::printf("libg=%d\n", libg_marker()); - return 0; -} -EOF -( - cd "$C2/app" - ec=0 - "$MCPP" run > run.log 2>&1 || ec=$? - [[ $ec -eq 0 ]] || fail "case 2: exit $ec" run.log - grep -q "libg" run.log || fail "case 2: warning does not name the library" run.log - grep -q "libg=199" run.log \ - || fail "case 2: program did not print the working tree's marker" run.log -) -echo "ok: case 2 -- root path wins over a library's git rev, program sees the working tree" - -# ═══════════════════════════════════════════════════════════════════════ -# Case 3: no root declaration, two libraries at B and C -- the first one -# dequeued (libb, alphabetically before libc) wins, and BOTH edges are -# redirected to it -- libc_marker() must also read B, not C. -# ═══════════════════════════════════════════════════════════════════════ -C3="$TMP/case3" -make_lib "$C3/libb" "libb" "$(cat < "$C3/app/mcpp.toml" <<'EOF' -[package] -name = "app" -version = "0.1.0" - -[build] -sources = ["src/*.cpp"] - -[targets.app] -kind = "bin" -main = "src/main.cpp" - -[dependencies.libb] -path = "../libb" - -[dependencies.libc] -path = "../libc" -EOF -cat > "$C3/app/src/main.cpp" <<'EOF' -#include -extern "C" int libb_marker(void); -extern "C" int libc_marker(void); -int main() { - std::printf("libb=%d libc=%d\n", libb_marker(), libc_marker()); - return 0; -} -EOF -( - cd "$C3/app" - ec=0 - "$MCPP" run > run.log 2>&1 || ec=$? - [[ $ec -eq 0 ]] || fail "case 3: exit $ec" run.log - grep -q "libb" run.log || fail "case 3: warning does not name libb" run.log - grep -q "libc" run.log || fail "case 3: warning does not name libc" run.log - grep -q "libb=102 libc=102" run.log \ - || fail "case 3: both libraries did not converge on the first-resolved framework (B)" run.log -) -echo "ok: case 3 -- with no root opinion, the first-dequeued declaration wins for both edges" - -# ═══════════════════════════════════════════════════════════════════════ -# Case 4: two libraries, one `path` one `git` -- neither is the root, so -# this is refused exactly as a KIND clash always was, plus the new hint. -# ═══════════════════════════════════════════════════════════════════════ -C4="$TMP/case4" -FW_V020_HOST="$(host_path "$FW_V020")" -make_lib "$C4/libd" "libd" "$(cat < "$C4/app/mcpp.toml" <<'EOF' -[package] -name = "app" -version = "0.1.0" - -[build] -sources = ["src/*.cpp"] - -[targets.app] -kind = "bin" -main = "src/main.cpp" - -[dependencies.libd] -path = "../libd" - -[dependencies.libe] -path = "../libe" -EOF -cat > "$C4/app/src/main.cpp" <<'EOF' -int main() { return 0; } -EOF -( - cd "$C4/app" - "$MCPP" build > build.log 2>&1 && ec=0 || ec=$? - [[ $ec -ne 0 ]] || fail "case 4: expected a refusal but the build succeeded" build.log - grep -q "Pick one" build.log || fail "case 4: refusal message missing 'Pick one'" build.log - grep -q "in the root to settle it" build.log \ - || fail "case 4: refusal is missing the new root hint" build.log - grep -q "libd" build.log || fail "case 4: refusal does not name libd" build.log - grep -q "libe" build.log || fail "case 4: refusal does not name libe" build.log -) -echo "ok: case 4 -- a kind clash between two non-root requesters is still refused, with the hint" - -# ═══════════════════════════════════════════════════════════════════════ -# Case 5: the root pins a checkout below a library's SemVer floor -- the -# root still wins the reference, but the floor is checked against the -# checkout's OWN `[package] version` and a violation is refused. -# ═══════════════════════════════════════════════════════════════════════ -C5="$TMP/case5" -INDEX_DIR="$C5/local-index" -INDEX_DIR_HOST="$(host_path "$INDEX_DIR")" -mkdir -p "$INDEX_DIR/pkgs/f" -cat > "$INDEX_DIR/pkgs/f/framework.lua" <<'EOF' -package = { - spec = "1", - name = "framework", - description = "SemVer side of the 661 fixture -- never actually fetched", - licenses = {"MIT"}, - type = "package", - xpm = { - linux = { - ["0.5.0"] = { - url = "https://example.invalid/framework-0.5.0.tar.gz", - sha256 = "0000000000000000000000000000000000000000000000000000000000000000", - }, - }, - }, - mcpp = { - language = "c++23", - import_std = false, - sources = { "src/*.c" }, - targets = { ["framework"] = { kind = "lib" } }, - deps = {}, - }, -} -EOF -make_lib "$C5/libv" "libv" "$(cat <<'EOF' -[dependencies] -framework = ">=0.3" -EOF -)" -mkdir -p "$C5/app/src" -cat > "$C5/app/mcpp.toml" < "$C5/app/src/main.cpp" <<'EOF' -int main() { return 0; } -EOF -( - cd "$C5/app" - "$MCPP" build > build.log 2>&1 && ec=0 || ec=$? - [[ $ec -ne 0 ]] || fail "case 5: expected a refusal but the build succeeded" build.log - grep -q "0.2.0" build.log || fail "case 5: refusal does not name the checkout's version" build.log - grep -q ">=0.3" build.log || fail "case 5: refusal does not name the requirement" build.log - grep -q "app" build.log || fail "case 5: refusal does not name the root" build.log - grep -q "libv" build.log || fail "case 5: refusal does not name the library" build.log -) -echo "ok: case 5 -- a root pin below a library's SemVer floor is refused, naming both" - -# ═══════════════════════════════════════════════════════════════════════ -# Case 6 (negative): the SAME reference declared twice must warn about -# NOTHING, and must not trip `--strict` -- without this row, a fixture -# that warns unconditionally would still pass every row above. -# ═══════════════════════════════════════════════════════════════════════ -C6="$TMP/case6" -make_lib "$C6/libf" "libf" "$(cat < "$C6/app/mcpp.toml" < "$C6/app/src/main.cpp" <<'EOF' -#include -extern "C" int libf_marker(void); -int main() { - std::printf("libf=%d\n", libf_marker()); - return 0; -} -EOF -( - cd "$C6/app" - ec=0 - "$MCPP" build --strict > build.log 2>&1 || ec=$? - [[ $ec -eq 0 ]] || fail "case 6: --strict refused an identical, repeated declaration" build.log - # `grep -q ... && fail ...` would make a NON-match (the success path here) - # the last command's exit status, which -- under `set -e`, outside any - # conditional -- would abort the whole script silently. `if` is what - # keeps a passing check from reading as a script failure. - if grep -q "wins" build.log; then - fail "case 6: an identical reference declared twice still produced an override warning" build.log - fi -) -echo "ok: case 6 -- the same reference declared twice is silent, and --strict stays green" - -echo "OK" diff --git a/tests/e2e/679_a_path_dependency_takes_its_manifests_identity.sh b/tests/e2e/679_a_path_dependency_takes_its_manifests_identity.sh deleted file mode 100755 index 7cc2b118a..000000000 --- a/tests/e2e/679_a_path_dependency_takes_its_manifests_identity.sh +++ /dev/null @@ -1,139 +0,0 @@ -#!/usr/bin/env bash -# 679 -- a `path` or `git` dependency's identity is the one its manifest -# declares, whatever key reached it (#634, A2; SPEC-001 §1.2). -# -# Before, only the short name was compared: `fw` reaching a manifest that -# declares `huxdemo.fw` resolved as `mcpplibs.fw`, and a second edge keyed -# `huxdemo.fw` over the same directory put the same module into the build -# twice, which the scanner then refused naming one file twice. -# -# A bare key of a `path` or `git` dependency states only the short name (rule -# W-b, package-identity.md 4.2): the source fixes the package, so adopting the -# namespace its manifest declares corrects nothing and is not reported. A key -# that WRITES a namespace the manifest contradicts is still reported, once per -# consumer manifest and declared namespace. -# -# Legs: -# A. Two edges over one directory, keyed `huxdemo.fw` (the application) and -# `fw` (a component): the build succeeds, the unit is compiled once, no -# identity warning is written (the bare key states no namespace), and the -# resolution record shows one package with both keys. -# B. Keys that match the declaration warn nothing. -# C. Two keys over a manifest that declares no namespace are two identities -# over one source, and are refused before scanning. -# D. Keys that state a namespace the manifests contradict are reported in one -# warning that names the consumer manifest by its path relative to the -# project root, lists the keys, and gives a hint in TOML. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT - -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } - -mkdir -p "$TMP/fw/src" "$TMP/comp/src" "$TMP/app/src" "$TMP/plain/src" -cat > "$TMP/fw/mcpp.toml" <<'TOML' -[package] -namespace = "huxdemo" -name = "fw" -version = "0.1.0" -[targets.fw] -kind = "lib" -TOML -printf 'export module fw;\nexport int fw_anchor() { return 7; }\n' > "$TMP/fw/src/fw.cppm" -cat > "$TMP/comp/mcpp.toml" <<'TOML' -[package] -namespace = "huxdemo" -name = "comp" -version = "0.1.0" -[targets.comp] -kind = "lib" -[dependencies] -fw = { path = "../fw" } -TOML -printf 'export module comp;\nimport fw;\nexport int comp_anchor() { return fw_anchor(); }\n' > "$TMP/comp/src/comp.cppm" -cat > "$TMP/app/mcpp.toml" <<'TOML' -[package] -name = "app" -version = "0.1.0" -[dependencies] -huxdemo.fw = { path = "../fw" } -huxdemo.comp = { path = "../comp" } -TOML -printf 'import fw;\nimport comp;\nint main() { return fw_anchor() + comp_anchor() == 14 ? 0 : 1; }\n' > "$TMP/app/src/main.cpp" - -# ── A ────────────────────────────────────────────────────────────────────── -( cd "$TMP/app" && "$MCPP" build > build.log 2>&1 ) || fail "A: build failed" "$TMP/app/build.log" -( cd "$TMP/app" && "$MCPP" run > run.log 2>&1 ) || fail "A: the program did not exit 0" "$TMP/app/run.log" -if grep -q "declared identity is used\|that identity is used" "$TMP/app/build.log"; then - fail "A: a bare key produced an identity warning" "$TMP/app/build.log" -fi -units=$(find "$TMP/app/target" -path '*/obj/*' -name 'fw.m.o' | wc -l | tr -d ' ') -[ "$units" = "1" ] || fail "A: fw.cppm compiled $units times" -json=$(find "$TMP/app/target" -name resolution.json | head -1) -keys=$(python3 - "$json" <<'PY' -import json, sys -doc = json.load(open(sys.argv[1])) -fw = [p for p in doc["graph"]["packages"] if p["package"]["name"] == "fw"] -print(len(fw), " ".join(sorted(r["key"] for r in fw[0]["requested_by"])) if fw else "") -PY -) -[ "$keys" = "1 fw huxdemo.fw" ] || fail "A: graph record reads '$keys'" "$json" - -# ── B ────────────────────────────────────────────────────────────────────── -sed 's/^fw = { path/huxdemo.fw = { path/' "$TMP/comp/mcpp.toml" > "$TMP/comp/mcpp.toml.new" -mv "$TMP/comp/mcpp.toml.new" "$TMP/comp/mcpp.toml" -rm -rf "$TMP/app/target" -( cd "$TMP/app" && "$MCPP" build > build2.log 2>&1 ) || fail "B: build failed" "$TMP/app/build2.log" -if grep -q "declared identity is used" "$TMP/app/build2.log"; then - fail "B: matching keys produced an identity warning" "$TMP/app/build2.log" -fi - -# ── C ────────────────────────────────────────────────────────────────────── -mkdir -p "$TMP/nons/fw/src" "$TMP/nons/app/src" -printf '[package]\nname = "fw"\nversion = "0.1.0"\n[targets.fw]\nkind = "lib"\n' > "$TMP/nons/fw/mcpp.toml" -printf 'export module fw;\nexport int fw_anchor() { return 7; }\n' > "$TMP/nons/fw/src/fw.cppm" -printf '[package]\nname = "app"\nversion = "0.1.0"\n[dependencies]\na.fw = { path = "../fw" }\nb.fw = { path = "../fw" }\n' > "$TMP/nons/app/mcpp.toml" -printf 'import fw;\nint main() { return 0; }\n' > "$TMP/nons/app/src/main.cpp" -if ( cd "$TMP/nons/app" && "$MCPP" build > build.log 2>&1 ); then - fail "C: two identities over one source built" "$TMP/nons/app/build.log" -fi -grep -q "one source is reached as two packages" "$TMP/nons/app/build.log" \ - || fail "C: the refusal does not name the two identities" "$TMP/nons/app/build.log" -if grep -q "already provided\|is provided by package" "$TMP/nons/app/build.log"; then - fail "C: the refusal came from the scanner, not before scanning" "$TMP/nons/app/build.log" -fi - -# ── D ────────────────────────────────────────────────────────────────────── -mkdir -p "$TMP/grp/app/src" "$TMP/grp/app/libs/fw/src" "$TMP/grp/app/libs/comp/src" -cp "$TMP/fw/mcpp.toml" "$TMP/grp/app/libs/fw/mcpp.toml" -cp "$TMP/fw/src/fw.cppm" "$TMP/grp/app/libs/fw/src/fw.cppm" -cp "$TMP/comp/mcpp.toml" "$TMP/grp/app/libs/comp/mcpp.toml" -cp "$TMP/comp/src/comp.cppm" "$TMP/grp/app/libs/comp/src/comp.cppm" -sed -i.bak 's|^huxdemo.fw = { path = "../fw" }|fw = { path = "../fw" }|' "$TMP/grp/app/libs/comp/mcpp.toml" -cat > "$TMP/grp/app/mcpp.toml" <<'TOML' -[package] -name = "app" -version = "0.1.0" -[dependencies] -acme.fw = { path = "libs/fw" } -acme.comp = { path = "libs/comp" } -TOML -cp "$TMP/app/src/main.cpp" "$TMP/grp/app/src/main.cpp" -( cd "$TMP/grp/app" && "$MCPP" build > build.log 2>&1 ) || fail "D: build failed" "$TMP/grp/app/build.log" -n=$(grep -c "declared identity is used" "$TMP/grp/app/build.log" || true) -[ "$n" = "1" ] || fail "D: expected one grouped warning, saw $n" "$TMP/grp/app/build.log" -grep "declared identity is used" "$TMP/grp/app/build.log" | grep -q "mcpp.toml names 2 dependencies in namespace acme" \ - || fail "D: the warning does not name the manifest by its relative path and the count" "$TMP/grp/app/build.log" -grep "declared identity is used" "$TMP/grp/app/build.log" | grep -q "declare huxdemo" \ - || fail "D: the warning does not name the declared namespace" "$TMP/grp/app/build.log" -grep "declared identity is used" "$TMP/grp/app/build.log" | grep -q "acme.fw" \ - && grep "declared identity is used" "$TMP/grp/app/build.log" | grep -q "acme.comp" \ - || fail "D: the warning does not list the keys" "$TMP/grp/app/build.log" -grep -q "\[dependencies.huxdemo\] table, for example" "$TMP/grp/app/build.log" \ - || fail "D: the hint is not written in TOML" "$TMP/grp/app/build.log" -if grep -q "libs/comp/mcpp.toml names" "$TMP/grp/app/build.log"; then - fail "D: the bare key inside libs/comp was reported" "$TMP/grp/app/build.log" -fi - -echo "OK" diff --git a/tests/e2e/682_why_deps_prints_the_resolved_graph.sh b/tests/e2e/682_why_deps_prints_the_resolved_graph.sh deleted file mode 100755 index f6f1c147e..000000000 --- a/tests/e2e/682_why_deps_prints_the_resolved_graph.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env bash -# 682 -- `resolution.json` records the resolved dependency graph and -# `mcpp why deps` prints it (#634, X). Before, `why deps` printed only the -# lines of `mcpp.lock`, which does not record `path` dependencies, so a project -# of path dependencies showed "no mcpp.lock" after a successful build. -# -# Legs: -# A. `why deps` lists the path dependency with the key and table that -# requested it and its link form. -# B. The record keeps every field it had: `schema_version` 2, `toolchain`, -# `runtime`, and adds `graph` with the root first. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT - -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } - -mkdir -p "$TMP/fw/src" "$TMP/app/src" -printf '[package]\nnamespace = "demo"\nname = "fw"\nversion = "0.1.0"\n[targets.fw]\nkind = "lib"\n' > "$TMP/fw/mcpp.toml" -printf 'export module fw;\nexport int fw_anchor() { return 41; }\n' > "$TMP/fw/src/fw.cppm" -printf '[package]\nname = "app"\nversion = "0.1.0"\n[dependencies]\ndemo.fw = { path = "../fw" }\n' > "$TMP/app/mcpp.toml" -printf 'import fw;\nint main() { return fw_anchor() == 41 ? 0 : 1; }\n' > "$TMP/app/src/main.cpp" -cd "$TMP/app" - -"$MCPP" build > build.log 2>&1 || fail "build failed" build.log - -# ── A ────────────────────────────────────────────────────────────────────── -"$MCPP" why deps > why.log 2>&1 || fail "A: why deps failed" why.log -grep -q "^dependency graph:" why.log || fail "A: no graph section" why.log -grep -q "demo.fw@0.1.0" why.log || fail "A: the path dependency is not listed" why.log -grep -qF "requested by mcpplibs.app@0.1.0 as 'demo.fw' in [dependencies]" why.log \ - || fail "A: the request is not listed" why.log -grep -q "linked static (default)" why.log || fail "A: the link form is not listed" why.log - -# ── B ────────────────────────────────────────────────────────────────────── -json=$(find target -name resolution.json -not -path '*/.build-mcpp/*' | head -1) -python3 - "$json" <<'PY' || fail "B: record shape" "$json" -import json, sys -doc = json.load(open(sys.argv[1])) -assert doc["schema_version"] == 2, doc["schema_version"] -assert "toolchain" in doc and "runtime" in doc, sorted(doc) -pkgs = doc["graph"]["packages"] -assert pkgs[0]["root"] is True and pkgs[0]["requested_by"] == [], pkgs[0] -fw = [p for p in pkgs if p["package"]["canonical"] == "demo.fw@0.1.0"] -assert len(fw) == 1 and fw[0]["package"]["source"].startswith("path+"), fw -PY - -echo "OK" diff --git a/tests/e2e/713_a_git_dependency_selects_a_repository_member.sh b/tests/e2e/713_a_git_dependency_selects_a_repository_member.sh deleted file mode 100755 index 9b4ebc440..000000000 --- a/tests/e2e/713_a_git_dependency_selects_a_repository_member.sh +++ /dev/null @@ -1,172 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 713 -- a git dependency whose key names a package other than the repository's -# root package selects the `[workspace] members` entry that declares it -# (#649 E7). A second declaration of one dependency by one consumer keeps its -# requests, and the compile banner of a git dependency names its reference. -# -# A framework repository holds the framework, its rules and its tools, and a -# consumer pins it by revision before it is published. The git source always -# yielded the root package, so a key naming a member was either refused as a -# name mismatch or, beside a key naming the root, adopted the root's identity -# with a warning and lost its `tools` request. The same early return dropped -# the requests of a dependency named in `[dependencies]` and again in -# `[build-dependencies]`. -# -# Readings: the member alone builds its tool; the root and the member by the -# same revision build without an identity warning and with the member's tool; -# the banner prints the reference instead of an empty version; a dependency -# declared in both tables gets the tool the second declaration requests. -set -e - -source "$(dirname "$0")/_host_path.sh" - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -export MCPP_HOME="$TMP/mcpphome" -mkdir -p "$MCPP_HOME" -if [ -d "$HOME/.mcpp/registry" ]; then - ln -s "$HOME/.mcpp/registry" "$MCPP_HOME/registry" -fi - -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } - -command -v git >/dev/null || { echo "SKIP: git is not installed"; exit 77; } - -mkdir -p repo/src repo/tool/src -cat > repo/mcpp.toml <<'EOF' -[package] -name = "fw" -namespace = "spike" -version = "0.1.0" - -[targets.fw] -kind = "lib" - -[build] -sources = ["src/*.cpp"] - -[workspace] -members = ["tool"] -EOF -echo 'int fw_answer() { return 42; }' > repo/src/fw.cpp -cat > repo/tool/mcpp.toml <<'EOF' -[package] -name = "fw-installer" -namespace = "spike" -version = "0.1.0" - -[targets.fw-installer] -kind = "bin" -main = "src/main.cpp" - -[dependencies] -spike.fw = { path = ".." } -EOF -cat > repo/tool/src/main.cpp <<'EOF' -#include -int fw_answer(); -int main() { std::printf("fw-installer ran %d\n", fw_answer()); return 0; } -EOF -( - cd repo - git init -q - git -c user.email=e2e@mcpp.dev -c user.name=e2e add -A - git -c user.email=e2e@mcpp.dev -c user.name=e2e commit -qm fixture -) -REV=$(git -C repo rev-parse HEAD) -REPO_HOST=$(host_path "$TMP/repo") - -write_app() { - mkdir -p "$TMP/$1/src" - cat > "$TMP/$1/mcpp.toml" - echo 'int main() { return 0; }' > "$TMP/$1/src/main.cpp" - cat > "$TMP/$1/build.mcpp" <<'EOF' -import std; -import mcpp; -int main() { - const char* a = mcpp::dep_bin("spike.fw-installer", "fw-installer"); - mcpp::warning((std::string("DEPBIN tool=[") + (a ? a : "") + "]").c_str()); - return 0; -} -EOF -} - -# ── 1. the member alone ───────────────────────────────────────────────── -write_app member < b1.log 2>&1 || fail "a git dependency naming a repository member was refused" b1.log -tool=$(grep -o 'DEPBIN tool=\[[^]]*' b1.log | sed 's/DEPBIN tool=\[//') -[ -n "$tool" ] || fail "the member's tool was not provided" b1.log -[ "$("$tool")" = "fw-installer ran 42" ] || fail "the member's tool did not run" - -# ── 2. the root and the member by the same revision ───────────────────── -write_app both < "$TMP/both/src/main.cpp" <<'EOF' -int fw_answer(); -int main() { return fw_answer() == 42 ? 0 : 1; } -EOF -cd "$TMP/both" -"$MCPP" build -v > b2.log 2>&1 || fail "the root and a member by one revision did not build" b2.log -grep -q "declared identity is used" b2.log \ - && fail "the member's key adopted the root's identity" b2.log -grep -q 'DEPBIN tool=\[[^]]' b2.log || fail "the member's tools request was lost" b2.log -grep -qE "Compiling spike\.fw v( |$)" b2.log && fail "the git banner printed an empty version" b2.log -grep -qE "spike\.fw v[^ ]+ \(git rev ${REV:0:12}\)" b2.log \ - || fail "the git banner does not name the reference" b2.log -"$MCPP" run > r2.log 2>&1 || fail "the application over the git root does not run" r2.log - -# ── 3. one dependency in [dependencies] and in [build-dependencies] ───── -mkdir -p "$TMP/local/src" -cp -r "$TMP/repo/tool/." "$TMP/local/" -sed -i.bak 's|spike.fw = { path = ".." }||' "$TMP/local/mcpp.toml" -cat > "$TMP/local/src/main.cpp" <<'EOF' -#include -int main() { std::puts("local tool"); return 0; } -EOF -write_app twice <<'EOF' -[package] -name = "twice" -version = "0.1.0" - -[targets.twice] -kind = "bin" -main = "src/main.cpp" - -[dependencies] -spike.fw-installer = { path = "../local" } - -[build-dependencies] -spike.fw-installer = { path = "../local", tools = ["fw-installer"] } -EOF -cd "$TMP/twice" -"$MCPP" build > b3.log 2>&1 || fail "a dependency declared in two tables did not build" b3.log -grep -q 'DEPBIN tool=\[[^]]' b3.log \ - || fail "the second declaration's tools request was dropped" b3.log - -echo "PASS: 713 a git dependency selects a repository member; a second declaration keeps its requests" diff --git a/tests/e2e/79_gtest_regular_dep_feature_main.sh b/tests/e2e/79_gtest_regular_dep_feature_main.sh deleted file mode 100755 index c55cedd7c..000000000 --- a/tests/e2e/79_gtest_regular_dep_feature_main.sh +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env bash -# requires: -# 79_gtest_regular_dep_feature_main.sh — gtest as a REGULAR dependency -# (`[dependencies.compat]`, via `mcpp add compat.gtest`) must NOT inject gtest_main into a -# `mcpp build` app that has its own main. Regression for issue #168 -# (`gtest_main.o : error LNK2005: main already defined in main.o`). -# -# gtest_main.cc is gated behind the `main` feature in compat.gtest.lua: off by -# default (framework only) → no main collision; `features = ["main"]` opts in. -# -# No `requires:` capability → runs on all three CI platforms (the original bug -# was Windows/MSVC LNK2005). Depends on the published mcpp-index carrying the -# gtest `main` feature. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT - -cd "$TMP" -"$MCPP" new app > /dev/null -cd app - -# (1) #168: gtest in [dependencies] + app's own main → build must succeed, and -# gtest_main must NOT be linked. -"$MCPP" add compat.gtest@1.15.2 > /dev/null -grep -q '^\[dependencies\.compat\]' mcpp.toml || { echo "FAIL: add did not write [dependencies.compat]"; cat mcpp.toml; exit 1; } -"$MCPP" build > /dev/null || { echo "FAIL: #168 — build with regular-dep gtest failed"; exit 1; } -nj=$(find target -name build.ninja -not -path '*/.build-mcpp/*' | xargs ls -t 2>/dev/null | head -1) -if grep -q 'gtest_main' "$nj"; then - echo "FAIL: gtest_main linked into app by default (would collide with main)"; exit 1 -fi -grep -q 'gtest-all' "$nj" || { echo "FAIL: gtest framework (gtest-all) not linked"; exit 1; } - -# (2) opt-in: features = ["main"] + a TEST-only file (no own main) → gtest_main -# IS linked and provides the entry. -cat > mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" - -[dependencies.compat] -gtest = { version = "1.15.2", features = ["main"] } -EOF -cat > src/main.cpp <<'EOF' -#include -TEST(App, ok) { EXPECT_EQ(1 + 1, 2); } -EOF -"$MCPP" build > /dev/null || { echo "FAIL: features=[main] build failed"; exit 1; } -nj=$(find target -name build.ninja -not -path '*/.build-mcpp/*' | xargs ls -t 2>/dev/null | head -1) -grep -q 'gtest_main' "$nj" || { echo "FAIL: features=[main] did not link gtest_main"; exit 1; } - -# (3) `mcpp add --dev` routes to [dev-dependencies]. -cd "$TMP" -"$MCPP" new libapp > /dev/null -cd libapp -"$MCPP" add --dev compat.gtest@1.15.2 > /dev/null -grep -q '^\[dev-dependencies\.compat\]' mcpp.toml || { echo "FAIL: add --dev did not write [dev-dependencies.compat]"; cat mcpp.toml; exit 1; } - -echo "OK" diff --git a/tests/e2e/816_a_git_dependency_reports_its_fetch.sh b/tests/e2e/816_a_git_dependency_reports_its_fetch.sh deleted file mode 100755 index 1996a3614..000000000 --- a/tests/e2e/816_a_git_dependency_reports_its_fetch.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env bash -# requires: unix-shell -# 816_a_git_dependency_reports_its_fetch.sh — fetching a git dependency is -# reported with the renderer every other acquisition uses (W11). -# -# The clone's output used to be captured whole and shown only on failure, so a -# large repository printed nothing until it had arrived. The download phase is -# now drawn as a bar. When stdout is not a terminal the bar is one line when the -# item starts and one line when it finishes, with no carriage return and no -# erase sequence, so a CI log is not filled with repaints. Criteria: -# A. the build prints a `Fetching ` start line and a finish line; -# B. the captured output contains no carriage return and no ESC byte. -set -e - -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -fail() { echo "FAIL: $1"; [ -n "${2:-}" ] && cat "$2"; exit 1; } - -mkdir -p "$TMP/repo/src" && cd "$TMP/repo" -cat > mcpp.toml <<'EOF' -[package] -namespace = "probe816" -name = "gdep" -version = "0.1.0" - -[targets.gdep] -kind = "lib" -EOF -printf 'export module probe816.gdep;\nexport int gdep_value() { return 7; }\n' > src/gdep.cppm -git init -q -b main . && git add -A \ - && git -c user.email=e2e@mcpp -c user.name=e2e commit -qm init - -mkdir -p "$TMP/consumer/src" && cd "$TMP/consumer" -cat > mcpp.toml < src/main.cpp - -"$MCPP" build > build.log 2>&1 || fail "the build failed" build.log -grep -q "Fetching file://$TMP/repo" build.log || fail "A: no start line for the fetch" build.log -grep -q "Fetching file://$TMP/repo.* done" build.log || fail "A: no finish line for the fetch" build.log -if LC_ALL=C grep -q $'\r' build.log; then fail "B: the log carries a carriage return" build.log; fi -if LC_ALL=C grep -q $'\x1b' build.log; then fail "B: the log carries an ESC byte" build.log; fi -echo "ok: A, B" -echo "PASS: 816_a_git_dependency_reports_its_fetch" diff --git a/tests/e2e/821_an_edge_states_its_advice_on_both_paths.sh b/tests/e2e/821_an_edge_states_its_advice_on_both_paths.sh deleted file mode 100755 index 3d6c61643..000000000 --- a/tests/e2e/821_an_edge_states_its_advice_on_both_paths.sh +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/env bash -# requires: unix-shell -# 821_an_edge_states_its_advice_on_both_paths.sh -- SPEC-007 R4.5 (the -# 2026-09-28 design, WS3). A build edge that has something to say on success -# writes it to its advice file under the build directory, and mcpp reports it -# once after a successful build, by one function that the full build path and -# the fast path both call. Before 2026.9.28.2 an edge's output was shown only -# when the build failed or under `-v`, so what a successful edge had to say -# reached nobody; and a report attached to one of the two paths appears or not -# depending on whether build.ninja happened to be current. -# -# The edge here is a `check` action that states a note naming its input's -# contents. `place-dlls` is the engine's own writer of this channel, and it -# runs only for PE programs; the rule is the edge's, not the tool's. -# -# A1 the full path reports the advice once, without -v; -# A2 after the action's input changes, the build takes the fast path (on -# Linux; it serves only ELF products, #400), the edge runs again, and -# the advice is reported once, with the new text; -# A3 a build in which the edge does not run reports nothing. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } -MCPP="${MCPP:-mcpp}" - -mkdir -p p/src p/data -cd p -cat > mcpp.toml <<'EOF' -[package] -name = "advice821" -version = "0.1.0" -EOF -printf 'int main() { return 0; }\n' > src/main.cpp -printf 'first\n' > data/probe.in -cat > build.mcpp <<'EOF' -import std; -import mcpp; -int main() { - const std::string in = std::string(mcpp::manifest_dir()) + "/data/probe.in"; - const std::string out = std::string(mcpp::out_dir()) + "/probe.stamp"; - mcpp::action a; - a.id = "advise"; - a.role = mcpp::roles::check; - // The advice file of this edge, in the build directory the edge runs in. - a.arg("sh").arg("-c") - .arg("mkdir -p .mcpp-advice && printf 'note\\tprobe input reads %s\\n' \"$(cat \"$1\")\" > .mcpp-advice/probe.stamp.advice") - .arg("sh").arg(in.c_str()) - .input(in.c_str()) - .output(out.c_str()) - .submit(); - return 0; -} -EOF - -"$MCPP" build > b1.log 2>&1 || fail "the first build failed" b1.log -[[ "$(grep -c "probe input reads first" b1.log)" -eq 1 ]] \ - || fail "A1: the full path did not report the edge's advice exactly once" b1.log -echo "ok: A1 the full path reports the advice once" - -# Whether the last build planned: the full path writes build.ninja (or moves -# its time when the text is unchanged), and the fast path replays it untouched. -# A `Compiling` line no longer tells the two apart: it states that a package's -# steps ran, on either path (build progress design 2026-09-29). -planned_since() { find target -name build.ninja -not -path '*/.build-mcpp/*' -newer "$1" 2>/dev/null | grep -q .; } -touch .before-b2 -sleep 1 # a coarse file system clock must see the input as newer -printf 'second\n' > data/probe.in -"$MCPP" build > b2.log 2>&1 || fail "the second build failed" b2.log -# The fast path serves only ELF products (#400): on Linux the second build -# must take it, or A2 tests nothing new; elsewhere it declines, and A2 reads -# the full path again. -if planned_since .before-b2; then - case "$(uname -s)" in - Linux) fail "A2: the second build planned again, so the fast path was not exercised" b2.log ;; - *) echo "READING 821: the fast path declines on $(uname -s) (#400); A2 reads the full path" ;; - esac -fi -[[ "$(grep -c "probe input reads second" b2.log)" -eq 1 ]] \ - || fail "A2: the second build did not report the edge's advice exactly once" b2.log -if grep -q "probe input reads first" b2.log; then - fail "A2: the second build reported the previous run's advice" b2.log -fi -echo "ok: A2 the second build reports the advice of the edge that ran" - -"$MCPP" build > b3.log 2>&1 || fail "the third build failed" b3.log -if grep -q "probe input reads" b3.log; then - fail "A3: a build in which the edge did not run reported its advice" b3.log -fi -echo "ok: A3 a build without the edge reports nothing" - -echo "PASS: 821_an_edge_states_its_advice_on_both_paths" diff --git a/tests/e2e/822_a_package_states_the_oldest_mcpp_it_supports.sh b/tests/e2e/822_a_package_states_the_oldest_mcpp_it_supports.sh deleted file mode 100755 index c015eeb20..000000000 --- a/tests/e2e/822_a_package_states_the_oldest_mcpp_it_supports.sh +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env bash -# requires: unix-shell -# resources: machine -# 822_a_package_states_the_oldest_mcpp_it_supports.sh -- #734 E9 and E12. -# -# `[package] mcpp = ">=V"` states the oldest mcpp release a package supports. -# An engine below the floor stops before any other phase, naming the package, -# the floor, its own release and the way to upgrade. The floor is written with -# `>=` only: a bare release means "exactly" elsewhere in mcpp, so it is refused -# with the spelling that is meant. `[workspace.package] mcpp` is inherited by -# members. `[lib]` reports an unknown key, as `[build]` and `[package]` do. -# -# F1 a floor above this release stops the build with the hint; -# F2 a floor at or below this release builds; -# F3 a bare release and a non-release are refused, each with its reason; -# F4 a workspace floor reaches a member built with -p; -# F5 `[lib]` names an unknown key. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } -MCPP="${MCPP:-mcpp}" -HAVE=$("$MCPP" --version | awk '{print $2}') - -mkdir -p p/src -cd p -printf 'int main() { return 0; }\n' > src/main.cpp -manifest() { printf '[package]\nname = "floor822"\nversion = "0.1.0"\nmcpp = %s\n' "$1" > mcpp.toml; } - -# F1 -manifest '">=2999.1.1"' -if "$MCPP" build > f1.log 2>&1; then fail "F1: a floor above $HAVE built" f1.log; fi -grep -q "requires mcpp >= 2999.1.1; this is mcpp $HAVE" f1.log || fail "F1: the message does not name the floor and this release" f1.log -grep -q "xlings install mcpp@2999.1.1" f1.log || fail "F1: the message does not say how to upgrade" f1.log - -# F2 -manifest "\">=$HAVE\"" -"$MCPP" build > f2.log 2>&1 || fail "F2: a floor equal to this release did not build" f2.log -manifest '">=2026.1.1"' -"$MCPP" build > f2b.log 2>&1 || fail "F2: a floor below this release did not build" f2b.log - -# F3 -manifest "\"$HAVE\"" -if "$MCPP" build > f3.log 2>&1; then fail "F3: a bare release was accepted" f3.log; fi -grep -q "names one release exactly; a floor is written \">=$HAVE\"" f3.log || fail "F3: the bare release is not explained" f3.log -manifest '">=banana"' -if "$MCPP" build > f3b.log 2>&1; then fail "F3: a non-release was accepted" f3b.log; fi -grep -q "'banana' is not an mcpp release" f3b.log || fail "F3: the non-release is not explained" f3b.log - -# F4 -cd "$TMP" -mkdir -p ws/app/src -cat > ws/mcpp.toml <<'EOF' -[workspace] -members = ["app"] - -[workspace.package] -version = "0.1.0" -mcpp = ">=2999.1.1" -EOF -printf '[package]\nname = "app822"\n' > ws/app/mcpp.toml -printf 'int main() { return 0; }\n' > ws/app/src/main.cpp -cd ws -if "$MCPP" build -p app > f4.log 2>&1; then fail "F4: the workspace floor did not reach the member" f4.log; fi -grep -q "package 'app822'.*requires mcpp >= 2999.1.1" f4.log || fail "F4: the member is not named with the inherited floor" f4.log - -# F5 -cd "$TMP/p" -printf '[package]\nname = "floor822"\nversion = "0.1.0"\n\n[lib]\npth = "src/x.cppm"\n' > mcpp.toml -"$MCPP" build > f5.log 2>&1 || fail "F5: the build failed" f5.log -grep -q "\[lib\] has unsupported key 'pth' (ignored). Supported keys: path." f5.log || fail "F5: [lib] did not name the unknown key" f5.log - -echo "OK" diff --git a/tests/e2e/844_a_planned_build_keeps_one_lock_entry_per_identity.sh b/tests/e2e/844_a_planned_build_keeps_one_lock_entry_per_identity.sh deleted file mode 100644 index e0a581132..000000000 --- a/tests/e2e/844_a_planned_build_keeps_one_lock_entry_per_identity.sh +++ /dev/null @@ -1,182 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc python3 fresh-sandbox -# 844 -- a planned build of a workspace root keeps ONE mcpp.lock entry per -# identity, spelled as the root's manifest spells it (defect F11). -# -# A workspace root that also has a [package] and declares an index dependency -# under `[dependencies.mcpplibs]` has the map key `mcpplibs.cmdline`, and -# 2026.9.28.3 locked it under that name. Since 2026.9.29.1 a build of the root -# is a plan of a workspace under a virtual root whose own dependency table is -# empty, so the lock name fell through to the qualified name `cmdline` (with a -# hash taken over that spelling), and the partial-workspace merge, which keyed -# prior entries by the spelled name, kept the old entry beside it: two entries -# for one identity, and a committed lock modified by every build. -# -# (a) a lock in the 2026.9.28.3 spelling is left byte-identical; -# (b) a lock that holds both spellings is reduced to one entry; -# (c) a second planned build leaves the result byte-identical. -# -# The dependency is served by a project-local index and pre-extracted into the -# private xlings data directory, so the build touches no network. -set -e - -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -fail() { echo "FAIL: $1"; [ -n "${2:-}" ] && cat "$2"; exit 1; } - -export MCPP_HOME="$TMP/mcpp-home" -source "$(dirname "$0")/_inherit_toolchain.sh" - -mkdir -p "$TMP/ws" && cd "$TMP/ws" - -mkdir -p local-index/pkgs/c -cat > local-index/pkgs/c/cmdline.lua <<'EOF' -package = { - spec = "1", - namespace = "mcpplibs", - name = "cmdline", - description = "fixture for the one-entry-per-identity lock (F11)", - licenses = {"MIT"}, - type = "package", - xpm = { linux = { ["1.0.0"] = { - url = "https://example.invalid/cmdline-1.0.0.tar.gz", - sha256 = "0000000000000000000000000000000000000000000000000000000000000000", - } } }, - mcpp = { - language = "c++23", - import_std = false, - sources = { "src/cmdline.cppm" }, - targets = { ["cmdline"] = { kind = "lib" } }, - deps = {}, - }, -} -EOF -XP=".mcpp/.xlings/data/xpkgs/mcpplibs-x-cmdline/1.0.0" -mkdir -p "$XP/src" -cat > "$XP/src/cmdline.cppm" <<'EOF' -export module fixture.cmdline; -export int cmdline_value() { return 7; } -EOF -printf 'ok\n' > "$XP/.mcpp_ok" - -mkdir -p src m/src -cat > mcpp.toml < src/main.cpp <<'EOF' -import fixture.cmdline; -int main() { return cmdline_value() == 7 ? 0 : 1; } -EOF -cat > m/mcpp.toml <<'EOF' -[package] -name = "m" -version = "0.1.0" - -[targets.m] -kind = "lib" - -[build] -sources = ["src/m.cppm"] -EOF -cat > m/src/m.cppm <<'EOF' -export module m; -export int m_value() { return 1; } -EOF - -# The hash 2026.9.28.3 recorded: FNV-1a over ":@", -# name spelled as the root's map key. -digest() { - python3 - "$1" <<'EOF' -import sys -h = 0xcbf29ce484222325 -for b in sys.argv[1].encode(): - h ^= b - h = (h * 0x100000001b3) & 0xffffffffffffffff -print("fnv1a:%016x" % h) -EOF -} -OLD_HASH=$(digest "mcpplibs:mcpplibs.cmdline@1.0.0") -NEW_HASH=$(digest "mcpplibs:cmdline@1.0.0") - -write_old_lock() { - cat > mcpp.lock < build.log 2>&1 || fail "the planned build did not succeed" build.log -cmp -s lock.before mcpp.lock || { - diff lock.before mcpp.lock || true - fail "(a) a planned build rewrote a lock in the 2026.9.28.3 spelling" -} - -# (b) a lock holding both spellings is reduced to one entry per identity. -cat >> mcpp.lock <> src/main.cpp -"$MCPP" build > build2.log 2>&1 || fail "the second planned build did not succeed" build2.log -[ "$(entries)" = 1 ] || fail "(b) the lock still holds $(entries) entries for one identity" mcpp.lock -cmp -s lock.before mcpp.lock || { - diff lock.before mcpp.lock || true - fail "(b) the surviving entry is not the one 2026.9.28.3 wrote" -} - -# (c) another planned build leaves bytes AND mtime unchanged (#807). -python3 - <<'PYTIME' -import os -os.utime("mcpp.lock", ns=(1000000000000000000, 1000000000000000000)) -open("lock.mtime", "w").write(str(os.stat("mcpp.lock").st_mtime_ns)) -PYTIME -echo "// touched again" >> src/main.cpp -"$MCPP" build > build3.log 2>&1 || fail "the third planned build did not succeed" build3.log -cmp -s lock.before mcpp.lock || fail "(c) a repeated planned build changed the lock" mcpp.lock -python3 - <<'PYTIME' || fail "(c) a repeated planned build rewrote an unchanged lock" -import os -assert os.stat("mcpp.lock").st_mtime_ns == int(open("lock.mtime").read()) -PYTIME - -# --locked reads the same identities. -echo "// and again" >> src/main.cpp -MCPP_LOCKED=1 "$MCPP" build > build4.log 2>&1 || fail "--locked reported drift on an unchanged lock" build4.log - -echo "PASS: 844_a_planned_build_keeps_one_lock_entry_per_identity" diff --git a/tests/e2e/939_a_lock_written_meanwhile_is_not_overwritten.sh b/tests/e2e/939_a_lock_written_meanwhile_is_not_overwritten.sh deleted file mode 100644 index c83cf9b18..000000000 --- a/tests/e2e/939_a_lock_written_meanwhile_is_not_overwritten.sh +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc python3 fresh-sandbox -# 939 -- mcpp.lock is written optimistically (design 2026-10-26 §4.1): no lock -# is held while a build resolves its dependencies, so the write at the end -# compares what is there with what the run read when it started. -# -# A the first build records the index dependency in mcpp.lock; -# B a lock that changes while a build resolves (here: the project's own -# build program rewrites it, between the read and the write) is not -# overwritten; the build stops, naming [lockfile-concurrent] and the -# remedy; -# C run again, the build resolves against the lock as it is now and -# succeeds. -# -# The dependency is served by a project-local index and pre-extracted into the -# private xlings data directory (as 844), so the build touches no network. -set -e - -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } - -export MCPP_HOME="$TMP/mcpp-home" -source "$(dirname "$0")/_inherit_toolchain.sh" - -mkdir -p "$TMP/p" && cd "$TMP/p" - -mkdir -p local-index/pkgs/c -cat > local-index/pkgs/c/cmdline.lua <<'EOF' -package = { - spec = "1", - namespace = "mcpplibs", - name = "cmdline", - description = "fixture for the optimistic lock write", - licenses = {"MIT"}, - type = "package", - xpm = { linux = { ["1.0.0"] = { - url = "https://example.invalid/cmdline-1.0.0.tar.gz", - sha256 = "0000000000000000000000000000000000000000000000000000000000000000", - } } }, - mcpp = { - language = "c++23", - import_std = false, - sources = { "src/cmdline.cppm" }, - targets = { ["cmdline"] = { kind = "lib" } }, - deps = {}, - }, -} -EOF -XP=".mcpp/.xlings/data/xpkgs/mcpplibs-x-cmdline/1.0.0" -mkdir -p "$XP/src" -cat > "$XP/src/cmdline.cppm" <<'EOF' -export module fixture.cmdline; -export int cmdline_value() { return 7; } -EOF -printf 'ok\n' > "$XP/.mcpp_ok" - -mkdir -p src -cat > mcpp.toml <<'EOF' -[package] -name = "p" -version = "0.1.0" - -[indices] -default = { path = "local-index" } - -[dependencies.mcpplibs] -cmdline = "1.0.0" -EOF -cat > src/main.cpp <<'EOF' -import fixture.cmdline; -int main() { return cmdline_value() == 7 ? 0 : 1; } -EOF - -# A -"$MCPP" build > a.log 2>&1 || fail "A: the first build failed" a.log -grep -q 'cmdline' mcpp.lock || fail "A: mcpp.lock does not record the dependency" mcpp.lock - -# B -- the build program runs after the lock is read and before it is written. -cat > build.mcpp <<'EOF' -#include -int main() { - std::ofstream f("mcpp.lock", std::ios::app); - f << "# written by another run\n"; - return f ? 0 : 1; -} -EOF -set +e -"$MCPP" build > b.log 2>&1; rc=$? -set -e -[[ $rc -ne 0 ]] || fail "B: a lock changed meanwhile was overwritten (the build succeeded)" b.log mcpp.lock -grep -q 'lockfile-concurrent' b.log || fail "B: the refusal does not name lockfile-concurrent" b.log -grep -q 'run the command again' b.log || fail "B: the refusal names no remedy" b.log -grep -q '# written by another run' mcpp.lock || fail "B: the other run's lock was overwritten" mcpp.lock - -# C -rm build.mcpp -"$MCPP" build > c.log 2>&1 || fail "C: the build after the concurrent write failed" c.log - -echo "PASS: 939 a lock written meanwhile is not overwritten" diff --git a/tests/e2e/943_a_transitive_git_branch_is_locked.sh b/tests/e2e/943_a_transitive_git_branch_is_locked.sh deleted file mode 100755 index feb819a1b..000000000 --- a/tests/e2e/943_a_transitive_git_branch_is_locked.sh +++ /dev/null @@ -1,104 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 943 -- a git branch reached through another package is locked as the root's -# own are. -# -# mcpp.lock recorded the commit of a branch dependency the ROOT declared, and -# read it back as the dependency's anchor; a branch two edges away was not -# written, so every build asked `git ls-remote` again and moved to the tip, and -# an `--offline` build stopped at it. Measured in mcpp's own CI, whose platform -# member depends on mcpplibs.os by branch and os on openkal by branch: -# -# offline mode: git dependency 'openkal' needs `git ls-remote` ... -# mcpp.lock records no commit for branch 'openkal-0.17.0' -# -# Criteria: -# A. After the first build mcpp.lock holds the transitive branch at the commit -# the build used. -# B. A commit pushed to that branch afterwards is not taken by the next build: -# the program still prints what the locked commit says. (A `file://` -# remote is local, so B rather than `--offline` is what shows the lock line -# is read back as the anchor.) -# C. `mcpp update` drops the recorded commit, and the next build takes the -# branch's tip: the record of the previous build is not replayed under a -# lock it was not planned with (2026.10.19.1 printed the old value on every -# run after the update). -set -e - -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } - -repo() { # repo : a git repository with one commit of what is there - (cd "$1" && git init --quiet -b main && git config user.email t@local \ - && git config user.name t && git add -A && git commit --quiet -m init) -} - -# leaf: the transitive dependency, on branch main. -mkdir -p "$TMP/leaf/src" -cat > "$TMP/leaf/mcpp.toml" <<'TOML' -[package] -name = "leaf" -version = "0.1.0" - -[targets.leaf] -kind = "lib" -TOML -printf 'export module leaf;\nexport int leaf_value() { return 1; }\n' > "$TMP/leaf/src/leaf.cppm" -repo "$TMP/leaf" - -# mid: depends on leaf by branch. -mkdir -p "$TMP/mid/src" -cat > "$TMP/mid/mcpp.toml" < "$TMP/mid/src/mid.cppm" -repo "$TMP/mid" - -# app: depends on mid by branch; leaf is two edges away. -mkdir -p "$TMP/app/src" -cd "$TMP/app" -cat > mcpp.toml <\nint main() { std::printf("value %%d\\n", mid_value()); }\n' > src/main.cpp - -"$MCPP" build > b1.log 2>&1 || fail "the first build" b1.log -LEAF_COMMIT=$(git -C "$TMP/leaf" rev-parse HEAD) - -# A -grep -q "branch=main@$LEAF_COMMIT" mcpp.lock || fail "A: mcpp.lock does not hold the transitive branch's commit" mcpp.lock -echo "ok: the transitive branch is locked at the commit the build used" - -# B -printf 'export module leaf;\nexport int leaf_value() { return 2; }\n' > "$TMP/leaf/src/leaf.cppm" -(cd "$TMP/leaf" && git commit --quiet -am two) -"$MCPP" build > b2.log 2>&1 || fail "B: the second build" b2.log -"$MCPP" run > run.log 2>&1 || fail "B: the program did not run" run.log -grep -q "value 1" run.log || fail "B: the build moved to the branch's new tip" run.log mcpp.lock -echo "ok: a later commit on the branch is not taken" - -# C -"$MCPP" update > update.log 2>&1 || fail "C: mcpp update" update.log -"$MCPP" run > run2.log 2>&1 || fail "C: the program did not run after the update" run2.log -grep -q "value 2" run2.log || fail "C: the build after mcpp update did not take the branch's tip" run2.log -echo "ok: after mcpp update the next build takes the branch's tip" - -echo "PASS: 943 a transitive git branch is locked" diff --git a/tests/e2e/944_a_root_checkout_answers_a_version_the_index_lacks.sh b/tests/e2e/944_a_root_checkout_answers_a_version_the_index_lacks.sh deleted file mode 100755 index dd1b5b07b..000000000 --- a/tests/e2e/944_a_root_checkout_answers_a_version_the_index_lacks.sh +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 944 -- a dependency's request by version is answered by the root's checkout -# of the package, whether or not the index carries it. -# -# The root's declaration of an identity by path or git overrides a -# dependency's request for it by version (the 2026-09-13-630 decision table, -# row 4). The request was first given to the index to select and pin, so a -# package the index did not carry yet -- the case a root pins a checkout for, -# ahead of a release -- refused the build: -# -# error: dependency 'openkal-ld': no package found for exact selector -# -# Measured with openkal-musl's `openkal-ld = "0.1.0"` under `cfg(os = "openkal")` -# against a language server's git checkout of openkal-ld. -# -# Criteria: -# A. A dependency's conditional request by version, for a package no index -# carries, is answered by the root's path checkout under a target selector. -# B. The same by the root's git checkout of a repository member. -# C. A requirement the root's checkout does not satisfy is still refused, -# naming both. -set -e - -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -source "$(dirname "$0")/_host_path.sh" -TMP_HOST="$(host_path "$TMP")" -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } - -case "$(uname -s)" in - Linux) OS=linux ;; - Darwin) OS=macos ;; - *) OS=windows ;; -esac - -# A repository whose member is the package, at a name no index has. -mkdir -p "$TMP/ws/m944leaf/src" -printf '[workspace]\nmembers = ["m944leaf"]\n' > "$TMP/ws/mcpp.toml" -printf '[package]\nname = "m944leaf"\nversion = "0.1.0"\n\n[targets.m944leaf]\nkind = "lib"\n' > "$TMP/ws/m944leaf/mcpp.toml" -printf 'export module m944leaf;\nexport int leaf() { return 1; }\n' > "$TMP/ws/m944leaf/src/leaf.cppm" -(cd "$TMP/ws" && git init --quiet -b main && git config user.email t@local && git config user.name t \ - && git add -A && git commit --quiet -m init) - -# A dependency that asks for it by version, on this system only. -mkdir -p "$TMP/mid/src" -cat > "$TMP/mid/mcpp.toml" < "$TMP/mid/src/mid.cppm" - -app() { # app - mkdir -p "$1/src" - cat > "$1/mcpp.toml" < "$1/src/main.cpp" -} - -# A -app "$TMP/a" "{ path = \"$TMP_HOST/ws/m944leaf\" }" -(cd "$TMP/a" && "$MCPP" build > build.log 2>&1) || fail "A: the root's path checkout did not answer" "$TMP/a/build.log" -grep -q "no package found" "$TMP/a/build.log" && fail "A: the index was asked" "$TMP/a/build.log" -echo "ok: a path checkout answers a conditional request by version" - -# B -app "$TMP/b" "{ git = \"file://$TMP_HOST/ws\", branch = \"main\" }" -(cd "$TMP/b" && "$MCPP" build > build.log 2>&1) || fail "B: the root's git checkout did not answer" "$TMP/b/build.log" -echo "ok: a git checkout of a member answers it" - -# C -sed -i.bak 's/^m944leaf = "0.1.0"/m944leaf = "0.2.0"/' "$TMP/mid/mcpp.toml" -app "$TMP/c" "{ path = \"$TMP_HOST/ws/m944leaf\" }" -if (cd "$TMP/c" && "$MCPP" build > build.log 2>&1); then - fail "C: a requirement the checkout does not satisfy was accepted" "$TMP/c/build.log" -fi -grep -q "requires 0.2.0" "$TMP/c/build.log" || fail "C: the refusal does not name the requirement" "$TMP/c/build.log" -echo "ok: an unsatisfied requirement is still refused" - -echo "PASS: 944 a root checkout answers a version the index lacks" diff --git a/tests/e2e/945_a_miss_anywhere_in_the_graph_asks_for_a_refresh.sh b/tests/e2e/945_a_miss_anywhere_in_the_graph_asks_for_a_refresh.sh deleted file mode 100755 index 0b5d11dd6..000000000 --- a/tests/e2e/945_a_miss_anywhere_in_the_graph_asks_for_a_refresh.sh +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc fresh-sandbox -# 945 -- a request by version that the local index cannot answer asks for a -# refresh wherever it is in the graph, not only in the root's [dependencies]. -# -# The refresh before the dependency walk (#315) asked about the root's and the -# selected members' [dependencies]; a package reached by `path` that named a -# version the index did not yet carry refused the build with no refresh at all. -# Measured in mcpp's own CI: modules/platform named `os = "0.1.0"` minutes -# after the index published it, and a sandbox index "never refreshed" answered -# -# error: dependency 'mcpplibs.os': no package found for exact selector -# -# Hermetic, as 173 is: the builtin index is fabricated, and every case is -# offline or debounced, so the decision is observed without a sync. -# -# Criteria: -# A. Offline, the miss in a path dependency reaches the refresh policy, which -# declines for offline mode and says so for that package. -# B. Inside the debounce window, the same miss is declined as "just -# refreshed": the policy, not the position of the edge, decides. -# C. The refusal that follows still names the selector and the index. -set -e - -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } - -export MCPP_HOME="$TMP/mcpp-home" -source "$(dirname "$0")/_inherit_toolchain.sh" - -INDEX="$MCPP_HOME/indices/mcpplibs" -mkdir -p "$INDEX/pkgs/w" -echo "abc1234" > "$INDEX/.xlings-index-version" -: > "$INDEX/.mcpp-index-updated" -touch -d '30 days ago' "$INDEX/.mcpp-index-updated" 2>/dev/null \ - || touch -A -300000 "$INDEX/.mcpp-index-updated" 2>/dev/null || true - -# mid: reached by path; it names a package the local index does not carry. -mkdir -p "$TMP/mid/src" -cat > "$TMP/mid/mcpp.toml" <<'TOML' -[package] -name = "mid" -version = "0.1.0" - -[targets.mid] -kind = "lib" - -[dependencies] -absentpkg = "1.0.0" -TOML -printf 'export module mid;\nexport int mid_value() { return 1; }\n' > "$TMP/mid/src/mid.cppm" - -mkdir -p "$TMP/app/src" -cd "$TMP/app" -cat > mcpp.toml <<'TOML' -[package] -name = "app" -version = "0.1.0" - -[targets.app] -kind = "bin" -main = "src/main.cpp" - -[dependencies] -mid = { path = "../mid" } -TOML -echo 'int main() { return 0; }' > src/main.cpp - -# A -MCPP_VERBOSE=1 MCPP_OFFLINE=1 "$MCPP" build > a.log 2>&1 && fail "A: the build resolved a package no index carries" a.log -grep -qE "skip refresh for (mcpplibs[.:])?absentpkg: offline mode" a.log \ - || fail "A: the miss in a path dependency did not reach the refresh policy" a.log -echo "ok: a miss in a path dependency asks for a refresh (declined offline)" - -# B -: > "$INDEX/.mcpp-index-updated" -MCPP_VERBOSE=1 "$MCPP" build > b.log 2>&1 && fail "B: the build resolved a package no index carries" b.log -grep -qE "skip refresh for (mcpplibs[.:])?absentpkg: index was just refreshed" b.log \ - || fail "B: the debounce did not decide the miss in a path dependency" b.log -grep -qE "Refreshing +package index" b.log && fail "B: a sync inside the debounce window" b.log -echo "ok: inside the debounce window the same miss is declined" - -# C -grep -q "dependency 'absentpkg': no package found for exact selector" b.log \ - || fail "C: the refusal does not name the selector" b.log -grep -q "index: local index abc1234" b.log || fail "C: the refusal does not name the index" b.log -echo "ok: the refusal names the selector and the index" - -echo "PASS: 945 a miss anywhere in the graph asks for a refresh" diff --git a/tests/e2e/946_a_program_begins_in_a_dependency_entry.sh b/tests/e2e/946_a_program_begins_in_a_dependency_entry.sh deleted file mode 100755 index db3329f08..000000000 --- a/tests/e2e/946_a_program_begins_in_a_dependency_entry.sh +++ /dev/null @@ -1,204 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc elf -# 946 -- a program's beginning is a dependency's `kind = "entry"` target -# (design 2026-10-26 §24, mcpp#845). -# -# A framework supplies `main`; the application writes only the callback the -# framework declares. Before, `main` named a file of the program's own package, -# and every application carried the same three-line file. -# -# Criteria: -# A. `main = { from = "fw/player" }` builds a program that begins in fw's -# entry, compiled with fw's flags, and reaches the callback the program -# defines. -# B. The entry is in no other image: fw built as the root links its library, -# and a second program with its own `main` over fw links. -# C. An entry whose `required_features` are inactive is refused naming the -# feature and the dependency line; with the feature it builds. -# D. An unknown dependency key and a target that is not an entry are refused -# with the candidates; flags for the program's own entry beside `from` are -# refused. -# E. `windows_entry` follows the entry, and a program stating another one is -# refused naming both. -# F. A row replaces the entry's file on the rows it matches. -# G. `[test] main = { from }`: test files without `main`, the package's own -# entry named by its own name. -# H. `start` on a hosted target is refused, naming the C runtime. -# I. The result record names where the program begins. -set -e - -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } - -# fw: a library, an entry that calls the application's callback, and an entry -# that needs a feature. -mkdir -p "$TMP/fw/src/entry" "$TMP/fw/tests" -cat > "$TMP/fw/mcpp.toml" <<'TOML' -[package] -name = "fw" -version = "0.1.0" - -[features] -app = {} - -[targets.fw] -kind = "lib" - -[targets.player] -kind = "entry" -main = "src/entry/player.cpp" -defines = ["FW_PLAYER"] - -[targets.app] -kind = "entry" -main = "src/entry/app.cpp" -required_features = ["app"] - -[target.'cfg(os = "linux")'.targets.app] -main = "src/entry/app_linux.cpp" - -[test] -main = { from = "fw/player" } -TOML -cat > "$TMP/fw/src/fw.cppm" <<'CPP' -export module fw; -import std; -// The callback an application defines. Declared with C++ language linkage at -// global scope: a declaration attached to this module could not be defined -// outside it. -export extern "C++" int fw_app(); -export namespace fw { -int twice(int v) { return 2 * v; } -} -CPP -cat > "$TMP/fw/src/entry/player.cpp" <<'CPP' -import std; -import fw; -#ifndef FW_PLAYER -#error the entry is compiled with its own target's defines -#endif -int main() { - std::println("player {}", fw::twice(fw_app())); - return 0; -} -CPP -cat > "$TMP/fw/src/entry/app.cpp" <<'CPP' -import std; -import fw; -int main() { std::println("app {}", fw_app()); return 0; } -CPP -cat > "$TMP/fw/src/entry/app_linux.cpp" <<'CPP' -import std; -import fw; -int main() { std::println("app-linux {}", fw_app()); return 0; } -CPP -cat > "$TMP/fw/tests/callback.cpp" <<'CPP' -int fw_app() { return 21; } -CPP - -# notes: the application, which writes only the callback. -mkdir -p "$TMP/notes/src" -cat > "$TMP/notes/src/app.cpp" <<'CPP' -int fw_app() { return 21; } -CPP -FW_DEP='fw = { path = "../fw" }' -notes() { # notes [dependency line] - local dep="${2:-$FW_DEP}" - cat > "$TMP/notes/mcpp.toml" < a.log 2>&1 || fail "A: the program over fw's entry did not build" a.log -"$MCPP" run > a-run.log 2>&1 || fail "A: the program did not run" a-run.log -grep -q "player 42" a-run.log || fail "A: the program did not begin in fw's entry with its flags" a-run.log -echo "ok: a program begins in a dependency's entry and reaches its own callback" - -# B -(cd "$TMP/fw" && "$MCPP" build > "$TMP/b-fw.log" 2>&1) || fail "B: fw as the root did not build" "$TMP/b-fw.log" -mkdir -p "$TMP/other/src" -cat > "$TMP/other/mcpp.toml" <<'TOML' -[package] -name = "other" -version = "0.1.0" - -[dependencies] -fw = { path = "../fw" } -TOML -printf 'import std;\nimport fw;\nint fw_app() { return 1; }\nint main() { std::println("own {}", fw::twice(3)); }\n' > "$TMP/other/src/main.cpp" -(cd "$TMP/other" && "$MCPP" run > "$TMP/b-other.log" 2>&1) || fail "B: a program with its own main over fw did not link" "$TMP/b-other.log" -grep -q "own 6" "$TMP/b-other.log" || fail "B: the program with its own main did not run" "$TMP/b-other.log" -echo "ok: the entry is in no image that does not select it" - -# C -notes 'main = { from = "fw/app" }' -"$MCPP" build > c.log 2>&1 && fail "C: an entry with an inactive required feature was linked" c.log -grep -q 'requires the feature "app"' c.log || fail "C: the refusal does not name the feature" c.log -grep -q 'fw = { ..., features = \["app"\] }' c.log || fail "C: the refusal does not give the dependency line" c.log -notes 'main = { from = "fw/app" }' 'fw = { path = "../fw", features = ["app"] }' -"$MCPP" run > c-run.log 2>&1 || fail "C: the entry with its feature did not build" c-run.log -echo "ok: an entry's required features gate its selection" - -# D -notes 'main = { from = "nope/player" }' -"$MCPP" build > d1.log 2>&1 && fail "D: an unknown dependency was accepted" d1.log -grep -q "'nope' is neither a dependency of 'notes' nor its own name (its dependencies: fw)" d1.log \ - || fail "D: the unknown dependency is not named with the candidates" d1.log -notes 'main = { from = "fw/fw" }' -"$MCPP" build > d2.log 2>&1 && fail "D: a library target was accepted as an entry" d2.log -grep -q "declares no entry target 'fw' (its \`kind = \"entry\"\` targets: app, player)" d2.log \ - || fail "D: the entry targets are not listed" d2.log -notes 'main = { from = "fw/player" } -defines = ["X=1"]' -"$MCPP" build > d3.log 2>&1 && fail "D: flags for the program's own entry were accepted beside from" d3.log -grep -q "targets.notes.defines applies to the target's own entry translation unit" d3.log \ - || fail "D: the flags refusal does not say why" d3.log -echo "ok: a selection that names nothing is refused with the candidates" - -# E -sed -i 's/^defines = \["FW_PLAYER"\]/&\nwindows_entry = "main"/' "$TMP/fw/mcpp.toml" -notes 'main = { from = "fw/player" } -windows_entry = "wmain"' -"$MCPP" build > e.log 2>&1 && fail "E: two entry functions were accepted" e.log -grep -q 'windows_entry = "wmain", and its beginning .fw/player. defines `main`' e.log \ - || fail "E: the conflict does not name both statements" e.log -echo "ok: the entry function follows the entry" - -# F -notes 'main = { from = "fw/app" }' 'fw = { path = "../fw", features = ["app"] }' -grep -q "app-linux 21" c-run.log || fail "F: the row did not replace the entry's file" c-run.log -echo "ok: a row replaces the entry's file" - -# G -(cd "$TMP/fw" && "$MCPP" test > "$TMP/g.log" 2>&1) || fail "G: the test over the package's own entry failed" "$TMP/g.log" -grep -q "callback" "$TMP/g.log" || fail "G: the test did not run" "$TMP/g.log" -echo "ok: test programs begin in an entry named by the package's own name" - -# H -notes 'main = { from = "fw/player" } -start = "_start"' -"$MCPP" build > h.log 2>&1 && fail "H: start on a hosted target was accepted" h.log -grep -q "the image is entered by the C runtime" h.log || fail "H: the refusal does not name the C runtime" h.log -echo "ok: start on a hosted target is refused" - -# I -notes 'main = { from = "fw/player" }' -"$MCPP" build --message-format json > i.json 2>/dev/null || fail "I: the json build failed" i.json -grep -q '"entry":{"from":"fw/player"}' i.json || fail "I: the result record does not name where the program begins" i.json -echo "ok: the result record names the program's beginning" - -echo "PASS: 946 a program begins in a dependency entry" diff --git a/tests/e2e/948_an_upgraded_dependency_is_restaged_at_the_same_size.sh b/tests/e2e/948_an_upgraded_dependency_is_restaged_at_the_same_size.sh deleted file mode 100755 index d71422d3e..000000000 --- a/tests/e2e/948_an_upgraded_dependency_is_restaged_at_the_same_size.sh +++ /dev/null @@ -1,125 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc fresh-sandbox -# 948_an_upgraded_dependency_is_restaged_at_the_same_size.sh — a dependency -# upgraded to a version the build cache already holds is staged over the -# previous version's files, whatever their size. -# -# A package's BMI and objects land at paths named by the module and the source, -# not by the cache entry, so after an upgrade the previous version's files are -# where the new ones go. Staging under `--verify size` took an equal size for an -# equal file. A change of a constant keeps both sizes, so the previous BMI and -# object stayed: the program printed the old value, and a unit compiled against -# the new graph met the old BMI ("CRC mismatch"). -set -e -source "$(dirname "$0")/_host_path.sh" - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT - -export MCPP_HOME="$TMP/mcpp-home" -source "$(dirname "$0")/_inherit_toolchain.sh" - -INDEX_DIR="$TMP/local-index" -INDEX_DIR_HOST="$(host_path "$INDEX_DIR")" -mkdir -p "$INDEX_DIR/pkgs/c" -cat > "$INDEX_DIR/pkgs/c/constant.lua" <<'EOF' -package = { - spec = "1", - name = "constant", - description = "One constant", - licenses = {"MIT"}, - type = "package", - xpm = { - linux = { - ["1.0.0"] = { - url = "https://example.invalid/constant-1.0.0.tar.gz", - sha256 = "0000000000000000000000000000000000000000000000000000000000000000", - }, - ["1.0.1"] = { - url = "https://example.invalid/constant-1.0.1.tar.gz", - sha256 = "0000000000000000000000000000000000000000000000000000000000000000", - }, - }, - }, - mcpp = { - language = "c++23", - import_std = true, - sources = { "src/**/*.cppm" }, - targets = { ["constant"] = { kind = "lib" } }, - deps = {}, - }, -} -EOF - -# `make_project `: a program printing the constant of `version`, -# whose payloads hold 41 (1.0.0) and 42 (1.0.1) --- one BMI size, one object size. -make_project() { - local dir="$1" version="$2" - mkdir -p "$TMP/$dir/src" - for v in 1.0.0:41 1.0.1:42; do - local d="$TMP/$dir/.mcpp/.xlings/data/xpkgs/local-dev.constant/${v%%:*}/src" - mkdir -p "$d" - cat > "$d/constant.cppm" < "$TMP/$dir/src/main.cpp" <<'EOF' -import std; -import constant; -int main() { std::println("{} {}", value, stated()); return 0; } -EOF - cat > "$TMP/$dir/mcpp.toml" < build.log 2>&1) || { cat "$TMP/omega/build.log"; exit 1; } -run_prints omega "42 42" - -# This project builds 1.0.0, then moves to 1.0.1: the staged files replace the -# ones 1.0.0 left at the same paths. -make_project alpha 1.0.0 -cd "$TMP/alpha" -"$MCPP" build > build.log 2>&1 || { cat build.log; exit 1; } -run_prints alpha "41 41" -bmi="$(find target -path '*gcm.cache*' -name 'constant.gcm' | head -1)" -[[ -n "$bmi" ]] || { echo "FAIL: no BMI of the dependency in the build directory"; exit 1; } -before="$(wc -c < "$bmi")" - -sed -i.bak 's/"local-dev.constant" = "1.0.0"/"local-dev.constant" = "1.0.1"/' mcpp.toml -rm -f mcpp.toml.bak -"$MCPP" build > upgrade.log 2>&1 || { cat upgrade.log; exit 1; } -grep -qE 'Cached local-dev\.constant v1\.0\.1' upgrade.log || { - echo "FAIL: 1.0.1 was not served from the cache, so nothing here is staged" - cat upgrade.log; exit 1 -} -[[ "$(wc -c < "$bmi")" == "$before" ]] || { echo "FAIL: the BMI's size changed; the case is not the same-size one"; exit 1; } -run_prints alpha "42 42" -cmp -s "$bmi" "$(find "$MCPP_HOME/build-cache" -path '*constant@1.0.1*' -name 'constant.gcm' | head -1)" || { - echo "FAIL: the staged BMI is not 1.0.1's"; exit 1 -} - -echo "OK" diff --git a/tests/e2e/980_an_ambiguous_dependency_name_is_no_packages.sh b/tests/e2e/980_an_ambiguous_dependency_name_is_no_packages.sh deleted file mode 100755 index de8724f05..000000000 --- a/tests/e2e/980_an_ambiguous_dependency_name_is_no_packages.sh +++ /dev/null @@ -1,91 +0,0 @@ -#!/usr/bin/env bash -# requires: elf gcc -# #852: two dependencies whose manifests both say `rules` (namespaces openmui -# and gongfu). The bare name is no package's: it is not published, the build -# does not warn on every run, MCPP_DEP_AMBIGUOUS_RULES names the qualified -# names, and each qualified name still reaches its own directory. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -for ns in openmui gongfu; do - mkdir -p "$ns-rules/src" - echo "int ${ns}_rules_touch() { return 1; }" > "$ns-rules/src/r.cpp" - cat > "$ns-rules/mcpp.toml" < app/src/main.cpp <<'EOF' -import std; -extern const char* seen(); -int main() { std::println("{}", seen()); } -EOF -cat > app/build.mcpp <<'EOF' -#include -#include -#include -import mcpp; -int main() { - auto env = [](const char* n) { const char* v = std::getenv(n); return std::string(v ? v : ""); }; - std::string line = "bare=" + env("MCPP_DEP_RULES_DIR") - + " ambiguous=" + env("MCPP_DEP_AMBIGUOUS_RULES") - + " openmui=" + env("MCPP_DEP_OPENMUI_RULES_DIR") - + " gongfu=" + env("MCPP_DEP_GONGFU_RULES_DIR"); - std::string out = "src/seen.cpp"; - std::FILE* f = std::fopen(out.c_str(), "w"); - std::fprintf(f, "const char* seen() { return R\"(%s)\"; }\n", line.c_str()); - std::fclose(f); - mcpp::generated("src/seen.cpp"); - return 0; -} -EOF -cat > app/mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" -[modules] -sources = ["src/**/*.cpp"] -[dependencies] -openmui.rules = { path = "../openmui-rules" } -gongfu.rules = { path = "../gongfu-rules" } -[targets.app] -kind = "bin" -main = "src/main.cpp" -EOF - -cd app -"$MCPP" build > build.log 2>&1 || { cat build.log; echo "FAIL: build failed"; exit 1; } -if grep -q "collides on" build.log; then - cat build.log; echo "FAIL: an ambiguous name still warns"; exit 1 -fi -seen="$("$MCPP" run 2>/dev/null | tail -1)" -echo "$seen" -[[ "$seen" == *"bare="* ]] || { echo "FAIL: the bare name is bound"; exit 1; } -[[ "$seen" == *"ambiguous=gongfu.rules,openmui.rules"* || "$seen" == *"ambiguous=openmui.rules,gongfu.rules"* ]] \ - || { echo "FAIL: MCPP_DEP_AMBIGUOUS_RULES does not name both"; exit 1; } -[[ "$seen" == *"openmui="*"openmui-rules"* ]] || { echo "FAIL: openmui.rules lost its directory"; exit 1; } -[[ "$seen" == *"gongfu="*"gongfu-rules"* ]] || { echo "FAIL: gongfu.rules lost its directory"; exit 1; } - -# A build program that asks for the ambiguous name ends, naming both. -cat > build.mcpp <<'EOF' -import mcpp; -int main() { return mcpp::dep_dir("rules")[0] ? 0 : 1; } -EOF -rm -f src/seen.cpp -if "$MCPP" build > ask.log 2>&1; then cat ask.log; echo "FAIL: dep_dir(\"rules\") answered"; exit 1; fi -grep -q 'dep_dir("rules") names more than one dependency' ask.log \ - || { cat ask.log; echo "FAIL: the program did not say the name is ambiguous"; exit 1; } -grep -q 'openmui.rules' ask.log && grep -q 'gongfu.rules' ask.log \ - || { cat ask.log; echo "FAIL: the refusal does not name both packages"; exit 1; } -echo "OK" diff --git a/tests/e2e/990_a_lock_key_does_not_depend_on_the_selected_members.sh b/tests/e2e/990_a_lock_key_does_not_depend_on_the_selected_members.sh deleted file mode 100755 index d710bc247..000000000 --- a/tests/e2e/990_a_lock_key_does_not_depend_on_the_selected_members.sh +++ /dev/null @@ -1,124 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc fresh-sandbox -# 990 -- a lock entry's key and hash do not depend on which members a command -# plans (#868). -# -# Two members take one workspace dependency under two spellings: `a` writes -# `widget2.workspace = true`, `c` writes `[dependencies.mcpplibs] -# widget2.workspace = true`. The key was the spelling of a selected member, so -# `-p a` wrote `[package."widget2"]` and `-p c` `[package."mcpplibs.widget2"]`, -# each with its own hash (the hash is taken over the key), and the lock changed -# between commands with no change to a manifest. -# -# Criteria: `--workspace`, `-p a` and `-p c` write the same key and hash; it is -# the workspace's own spelling (`[workspace.dependencies] widget2`), which -# precedes every member's. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT - -export MCPP_HOME="$TMP/mcpp-home" -source "$(dirname "$0")/_inherit_toolchain.sh" - -mkdir -p "$TMP/ws" -cd "$TMP/ws" - -# A package of the default namespace, from a project-local index, with its -# payload already in place: nothing is fetched. -mkdir -p local-index/pkgs/w .mcpp/.xlings/data/xpkgs/mcpplibs.widget2/1.0.0/src -cat > local-index/pkgs/w/widget2.lua <<'EOF' -package = { - spec = "1", - namespace = "mcpplibs", - name = "widget2", - description = "a package of the default namespace from a local index", - licenses = {"MIT"}, - type = "package", - xpm = { - linux = { ["1.0.0"] = { url = "https://example.invalid/w.tar.gz" } }, - macosx = { ["1.0.0"] = { url = "https://example.invalid/w.tar.gz" } }, - windows = { ["1.0.0"] = { url = "https://example.invalid/w.zip" } }, - }, - mcpp = { - language = "c++23", - import_std = false, - sources = { "src/widget2.cppm" }, - targets = { ["widget2"] = { kind = "lib" } }, - deps = {}, - }, -} -EOF -printf 'export module widget2;\nexport int widget2_value() { return 35; }\n' \ - > .mcpp/.xlings/data/xpkgs/mcpplibs.widget2/1.0.0/src/widget2.cppm - -cat > mcpp.toml <<'EOF' -[workspace] -members = ["a", "c"] - -[workspace.indices] -mcpplibs = { path = "local-index" } - -[workspace.dependencies] -widget2 = "1.0.0" -EOF - -mkdir -p a/src c/src -cat > a/mcpp.toml <<'EOF' -[package] -name = "a" -version = "0.1.0" - -[dependencies] -widget2.workspace = true - -[targets.a] -kind = "lib" -EOF -printf 'export module a;\nimport widget2;\nexport int a_value() { return widget2_value() + 7; }\n' > a/src/a.cppm - -cat > c/mcpp.toml <<'EOF' -[package] -name = "c" -version = "0.1.0" - -[dependencies.mcpplibs] -widget2.workspace = true - -[targets.c] -kind = "bin" -main = "src/main.cpp" -EOF -printf 'import widget2;\nint main() { return widget2_value() == 35 ? 0 : 1; }\n' > c/src/main.cpp - -entry() { # the lock's widget2 entry: its header and its hash - grep -E '^\[package\..*widget2"\]' mcpp.lock - awk '/widget2"\]/{f=1} f && /^hash/{print; exit}' mcpp.lock -} - -declare -A seen -for sel in "--workspace" "-p a" "-p c"; do - rm -f mcpp.lock - # shellcheck disable=SC2086 - "$MCPP" build $sel --configure-only > "build.log" 2>&1 || { - cat build.log - echo "FAIL: mcpp build $sel --configure-only" - exit 1 - } - seen[$sel]="$(entry)" -done - -for sel in "--workspace" "-p a" "-p c"; do - [[ "${seen[$sel]}" == "${seen[--workspace]}" ]] || { - for s in "--workspace" "-p a" "-p c"; do echo "$s: ${seen[$s]}"; done - echo "FAIL: the lock entry of widget2 depends on the members a command plans" - exit 1 - } -done -grep -q '^\[package\."widget2"\]' <<<"${seen[--workspace]}" || { - echo "${seen[--workspace]}" - echo "FAIL: the key is not the workspace's own spelling (widget2)" - exit 1 -} - -echo "OK" diff --git a/tests/e2e/MIGRATION.tsv b/tests/e2e/MIGRATION.tsv index 08bef097c..55181b2a9 100644 --- a/tests/e2e/MIGRATION.tsv +++ b/tests/e2e/MIGRATION.tsv @@ -8,10 +8,10 @@ script subsystem class destination note 06_emit_xpkg.sh index sink unit/test_emit_xpkg answers a parse/selection rule without a build: Lua entry from a manifest 07_static_library.sh build merge e2e/build 08_shared_library.sh build merge e2e/build -09_path_dependency.sh deps merge e2e/deps +09_path_dependency.sh deps merge e2e/deps::path/09 path dependency migrated (batch 3); script removed 10_env_command.sh self merge e2e/self 11_index_list.sh index merge e2e/index -12_add_command.sh deps merge e2e/deps +12_add_command.sh deps merge e2e/deps::path/12 add command migrated (batch 3); script removed 13_toolchain_pin.sh toolchain merge e2e/toolchain 14_toolchain_fallback.sh toolchain merge e2e/toolchain 15_test_passing.sh test merge e2e/test::report/15 test passing migrated (batch 1); script removed @@ -23,19 +23,19 @@ script subsystem class destination note 21_ninja_dyndep.sh modules merge e2e/modules 22_doctor_cache_publish.sh index merge e2e/index 23_remove_update.sh deps sink unit/test_manifest_edit answers a parse/selection rule without a build: remove/update rewrite mcpp.toml -24_git_dependency.sh deps merge e2e/deps +24_git_dependency.sh deps merge e2e/deps::git/24 git dependency migrated (batch 3); script removed 25_convention_mode.sh build merge e2e/build 26_c_language_support.sh build merge e2e/build 26_toolchain_management.sh toolchain merge e2e/toolchain -27_namespace_dependencies.sh deps merge e2e/deps +27_namespace_dependencies.sh deps merge e2e/deps::path/27 namespace dependencies migrated (batch 3); script removed 27_self_contained_home.sh self keep keep home located from the installed binary's own directory (/bin/mcpp): a release-layout install 28_target_static.sh target merge e2e/target 29_toolchain_partial_versions.sh toolchain merge e2e/toolchain 30_dev_binary_home.sh self keep keep a dev binary's home when MCPP_HOME is unset: needs the real ~/.mcpp of the machine 30_pack_modes.sh pack merge e2e/pack -31_transitive_deps.sh deps merge e2e/deps -32_semver_merge.sh deps merge e2e/deps -33_multi_version_mangling.sh deps merge e2e/deps +31_transitive_deps.sh deps merge e2e/deps::path/31 transitive deps migrated (batch 3); script removed +32_semver_merge.sh deps merge e2e/deps::resolve/32 semver merge migrated (batch 3); script removed +33_multi_version_mangling.sh deps merge e2e/deps::resolve/33 multi version mangling migrated (batch 3); script removed 35_workspace.sh workspace merge e2e/workspace 36_llvm_toolchain.sh toolchain merge e2e/toolchain 37_llvm_import_std.sh toolchain merge e2e/toolchain @@ -65,10 +65,10 @@ script subsystem class destination note 58_preinstall_mcpp_deps_for_hooks.sh build-program merge e2e/build-program::deps/58 preinstall mcpp deps for hooks migrated (batch 2); script removed 59_cpp_standard_config.sh toolchain merge e2e/toolchain 60_stale_xpkg_cache_reinstall.sh index merge e2e/index -61_dependency_visibility.sh deps merge e2e/deps -62_dotted_dependency_selector_priority.sh deps merge e2e/deps +61_dependency_visibility.sh deps merge e2e/deps::resolve/61 dependency visibility migrated (batch 3); script removed +62_dotted_dependency_selector_priority.sh deps merge e2e/deps::resolve/62 dotted dependency selector priority migrated (batch 3); script removed 62_runtime_library_dirs.sh run merge e2e/run -63_bare_dependency_peer_root_priority.sh deps merge e2e/deps +63_bare_dependency_peer_root_priority.sh deps merge e2e/deps::resolve/63 bare dependency peer root priority migrated (batch 3); script removed 64_shared_soname_runtime_alias.sh runtime merge e2e/runtime 65_toolchain_runtime_dirs_for_run.sh run merge e2e/run 66_runtime_provides.sh runtime merge e2e/runtime @@ -85,7 +85,7 @@ script subsystem class destination note 76_qualified_custom_ns_noncanonical_filename.sh index merge e2e/index 77_cdb_preserves_test_entries.sh cdb merge e2e/cdb 78_test_main_combinations.sh test merge e2e/test::graph/78 test main combinations migrated (batch 1); script removed -79_gtest_regular_dep_feature_main.sh deps merge e2e/deps +79_gtest_regular_dep_feature_main.sh deps merge e2e/deps::resolve/79 gtest regular dep feature main migrated (batch 3); script removed 80_feature_defines.sh features merge e2e/features 81_capability_binding.sh features merge e2e/features 82_feature_optional_deps.sh features merge e2e/features @@ -167,11 +167,11 @@ script subsystem class destination note 158_test_signal_and_duration.sh test merge e2e/test::report/158 test signal and duration migrated (batch 1); script removed 159_test_list.sh test merge e2e/test::selection/159 test list migrated (batch 1); script removed 160_test_timeout.sh test merge e2e/test::report/160 test timeout migrated (batch 1); script removed -161_xpkg_name_form.sh deps merge e2e/deps -162_bare_name_namespace_scope.sh deps merge e2e/deps -163_identity_first_resolution.sh deps merge e2e/deps +161_xpkg_name_form.sh deps merge e2e/deps::names/161 xpkg name form migrated (batch 3); script removed +162_bare_name_namespace_scope.sh deps merge e2e/deps::names/162 bare name namespace scope migrated (batch 3); script removed +163_identity_first_resolution.sh deps merge e2e/deps::names/163 identity first resolution migrated (batch 3); script removed 164_ninja_goal_command_length.sh test merge e2e/test::graph/164 ninja goal command length migrated (batch 1); script removed -165_bare_name_cross_namespace_wire_address.sh deps merge e2e/deps +165_bare_name_cross_namespace_wire_address.sh deps merge e2e/deps::names/165 bare name cross namespace wire address migrated (batch 3); script removed 166_run_env_no_private_glibc.sh run merge e2e/run 167_build_defines_module_scan.sh modules merge e2e/modules 168_build_mcpp_musl_host_static.sh build-program merge e2e/build-program::cross/168 build mcpp musl host static migrated (batch 2); script kept: .github/workflows/ci-aarch64-fresh-install.yml runs it by path @@ -204,14 +204,14 @@ script subsystem class destination note 193_provision_reexport.sh build-program merge e2e/build-program::hosttools/193 provision reexport migrated (batch 2); script removed 194_build_mcpp_glob_input.sh build-program merge e2e/build-program::protocol/194 build mcpp glob input migrated (batch 2); script removed 195_target_cfg_feature_deps.sh features merge e2e/features -196_version_identity_and_lock.sh deps merge e2e/deps +196_version_identity_and_lock.sh deps merge e2e/deps::lock/196 version identity and lock migrated (batch 3); script removed 197_windows_resources.sh windows merge e2e/windows 198_windows_resources_cross.sh windows merge e2e/windows 199_resources_validation.sh windows merge e2e/windows 200_subos_env_reaches_program.sh runtime merge e2e/runtime 201_gcc_no_specs_pollution.sh toolchain merge e2e/toolchain 202_machine_output_contract.sh cli merge e2e/cli -203_exact_selector_lock_migration.sh deps merge e2e/deps +203_exact_selector_lock_migration.sh deps merge e2e/deps::lock/203 exact selector lock migration migrated (batch 3); script removed 204_new_transactional_scaffold.sh cli merge e2e/cli 205_root_local_subos.sh self merge e2e/self 206_runtime_binding_physics.sh runtime merge e2e/runtime @@ -314,7 +314,7 @@ script subsystem class destination note 323_post_link_record_durability.sh runtime merge e2e/runtime 324_fast_path_sees_path_deps.sh cache merge e2e/cache 325_system_toolchain_refused.sh toolchain merge e2e/toolchain -326_dependency_standard_floor.sh deps merge e2e/deps +326_dependency_standard_floor.sh deps merge e2e/deps::floors/326 dependency standard floor migrated (batch 3); script removed 327_declared_xlings_deps_report_their_own_failure.sh index merge e2e/index 328_layer_predicates_apply_and_unknown_ones_say_so.sh features merge e2e/features 329_exit_codes_match_the_contract.sh cli merge e2e/cli @@ -331,7 +331,7 @@ script subsystem class destination note 340_no_orphan_survives_a_killed_mcpp.sh cli merge e2e/cli 600_accel_variant_selection.sh build-program merge e2e/build-program::accel/600 accel variant selection migrated (batch 2); script removed 601_exclusive_capability.sh features merge e2e/features -603_version_floor.sh deps merge e2e/deps +603_version_floor.sh deps merge e2e/deps::floors/603 version floor migrated (batch 3); script removed 604_offline_skips_sandbox_bootstrap.sh self merge e2e/self 605_probe_channel_and_accel_reach_the_build_program.sh build-program merge e2e/build-program::accel/605 probe channel and accel reach the build program migrated (batch 2); script removed 606_constrained_source_globs_narrow_to_the_build.sh actions merge e2e/actions @@ -356,9 +356,9 @@ script subsystem class destination note 624_a_missing_host_module_names_the_key.sh build-program merge e2e/build-program::hosttools/624 a missing host module names the key migrated (batch 2); script removed 625_a_payload_can_be_gated_on_the_accelerator.sh build-program merge e2e/build-program::accel/625 a payload can be gated on the accelerator migrated (batch 2); script removed 626_naming_a_subset_of_backends_is_not_a_mismatch.sh build-program merge e2e/build-program::accel/626 naming a subset of backends is not a mismatch migrated (batch 2); script removed -627_one_package_one_version.sh deps merge e2e/deps -628_a_pin_below_a_stated_floor_is_refused.sh deps merge e2e/deps -629_a_version_range_is_answered_by_xpkg_dir.sh deps merge e2e/deps +627_one_package_one_version.sh deps merge e2e/deps::tools/627 one package one version migrated (batch 3); script removed +628_a_pin_below_a_stated_floor_is_refused.sh deps merge e2e/deps::tools/628 a pin below a stated floor is refused migrated (batch 3); script removed +629_a_version_range_is_answered_by_xpkg_dir.sh deps merge e2e/deps::tools/629 a version range is answered by xpkg dir migrated (batch 3); script removed 630_a_rule_brings_its_own_environment.sh actions merge e2e/actions 631_action_depfile_tracks_a_runtime_discovered_include.sh actions merge e2e/actions 632_a_build_program_is_told_its_package_name.sh build-program merge e2e/build-program::protocol/632 a build program is told its package name migrated (batch 2); script removed @@ -390,7 +390,7 @@ script subsystem class destination note 658_a_program_runs_on_an_attached_android_device.sh android keep keep a physical Android device over adb 659_an_action_declaration_has_no_size_limit.sh actions merge e2e/actions 660_an_action_names_the_engine_through_mcpp_self.sh actions merge e2e/actions -661_one_identity_two_declarations.sh deps merge e2e/deps +661_one_identity_two_declarations.sh deps merge e2e/deps::git/661 one identity two declarations migrated (batch 3); script removed 662_pack_stages_declared_files_before_the_closure.sh pack merge e2e/pack 663_a_graph_libcxx_over_the_payloads_c_library.sh toolchain merge e2e/toolchain 664_a_universal_apk_is_two_legs_in_one_tree.sh android merge e2e/android @@ -407,10 +407,10 @@ script subsystem class destination note 675_android_test_programs_carry_their_cxx_runtime.sh android merge e2e/android 677_a_conditional_dependency_replaces_the_unconditional_one.sh features merge e2e/features 678_a_row_states_a_library_form.sh target merge e2e/target -679_a_path_dependency_takes_its_manifests_identity.sh deps merge e2e/deps +679_a_path_dependency_takes_its_manifests_identity.sh deps merge e2e/deps::identity/679 a path dependency takes its manifests identity migrated (batch 3); script removed 680_a_dependency_floor_on_the_android_api_level.sh android merge e2e/android 681_index_repos_reach_an_existing_home.sh self merge e2e/self -682_why_deps_prints_the_resolved_graph.sh deps merge e2e/deps +682_why_deps_prints_the_resolved_graph.sh deps merge e2e/deps::identity/682 why deps prints the resolved graph migrated (batch 3); script removed 683_a_build_program_reads_the_payload_pkg_config_view.sh build-program merge e2e/build-program::protocol/683 a build program reads the payload pkg config view migrated (batch 2); script removed 684_a_runner_supplied_twice_is_refused.sh run merge e2e/run 685_a_nested_test_loads_a_graph_shared_library.sh runtime merge e2e/runtime @@ -432,7 +432,7 @@ script subsystem class destination note 710_a_forward_reaches_every_dependency_table.sh features merge e2e/features 711_a_feature_deps_restatement_names_one_source.sh features merge e2e/features 712_a_tool_package_contributes_nothing_to_its_consumer.sh build-program merge e2e/build-program::hosttools/712 a tool package contributes nothing to its consumer migrated (batch 2); script removed -713_a_git_dependency_selects_a_repository_member.sh deps merge e2e/deps +713_a_git_dependency_selects_a_repository_member.sh deps merge e2e/deps::git/713 a git dependency selects a repository member migrated (batch 3); script removed 714_features_names_a_dependency_feature.sh features merge e2e/features 720_the_root_build_program_reads_the_resolved_graph.sh build-program merge e2e/build-program::deps/720 the root build program reads the resolved graph migrated (batch 2); script removed 721_an_android_row_links_on_a_macos_host.sh macos merge e2e/macos @@ -502,13 +502,13 @@ script subsystem class destination note 813_conditional_dialect_cxxflags_root_only.sh features merge e2e/features 814_the_llvm_row_defaults_to_the_dynamic_crt.sh windows merge e2e/windows 815_the_database_describes_what_a_rule_generates.sh cdb merge e2e/cdb -816_a_git_dependency_reports_its_fetch.sh deps merge e2e/deps +816_a_git_dependency_reports_its_fetch.sh deps merge e2e/deps::git/816 a git dependency reports its fetch migrated (batch 3); script removed 817_emit_writes_no_xlings_json_under_private_work_dir.sh cdb merge e2e/cdb 818_a_declared_deploy_outranks_a_search_dir_dll_cross.sh windows merge e2e/windows 819_the_crt_is_placed_by_its_rule_not_by_search_order.sh windows sink unit/test_runtime_placement answers a parse/selection rule without a build: CRT placed by its rule 820_a_windows_program_takes_its_runtime_from_one_resolver.sh windows merge e2e/windows -821_an_edge_states_its_advice_on_both_paths.sh deps merge e2e/deps -822_a_package_states_the_oldest_mcpp_it_supports.sh deps merge e2e/deps +821_an_edge_states_its_advice_on_both_paths.sh deps merge e2e/deps::advice/821 an edge states its advice on both paths migrated (batch 3); script removed +822_a_package_states_the_oldest_mcpp_it_supports.sh deps merge e2e/deps::floors/822 a package states the oldest mcpp it supports migrated (batch 3); script removed 823_mcpp_core_and_mcpp_name_one_interface.sh build-program merge e2e/build-program::protocol/823 mcpp core and mcpp name one interface migrated (batch 2); script removed 824_a_build_program_reads_the_resolved_toolchain.sh build-program merge e2e/build-program::protocol/824 a build program reads the resolved toolchain migrated (batch 2); script removed 825_a_build_program_reads_the_msvc_toolset.sh windows merge e2e/windows @@ -529,7 +529,7 @@ script subsystem class destination note 841_pack_summarizes_many_outputs.sh pack merge e2e/pack 842_the_build_reports_each_step_once.sh cli merge e2e/cli 843_a_terminal_names_the_action_that_runs.sh cli merge e2e/cli -844_a_planned_build_keeps_one_lock_entry_per_identity.sh deps merge e2e/deps +844_a_planned_build_keeps_one_lock_entry_per_identity.sh deps merge e2e/deps::lock/844 a planned build keeps one lock entry per identity migrated (batch 3); script removed 845_play_game_restores_the_terminal.sh cli merge e2e/cli 846_the_vendored_xlings_is_replaced_from_the_newest_source.sh self keep keep upgrade: the vendored xlings replaced from the newest source in a home 847_a_module_name_is_unique_within_a_program.sh modules merge e2e/modules @@ -624,16 +624,16 @@ script subsystem class destination note 936_the_control_contract.sh cli merge e2e/cli 937_the_root_compile_database_is_opt_in.sh cdb merge e2e/cdb 938_a_relink_never_exposes_a_partial_file.sh build merge e2e/build -939_a_lock_written_meanwhile_is_not_overwritten.sh deps merge e2e/deps +939_a_lock_written_meanwhile_is_not_overwritten.sh deps merge e2e/deps::lock/939 a lock written meanwhile is not overwritten migrated (batch 3); script removed 940_an_indirect_import_is_found_whatever_map_its_importer_has.sh modules merge e2e/modules 941_a_closed_reader_ends_mcpp_and_no_child_inherits_the_ignore.sh cli merge e2e/cli 942_an_ok_image_is_built_once_and_run_by_okrun.sh openkal merge e2e/openkal -943_a_transitive_git_branch_is_locked.sh deps merge e2e/deps -944_a_root_checkout_answers_a_version_the_index_lacks.sh deps merge e2e/deps -945_a_miss_anywhere_in_the_graph_asks_for_a_refresh.sh deps merge e2e/deps -946_a_program_begins_in_a_dependency_entry.sh deps merge e2e/deps +943_a_transitive_git_branch_is_locked.sh deps merge e2e/deps::git/943 a transitive git branch is locked migrated (batch 3); script removed +944_a_root_checkout_answers_a_version_the_index_lacks.sh deps merge e2e/deps::git/944 a root checkout answers a version the index lacks migrated (batch 3); script removed +945_a_miss_anywhere_in_the_graph_asks_for_a_refresh.sh deps merge e2e/deps::graph/945 a miss anywhere in the graph asks for a refresh migrated (batch 3); script removed +946_a_program_begins_in_a_dependency_entry.sh deps merge e2e/deps::entry/946 a program begins in a dependency entry migrated (batch 3); script removed 947_an_image_is_entered_at_one_stated_symbol.sh openkal merge e2e/openkal -948_an_upgraded_dependency_is_restaged_at_the_same_size.sh deps merge e2e/deps +948_an_upgraded_dependency_is_restaged_at_the_same_size.sh deps merge e2e/deps::graph/948 an upgraded dependency is restaged at the same size migrated (batch 3); script removed 949_a_version_the_index_lacks_refreshes_at_install.sh index merge e2e/index 950_a_kernel_abi_package_over_the_system_c_library_is_in_the_shared_image.sh openkal merge e2e/openkal 951_an_ok_image_needs_no_declaration.sh openkal merge e2e/openkal @@ -650,7 +650,7 @@ script subsystem class destination note 962_a_webassembly_package_is_neither_an_xlings_entry_nor_a_single_file.sh openkal merge e2e/openkal 963_a_target_set_builds_every_member_and_runs_where_it_is_reached.sh openkal merge e2e/openkal 964_tests_built_once_run_from_an_archive.sh openkal merge e2e/openkal -980_an_ambiguous_dependency_name_is_no_packages.sh deps merge e2e/deps +980_an_ambiguous_dependency_name_is_no_packages.sh deps merge e2e/deps::graph/980 an ambiguous dependency name is no packages migrated (batch 3); script removed 981_a_web_link_has_the_profiles_level_and_the_wasm_exception_model.sh web merge e2e/web 982_an_action_reads_the_builds_contract.sh actions merge e2e/actions 983_a_pack_holds_the_legs_it_names.sh openkal merge e2e/openkal @@ -660,7 +660,7 @@ script subsystem class destination note 987_the_universal_spellings_are_migrated.sh openkal merge e2e/openkal 988_the_windows_launcher_finds_its_package_from_a_windows_path.sh openkal keep keep emulator: runs the Windows launcher under wine 989_a_native_legs_tests_run_from_an_archive.sh test merge e2e/test::archive/989 a native legs tests run from an archive migrated (batch 1); script removed -990_a_lock_key_does_not_depend_on_the_selected_members.sh deps merge e2e/deps +990_a_lock_key_does_not_depend_on_the_selected_members.sh deps merge e2e/deps::lock/990 a lock key does not depend on the selected members migrated (batch 3); script removed 991_a_prebuilt_archive_in_ldflags_is_a_link_input.sh build merge e2e/build 992_a_polyglot_file_runs_on_every_system_it_holds.sh openkal merge e2e/openkal 993_a_test_describes_its_launch.sh test merge e2e/test::launch/993 a test describes its launch ; e2e/test::launch/993 a test describes its launch (a named runner) migrated (batch 1); script removed @@ -671,8 +671,8 @@ script subsystem class destination note 998_a_build_keeps_its_bytes_once.sh cache merge e2e/cache 999_debug_information_is_stored_once.sh cache merge e2e/cache 1000_a_configuration_keeps_one_directory.sh cache merge e2e/cache -1001_a_dependency_carries_its_patches.sh deps merge e2e/deps -1002_a_dependency_is_embedded.sh deps merge e2e/deps +1001_a_dependency_carries_its_patches.sh deps merge e2e/deps::edges/1001 a dependency carries its patches migrated (batch 3); script removed +1002_a_dependency_is_embedded.sh deps merge e2e/deps::edges/1002 a dependency is embedded migrated (batch 3); script removed 1003_one_program_one_variant.sh build merge e2e/build 1004_an_external_toolchain_finds_mcpps_ninja.sh toolchain merge e2e/toolchain 1005_clean_stale_says_what_it_kept.sh cache merge e2e/cache @@ -688,7 +688,7 @@ script subsystem class destination note 1015_action_progress_paths_are_utf8_on_every_host.sh actions merge e2e/actions 1016_a_killed_build_does_not_race_the_next_one.sh cli merge e2e/cli 1017_a_system_msvc_builds_the_build_program_of_a_cross_target.sh windows merge e2e/windows -1018_a_package_and_a_namespaced_package_of_one_name_both_link.sh deps merge e2e/deps +1018_a_package_and_a_namespaced_package_of_one_name_both_link.sh deps merge e2e/deps::graph/1018 a package and a namespaced package of one name both link migrated (batch 3); script removed 1019_a_later_edge_walks_the_feature_deps_it_activates.sh features merge e2e/features 1020_a_platform_version_group_chooses_the_version.sh openkal merge e2e/openkal 1021_a_platform_series_is_locked_and_skips_newer_engines.sh openkal merge e2e/openkal @@ -697,12 +697,12 @@ script subsystem class destination note 1024_a_launch_description_takes_the_fast_path.sh run merge e2e/run 1025_the_default_platform_is_one_release.sh openkal merge e2e/openkal 1026_an_archived_member_test_carries_its_members_files.sh test merge e2e/test::archive/1026 an archived member test carries its members files migrated (batch 1); script removed -1027_a_configuration_group_keeps_the_other_groups_lock_entries.sh deps merge e2e/deps +1027_a_configuration_group_keeps_the_other_groups_lock_entries.sh deps merge e2e/deps::lock/1027 a configuration group keeps the other groups lock entries migrated (batch 3); script removed 1028_a_projects_state_directory_ignores_itself.sh cache merge e2e/cache 1029_an_unused_std_module_is_collected_by_age.sh cache merge e2e/cache 1030_the_low_space_warning_knows_what_the_last_build_took.sh cache merge e2e/cache 1031_target_keeps_within_its_budget.sh cache merge e2e/cache -1032_a_quoted_dotted_dependency_key_is_deprecated.sh deps merge e2e/deps +1032_a_quoted_dotted_dependency_key_is_deprecated.sh deps merge e2e/deps::names/1032 a quoted dotted dependency key is deprecated migrated (batch 3); script removed 1033_an_ok_image_runs_on_the_system_that_built_it.sh openkal merge e2e/openkal 1034_a_workspace_members_test_finds_its_programs.sh test merge e2e/test::workspace/1034 a workspace members test finds its programs migrated (batch 1); script removed 1035_a_test_program_reports_its_cases.sh test merge e2e/test::protocol/1035 a test program reports its cases migrated (batch 1); script removed @@ -716,9 +716,9 @@ script subsystem class destination note 1043_several_programs_and_a_dotted_name_are_packed_as_stated.sh pack merge e2e/pack 1044_clang_pairs_with_the_mingw_payload.sh windows keep keep emulator: runs the clang/mingw program under wine 1045_a_required_capability_takes_its_default_provider.sh features merge e2e/features -1046_a_patch_made_with_git_diff_from_the_package_root_is_applied.sh deps merge e2e/deps +1046_a_patch_made_with_git_diff_from_the_package_root_is_applied.sh deps merge e2e/deps::edges/1046 a patch made with git diff from the package root is applied migrated (batch 3); script removed 1047_an_artifact_compiles_its_shared_sources_with_its_own_flags.sh build merge e2e/build -1048_a_git_reference_follows_the_default_branch.sh deps merge e2e/deps +1048_a_git_reference_follows_the_default_branch.sh deps merge e2e/deps::git/1048 a git reference follows the default branch migrated (batch 3); script removed 1049_a_platform_skeleton_is_made_and_its_statement_read_as_the_engine_reads_it.sh openkal merge e2e/openkal 1050_a_test_run_is_kept_in_the_mtest_store.sh test merge e2e/test::archive/1050 a test run is kept in the mtest store migrated (batch 1); script removed 1051_a_cross_targets_toolchain_section_is_not_the_build_programs.sh build-program merge e2e/build-program::cross/1051 a cross targets toolchain section is not the build programs migrated (batch 2); script removed @@ -730,21 +730,21 @@ script subsystem class destination note 1057_a_platform_row_takes_nothing_of_the_payloads_host.sh openkal merge e2e/openkal 1058_a_gnu_abi_windows_row_compiles_its_resources.sh windows merge e2e/windows 1059_a_base_package_states_the_layers_beside_its_c_library.sh openkal merge e2e/openkal -1060_a_patched_member_of_a_repository_keeps_its_siblings.sh deps merge e2e/deps +1060_a_patched_member_of_a_repository_keeps_its_siblings.sh deps merge e2e/deps::edges/1060 a patched member of a repository keeps its siblings migrated (batch 3); script removed 1061_an_artifact_for_another_system_is_packed_without_running_it.sh pack merge e2e/pack 1062_link_flags_on_a_compiler_link_are_named_not_dropped.sh build merge e2e/build 1063_a_mingw_base_package_serves_windows_gnu_beside_llvm.sh windows keep keep emulator: runs the mingw base-package program under wine 1064_a_scan_override_of_a_file_this_build_excludes_is_kept.sh modules merge e2e/modules 1065_test_records_carry_parts_live_cases_and_metrics.sh test merge e2e/test::protocol/1065 test records carry parts live cases and metrics migrated (batch 1); script removed -1066_a_merged_version_withdraws_its_requests.sh deps merge e2e/deps +1066_a_merged_version_withdraws_its_requests.sh deps merge e2e/deps::graph/1066 a merged version withdraws its requests migrated (batch 3); script removed 1067_a_borrowed_registry_keeps_its_owners_places.sh index merge e2e/index 1068_a_running_test_holds_no_build_directory.sh test merge e2e/test::fast/1068 a running test holds no build directory migrated (batch 1); script removed 1069_an_unchanged_project_tests_without_planning.sh test merge e2e/test::fast/1069 an unchanged project tests without planning migrated (batch 1); script removed 1070_the_test_spec_launches_frameworks_checks_and_output.sh test merge e2e/test::protocol/1070 the test spec launches frameworks checks and output migrated (batch 1); script removed -1071_the_root_overrides_a_dependency_for_the_whole_graph.sh deps merge e2e/deps +1071_the_root_overrides_a_dependency_for_the_whole_graph.sh deps merge e2e/deps::graph/1071 the root overrides a dependency for the whole graph migrated (batch 3); script removed 1072_a_resource_include_nothing_provides_is_the_scripts_own.sh windows merge e2e/windows 1073_the_freebsd_musl_identity_is_served_by_openkal.sh openkal merge e2e/openkal -1074_a_namespace_package_named_like_a_spec_field.sh deps merge e2e/deps +1074_a_namespace_package_named_like_a_spec_field.sh deps merge e2e/deps::names/1074 a namespace package named like a spec field migrated (batch 3); script removed 1075_a_graph_supplied_windows_row_finds_its_resource_headers.sh windows merge e2e/windows 1076_an_aarch64_linux_gnu_cross_comes_from_a_sysroot_payload.sh target merge e2e/target 1077_a_program_of_another_system_is_built_not_run.sh target merge e2e/target diff --git a/tests/e2e/deps/advice.cpp b/tests/e2e/deps/advice.cpp new file mode 100644 index 000000000..4eb0a1c82 --- /dev/null +++ b/tests/e2e/deps/advice.cpp @@ -0,0 +1,68 @@ +// What a build edge says on success: its advice, reported once, on the full and the fast path +// (formerly tests/e2e/821). +import std; +import mcpplibs.mtest; +import mcpp.test.support; + +using namespace mcpplibs::mtest; +namespace s = mcpp::test::support; +namespace fs = std::filesystem; + +namespace { + +E2E advice{"advice", [] { + "821 an edge states its advice on both paths"_test[tag{"requires:unix-shell"}] = [] { + // An edge that has something to say on success writes its advice file, and mcpp reports + // it once after a successful build, by one function the full build path and the fast + // path both call; a build in which the edge did not run reports nothing. + s::needs({"unix-shell"}); + s::project p{{{"mcpp.toml", "[package]\nname = \"advice821\"\nversion = \"0.1.0\"\n"}, + {"src/main.cpp", "int main() { return 0; }\n"}, + {"data/probe.in", "first\n"}, + {"build.mcpp", R"cpp(import std; +import mcpp; +int main() { + const std::string in = std::string(mcpp::manifest_dir()) + "/data/probe.in"; + const std::string out = std::string(mcpp::out_dir()) + "/probe.stamp"; + mcpp::action a; + a.id = "advise"; + a.role = mcpp::roles::check; + // The advice file of this edge, in the build directory the edge runs in. + a.arg("sh").arg("-c") + .arg("mkdir -p .mcpp-advice && printf 'note\\tprobe input reads %s\\n' \"$(cat \"$1\")\" > .mcpp-advice/probe.stamp.advice") + .arg("sh").arg(in.c_str()) + .input(in.c_str()) + .output(out.c_str()) + .submit(); + return 0; +} +)cpp"}}}; + auto b1 = p.mcpp({"build"}); + expect(fatal(e2e::succeeded(b1))) << "the first build failed"; + expect(eq(s::count_lines(s::text(b1), "probe input reads first"), std::size_t{1})) << "A1: the full path did not report the edge's advice exactly once"; + + p.write(".before-b2", ""); + std::this_thread::sleep_for(std::chrono::seconds{1}); // a coarse file system clock must see the input as newer + p.write("data/probe.in", "second\n"); + auto b2 = p.mcpp({"build"}); + expect(fatal(e2e::succeeded(b2))) << "the second build failed"; + const auto marker = fs::last_write_time(p.at(".before-b2")); + bool planned = false; + for (const auto& f : s::find_files(p.at("target"), R"(build\.ninja)")) + if (f.generic_string().find("/.build-mcpp/") == std::string::npos && fs::last_write_time(f) > marker) planned = true; + if (planned) { + if (s::on_linux()) + expect(fatal(false)) << "A2: the second build planned again, so the fast path was not exercised"; + else + std::println("READING 821: the fast path declines on this system (#400); A2 reads the full path"); + } + expect(eq(s::count_lines(s::text(b2), "probe input reads second"), std::size_t{1})) << "A2: the second build did not report the edge's advice exactly once"; + expect(s::lacks(s::text(b2), "probe input reads first")) << "A2: the second build reported the previous run's advice"; + + auto b3 = p.mcpp({"build"}); + expect(fatal(e2e::succeeded(b3))) << "the third build failed"; + expect(s::lacks(s::text(b3), "probe input reads")) << "A3: a build in which the edge did not run reported its advice"; + }; +}}; + +} // namespace diff --git a/tests/e2e/deps/edges.cpp b/tests/e2e/deps/edges.cpp new file mode 100644 index 000000000..070fd37e8 --- /dev/null +++ b/tests/e2e/deps/edges.cpp @@ -0,0 +1,237 @@ +// What a dependency edge carries: patches, and `embed` (formerly tests/e2e/1001, 1002, 1046, 1060). +import std; +import mcpplibs.mtest; +import mcpp.test.support; + +using namespace mcpplibs::mtest; +namespace s = mcpp::test::support; +namespace fs = std::filesystem; + +namespace { + +const s::options inherited{.where = s::home::inherited}; + +bool failed(const e2e::outcome& r) { return r.started && r.status != 0; } + +// git in `dir` with a committer: its standard output. +std::string git(const fs::path& dir, std::vector args) { + const auto program = s::on_path("git"); + if (!program) skip("git is not installed"); + args.insert(args.begin(), {"-C", dir.string(), "-c", "user.name=t", "-c", "user.email=t@t"}); + auto r = s::run(*program, args); + expect(fatal(e2e::succeeded(r))) << "git failed: " << s::text(r); + return r.out; +} + +// The directories under `dir` whose name starts with `prefix`. +std::vector entries(const fs::path& dir, std::string_view prefix = "") { + std::vector out; + std::error_code ec; + for (const auto& e : fs::directory_iterator(dir, ec)) + if (e.path().filename().string().starts_with(prefix)) out.push_back(e.path()); + return out; +} + +// The `root:` `mcpp why deps` reports for mcpplibs.cmdline (`sed -n '/^ mcpplibs.cmdline[@+]/, +// /^ [^ ]/s/^ root: //p' | head -1`). +std::string reported_root(std::string_view why) { + bool in = false; + std::istringstream lines{std::string(why)}; + for (std::string line; std::getline(lines, line);) { + if (!in) { + in = line.starts_with(" mcpplibs.cmdline@") || line.starts_with(" mcpplibs.cmdline+"); + if (!in) continue; + } else if (line.size() > 2 && line.starts_with(" ") && line[2] != ' ') { + in = false; + } + if (line.starts_with(" root: ")) return line.substr(12); + } + return {}; +} + +E2E edges{"edges", [] { + "1001 a dependency carries its patches"_test[tag{"requires:gcc"}] = [] { + // A patched library is another library: the verified source is copied to the variant's + // own tree under the mcpp home and patched there; the lock names the variant and each + // patch with its SHA-256; a changed patch is another variant; a patch that does not + // apply is refused naming the patch and its hunk and leaves no tree; a path dependency + // carries no patches. + s::needs({"gcc"}); + const auto home = s::case_home(s::home::inherited); + const auto variants = home / "cache/src/compat.zlib"; + s::project app{{{"patches/0001-mark.patch", "--- a/zlib-1.3.2/zlib.h\n+++ b/zlib-1.3.2/zlib.h\n@@ -31,4 +31,6 @@\n #ifndef ZLIB_H\n #define ZLIB_H\n \n+#define MCPP_PATCHED_ZLIB 1\n+\n #ifdef ZLIB_BUILD\n"}, + {"mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n[dependencies]\n\"compat.zlib\" = { version = \"1.3.2\", patches = [\"patches/0001-mark.patch\"] }\n"}, + {"src/main.cpp", R"cpp(#include +#include +int main() { +#ifdef MCPP_PATCHED_ZLIB + std::printf("patched %d %s\n", MCPP_PATCHED_ZLIB, zlibVersion()); + return 0; +#else + std::printf("unpatched\n"); + return 1; +#endif +} +)cpp"}}}; + auto a = app.mcpp({"run"}, inherited); + expect(fatal(e2e::succeeded(a))) << "the patched build did not run"; + expect(s::has_line(s::text(a), "^patched 1 1.3.2")) << "the program did not see the patched header"; + const auto made = entries(variants, "1.3.2+p"); + expect(fatal(!made.empty() && fs::exists(made.front() / ".mcpp-variant"))) << "no variant tree under the mcpp home"; + expect(e2e::contains(s::read_file(made.front() / "zlib-1.3.2/zlib.h"), "MCPP_PATCHED_ZLIB")) << "the variant's header is not patched"; + const auto installed = home / "registry/data/xpkgs/compat-x-zlib/1.3.2"; + if (fs::is_directory(installed)) + expect(s::lacks(s::read_file(installed / "zlib-1.3.2/zlib.h"), "MCPP_PATCHED_ZLIB")) << "the installed source was patched in place"; + const auto lock = app.read("mcpp.lock"); + expect(s::has_line(lock, "^variant = ")) << "the lock names no variant"; + expect(s::has_line(lock, "patches/0001-mark.patch#sha256:[0-9a-f]{64}")) << "the lock does not name the patch and its digest"; + auto sbom = app.mcpp({"emit", "sbom"}, inherited); + expect(fatal(e2e::succeeded(sbom))) << "sbom failed"; + expect(e2e::contains(sbom.out, "\"pedigree\"") && e2e::contains(sbom.out, "0001-mark.patch")) << "the bill of materials states no pedigree"; + + auto patch = app.read("patches/0001-mark.patch"); + patch.replace(patch.find("MCPP_PATCHED_ZLIB 1"), 19, "MCPP_PATCHED_ZLIB 2"); + app.write("patches/0001-mark.patch", patch); + auto c = app.mcpp({"run"}, inherited); + expect(fatal(e2e::succeeded(c))) << "the build with a changed patch failed"; + expect(s::has_line(s::text(c), "^patched 2 ")) << "a changed patch did not make another variant"; + expect(entries(variants, "1.3.2+p").size() >= 2) << "the changed patch did not get its own tree"; + + app.write("patches/0001-mark.patch", "--- a/zlib-1.3.2/zlib.h\n+++ b/zlib-1.3.2/zlib.h\n@@ -31,1 +31,1 @@\n-#ifndef NOT_THE_LINE\n+#ifndef ZLIB_H\n"); + const auto before = entries(variants).size(); + auto d = app.mcpp({"build"}, inherited); + expect(fatal(failed(d))) << "a patch that does not apply was accepted"; + expect(e2e::contains(s::text(d), "0001-mark.patch") && e2e::contains(s::text(d), "hunk 1")) << "the refusal does not name the patch and the hunk"; + expect(eq(entries(variants).size(), before)) << "a failed patching left a tree"; + + s::project{app.dir().parent_path() / "lib", {{"mcpp.toml", "[package]\nname = \"lib\"\nversion = \"0.1.0\"\n"}, + {"src/lib.cpp", "int lib_one() { return 1; }\n"}}}; + app.write("mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n[dependencies]\nlib = { path = \"../lib\", patches = [\"patches/0001-mark.patch\"] }\n"); + auto e = app.mcpp({"build"}, inherited); + expect(fatal(failed(e))) << "patches on a path dependency were accepted"; + expect(e2e::contains(s::text(e), "is a path and carries patches")) << "the refusal does not say why"; + }; + + "1002 a dependency is embedded"_test[tag{"requires:gcc"}] = [] { + // `embed = true` compiles a dependency into its consumer: its sources, less `exclude`, + // with its own include directories and macros; it builds no library; `exclude` needs + // `embed`; a program reaching it holds its embedder or is refused; its link flags and + // runtime libraries do not reach the program's link. + s::needs({"gcc"}); + const auto tmp = temp_dir(); + s::project zip{tmp / "zip", {{"mcpp.toml", "[package]\nname = \"zip\"\nversion = \"0.1.0\"\n[dependencies]\n\"compat.zlib\" = { version = \"1.3.2\", embed = true, exclude = [\"*/uncompr.c\"] }\n"}, + {"src/zip.cppm", "module;\n#include \nexport module zip;\nexport int zip_bound(int n) { return static_cast(compressBound(static_cast(n))); }\nexport const char* zip_version() { return zlibVersion(); }\n"}, + {"build.mcpp", "import std;\nint main() { return 0; }\n"}}}; + s::project app{tmp / "app", {{"mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n[dependencies]\nzip = { path = \"../zip\" }\n"}, + {"src/main.cpp", "import std;\nimport zip;\nint main() { std::println(\"{} {}\", zip_version(), zip_bound(100) > 100); }\n"}}}; + auto a = app.mcpp({"run"}, inherited); + expect(fatal(e2e::succeeded(a))) << "the program with an embedded zlib did not run"; + expect(s::lacks(s::text(a), "matched no source file")) << "the flags embed gives were reported as a glob that matched nothing"; + expect(s::has_line(s::text(a), "^1.3.2 true")) << "the program did not call the embedded zlib"; + fs::path dir; + for (const auto& d : s::glob(app.at("target"), "*/*")) + if (dir.empty() && fs::is_directory(d) && std::regex_match(d.filename().string(), std::regex{"[0-9a-f]{16}"})) dir = d; + expect(fatal(!dir.empty())) << "no build directory"; + const auto db = s::find_file(dir, "compile_commands.json").value_or(app.at("compile_commands.json")); + const auto commands = s::read_file(db); + expect(e2e::contains(commands, "deflate.c")) << "zlib's sources were not compiled"; + expect(s::lacks(commands, "uncompr.c")) << "an excluded file was compiled"; + expect(s::find_files(dir, R"(.*zlib.*\.(a|lib))").empty()) << "a library of the embedded package was built"; + expect(e2e::contains(commands, "mcpp_zlib_config.h")) << "the dependency's own flags did not reach its files"; + bool leaked = false; + for (const auto& e : s::json(commands)) { + if (!e["file"].get().ends_with("zip.cppm")) continue; + std::string all; + if (e.contains("arguments")) + for (const auto& arg : e["arguments"]) all += arg.get() + " "; + if (e.contains("command")) all += e["command"].get(); + leaked = leaked || all.find("mcpp_zlib_config.h") != std::string::npos; + } + expect(!leaked) << "the dependency's flags reached the consumer's own unit"; + + s::project bad{tmp / "bad", {{"mcpp.toml", "[package]\nname = \"bad\"\nversion = \"0.1.0\"\n\n[dependencies]\n\"compat.zlib\" = { version = \"1.3.2\", exclude = [\"*/gzwrite.c\"] }\n"}, + {"src/main.cpp", "int main() {}\n"}}}; + auto c = bad.mcpp({"build"}, inherited); + expect(fatal(failed(c))) << "exclude without embed was accepted"; + expect(e2e::contains(s::text(c), "exclude is allowed only with embed")) << "the refusal does not say why"; + + s::project ws{tmp / "ws", {{"mcpp.toml", "[workspace]\nmembers = [\"app\", \"plain\", \"zip\"]\n"}, + {"plain/mcpp.toml", "[package]\nname = \"plain\"\nversion = \"0.1.0\"\n\n[dependencies]\n\"compat.zlib\" = \"1.3.2\"\n"}, + {"plain/src/main.cpp", "#include \nint main() { return zlibVersion()[0] == 0; }\n"}}}; + fs::copy(app.dir(), ws.at("app"), fs::copy_options::recursive); + fs::copy(zip.dir(), ws.at("zip"), fs::copy_options::recursive); + ws.remove("app/target"); + auto d = ws.mcpp({"build", "--workspace"}, inherited); + expect(fatal(failed(d))) << "a program without the embedder was accepted"; + expect(e2e::contains(s::text(d), "which 'zip' embeds")) << "the refusal does not name the embedder"; + + s::project{tmp / "lk", {{"mcpp.toml", "[package]\nname = \"lk\"\nversion = \"0.1.0\"\n\n[build]\nldflags = [\"-lmcpp-no-such-library\"]\n\n[runtime]\nlibraries = [\"mcpp-no-such-runtime-library\"]\n"}, + {"src/lk.c", "int lk_answer(void) { return 42; }\n"}}}; + s::project lkapp{tmp / "lkapp", {{"mcpp.toml", "[package]\nname = \"lkapp\"\nversion = \"0.1.0\"\n\n[dependencies]\nlk = { path = \"../lk\", embed = true }\n"}, + {"src/main.cpp", "extern \"C\" int lk_answer();\nint main() { return lk_answer() == 42 ? 0 : 1; }\n"}}}; + expect(e2e::succeeded(lkapp.mcpp({"run"}, inherited))) << "the embedded package's link flags or libraries reached the program's link"; + }; + + "1046 a patch made with git diff from the package root is applied"_test[tag{"requires:elf"}][tag{"requires:gcc"}] = [] { + // `mcpp why deps` reports an index dependency's root; a patch made there with `git diff` + // applies, and the variant is then the root the build reads. + s::needs({"elf", "gcc"}); + const auto tmp = temp_dir(); + s::project app{tmp / "app", {{"mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n[dependencies]\nmcpplibs.cmdline = \"0.0.1\"\n"}, + {"src/main.cpp", "int main() { return 0; }\n"}}}; + auto why = app.mcpp({"why", "deps"}); + expect(fatal(e2e::succeeded(why))) << "why deps failed"; + const auto root = reported_root(s::text(why)); + expect(fatal(!root.empty() && fs::is_directory(root))) << "the dependency's root is not reported"; + const auto copy = tmp / "cmdline"; + fs::copy(root, copy, fs::copy_options::recursive); + const auto found = s::find_file(copy, "parse.cppm"); + expect(fatal(found.has_value())) << "the root holds no parse.cppm"; + const auto file = fs::relative(*found, copy).generic_string(); + git(copy, {"init", "-q"}); + git(copy, {"add", "-A"}); + git(copy, {"commit", "-qm", "base"}); + s::project{copy, {}}.write(file, s::read_file(*found) + "// changed for the patch\n"); + app.write("patches/0001-change.patch", git(copy, {"diff"})); + expect(fatal(e2e::contains(app.read("patches/0001-change.patch"), "--- a/" + file))) << "git did not name the file from the root"; + app.write("mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n[dependencies]\nmcpplibs.cmdline = { version = \"0.0.1\", patches = [\"patches/0001-change.patch\"] }\n"); + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "the patched build failed"; + auto why2 = app.mcpp({"why", "deps"}); + expect(fatal(e2e::succeeded(why2))) << "why deps failed after the patch"; + const auto variant = reported_root(s::text(why2)); + expect(fatal(!variant.empty() && variant != root)) << "the variant is not the reported root"; + expect(e2e::contains(s::read_file(fs::path(variant) / file), "changed for the patch")) << "the variant does not hold the change"; + }; + + "1060 a patched member of a repository keeps its siblings"_test[tag{"requires:gcc"}][tag{"requires:unix-shell"}] = [] { + // A patch on a member of a git repository applies to that member, and its path + // dependency on a sibling still reaches the sibling. + s::needs({"gcc", "unix-shell"}); + const auto tmp = temp_dir(); + s::project repo{tmp / "repo", {{"mcpp.toml", "[workspace]\nmembers = [\"core\", \"front\"]\n"}, + {"core/mcpp.toml", "[package]\nname = \"toy-core\"\nversion = \"0.1.0\"\n[targets.toy-core]\nkind = \"lib\"\n"}, + {"core/src/core.c", "int core_value(void) { return 40; }\n"}, + {"front/mcpp.toml", "[package]\nname = \"toy-front\"\nversion = \"0.1.0\"\n[dependencies]\ntoy-core = { path = \"../core\" }\n[targets.toy-front]\nkind = \"lib\"\n"}, + {"front/src/front.c", "int front_value(void) { return 1; }\n"}}}; + git(repo.dir(), {"init", "-q"}); + git(repo.dir(), {"add", "-A"}); + git(repo.dir(), {"commit", "-qm", "base"}); + git(repo.dir(), {"tag", "v1"}); + const auto edit = tmp / "front-edit"; + fs::copy(repo.at("front"), edit, fs::copy_options::recursive); + git(edit, {"init", "-q"}); + git(edit, {"add", "-A"}); + git(edit, {"commit", "-qm", "b"}); + s::project{edit, {}}.write("src/front.c", "int front_value(void) { return 2; }\n"); + s::project app{tmp / "app", {{"src/main.c", "#include \nint front_value(void); int core_value(void);\nint main(void) { printf(\"%d %d\\n\", front_value(), core_value()); return 0; }\n"}}}; + app.write("patches/0001-front.patch", git(edit, {"diff"})); + expect(fatal(e2e::contains(app.read("patches/0001-front.patch"), "--- a/src/front.c"))) << "git wrote no member-rooted patch"; + app.write("mcpp.toml", std::format("[package]\nname = \"app\"\nversion = \"0.1.0\"\n[dependencies]\ntoy-front = {{ git = \"file://{}\", tag = \"v1\", patches = [\"patches/0001-front.patch\"] }}\n[targets.app]\nkind = \"bin\"\nmain = \"src/main.c\"\n", repo.dir().generic_string())); + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "the patched member did not build"; + auto bins = s::glob(app.at("target"), "*/*/bin/app"); + expect(fatal(!bins.empty())) << "no program built"; + expect(eq(s::last_line(s::run(bins.front()).out), std::string{"2 40"})) << "the program's output"; + }; +}}; + +} // namespace diff --git a/tests/e2e/deps/entry.cpp b/tests/e2e/deps/entry.cpp new file mode 100644 index 000000000..7337426fc --- /dev/null +++ b/tests/e2e/deps/entry.cpp @@ -0,0 +1,139 @@ +// A program that begins in a dependency's `kind = "entry"` target (formerly tests/e2e/946). +import std; +import mcpplibs.mtest; +import mcpp.test.support; + +using namespace mcpplibs::mtest; +namespace s = mcpp::test::support; +namespace fs = std::filesystem; + +namespace { + +bool failed(const e2e::outcome& r) { return r.started && r.status != 0; } + +constexpr std::string_view fw_manifest = R"cpp([package] +name = "fw" +version = "0.1.0" +[features] +app = {} +[targets.fw] +kind = "lib" +[targets.player] +kind = "entry" +main = "src/entry/player.cpp" +defines = ["FW_PLAYER"] +[targets.app] +kind = "entry" +main = "src/entry/app.cpp" +required_features = ["app"] +[target.'cfg(os = "linux")'.targets.app] +main = "src/entry/app_linux.cpp" +[test] +main = { from = "fw/player" } +)cpp"; + +constexpr std::string_view fw_module = R"cpp(export module fw; +import std; +// The callback an application defines. Declared with C++ language linkage at +// global scope: a declaration attached to this module could not be defined +// outside it. +export extern "C++" int fw_app(); +export namespace fw { +int twice(int v) { return 2 * v; } +} +)cpp"; + +constexpr std::string_view player = R"cpp(import std; +import fw; +#ifndef FW_PLAYER +#error the entry is compiled with its own target's defines +#endif +int main() { + std::println("player {}", fw::twice(fw_app())); + return 0; +} +)cpp"; + +E2E entry{"entry", [] { + "946 a program begins in a dependency entry"_test[tag{"requires:gcc"}][tag{"requires:elf"}] = [] { + // A framework supplies `main` as an entry target; the application writes only the + // callback. The entry is in no image that does not select it; required features gate + // it; a selection naming nothing is refused with the candidates; the entry function + // follows the entry; a platform row replaces its file; test programs begin in an entry; + // `start` on a hosted target is refused; the result record names the beginning. + s::needs({"gcc", "elf"}); + const auto tmp = temp_dir(); + s::project fw{tmp / "fw", {{"mcpp.toml", fw_manifest}, + {"src/fw.cppm", fw_module}, + {"src/entry/player.cpp", player}, + {"src/entry/app.cpp", "import std;\nimport fw;\nint main() { std::println(\"app {}\", fw_app()); return 0; }\n"}, + {"src/entry/app_linux.cpp", "import std;\nimport fw;\nint main() { std::println(\"app-linux {}\", fw_app()); return 0; }\n"}, + {"tests/callback.cpp", "int fw_app() { return 21; }\n"}}}; + s::project notes{tmp / "notes", {{"src/app.cpp", "int fw_app() { return 21; }\n"}}}; + auto manifest = [&](std::string_view keys, std::string_view dep = "fw = { path = \"../fw\" }") { + notes.write("mcpp.toml", std::format("[package]\nname = \"notes\"\nversion = \"0.1.0\"\n[dependencies]\n{}\n[targets.notes]\nkind = \"bin\"\n{}\n", dep, keys)); + }; + + manifest("main = { from = \"fw/player\" }"); + expect(fatal(e2e::succeeded(notes.mcpp({"build"})))) << "A: the program over fw's entry did not build"; + auto a = notes.mcpp({"run"}); + expect(fatal(e2e::succeeded(a))) << "A: the program did not run"; + expect(e2e::contains(s::text(a), "player 42")) << "A: the program did not begin in fw's entry with its flags"; + + expect(fatal(e2e::succeeded(fw.mcpp({"build"})))) << "B: fw as the root did not build"; + s::project other{tmp / "other", {{"mcpp.toml", "[package]\nname = \"other\"\nversion = \"0.1.0\"\n[dependencies]\nfw = { path = \"../fw\" }\n"}, + {"src/main.cpp", "import std;\nimport fw;\nint fw_app() { return 1; }\nint main() { std::println(\"own {}\", fw::twice(3)); }\n"}}}; + auto b = other.mcpp({"run"}); + expect(fatal(e2e::succeeded(b))) << "B: a program with its own main over fw did not link"; + expect(e2e::contains(s::text(b), "own 6")) << "B: the program with its own main did not run"; + + manifest("main = { from = \"fw/app\" }"); + auto c = notes.mcpp({"build"}); + expect(fatal(failed(c))) << "C: an entry with an inactive required feature was linked"; + expect(e2e::contains(s::text(c), "requires the feature \"app\"")) << "C: the refusal does not name the feature"; + expect(e2e::contains(s::text(c), "fw = { ..., features = [\"app\"] }")) << "C: the refusal does not give the dependency line"; + manifest("main = { from = \"fw/app\" }", "fw = { path = \"../fw\", features = [\"app\"] }"); + auto c_run = notes.mcpp({"run"}); + expect(fatal(e2e::succeeded(c_run))) << "C: the entry with its feature did not build"; + + manifest("main = { from = \"nope/player\" }"); + auto d1 = notes.mcpp({"build"}); + expect(failed(d1)) << "D: an unknown dependency was accepted"; + expect(e2e::contains(s::text(d1), "'nope' is neither a dependency of 'notes' nor its own name (its dependencies: fw)")) << "D: the unknown dependency is not named with the candidates"; + manifest("main = { from = \"fw/fw\" }"); + auto d2 = notes.mcpp({"build"}); + expect(failed(d2)) << "D: a library target was accepted as an entry"; + expect(e2e::contains(s::text(d2), "declares no entry target 'fw' (its `kind = \"entry\"` targets: app, player)")) << "D: the entry targets are not listed"; + manifest("main = { from = \"fw/player\" }\ndefines = [\"X=1\"]"); + auto d3 = notes.mcpp({"build"}); + expect(failed(d3)) << "D: flags for the program's own entry were accepted beside from"; + expect(e2e::contains(s::text(d3), "targets.notes.defines applies to the target's own entry translation unit")) << "D: the flags refusal does not say why"; + + std::string with_entry{fw_manifest}; + const std::string defines = "defines = [\"FW_PLAYER\"]\n"; + with_entry.insert(with_entry.find(defines) + defines.size(), "windows_entry = \"main\"\n"); + fw.write("mcpp.toml", with_entry); + manifest("main = { from = \"fw/player\" }\nwindows_entry = \"wmain\""); + auto e = notes.mcpp({"build"}); + expect(failed(e)) << "E: two entry functions were accepted"; + expect(s::has_line(s::text(e), "windows_entry = \"wmain\", and its beginning .fw/player. defines `main`")) << "E: the conflict does not name both statements"; + + expect(e2e::contains(s::text(c_run), "app-linux 21")) << "F: the row did not replace the entry's file"; + + auto g = fw.mcpp({"test"}); + expect(fatal(e2e::succeeded(g))) << "G: the test over the package's own entry failed"; + expect(e2e::contains(s::text(g), "callback")) << "G: the test did not run"; + + manifest("main = { from = \"fw/player\" }\nstart = \"_start\""); + auto h = notes.mcpp({"build"}); + expect(failed(h)) << "H: start on a hosted target was accepted"; + expect(e2e::contains(s::text(h), "the image is entered by the C runtime")) << "H: the refusal does not name the C runtime"; + + manifest("main = { from = \"fw/player\" }"); + auto i = notes.mcpp({"build", "--message-format", "json"}); + expect(fatal(e2e::succeeded(i))) << "I: the json build failed"; + expect(e2e::contains(i.out, "\"entry\":{\"from\":\"fw/player\"}")) << "I: the result record does not name where the program begins"; + }; +}}; + +} // namespace diff --git a/tests/e2e/deps/floors.cpp b/tests/e2e/deps/floors.cpp new file mode 100644 index 000000000..f71c1965e --- /dev/null +++ b/tests/e2e/deps/floors.cpp @@ -0,0 +1,134 @@ +// What a dependency states it needs of the graph, the machine and mcpp: a standard, a version +// floor, the oldest mcpp release (formerly tests/e2e/326, 603, 822). +import std; +import mcpplibs.mtest; +import mcpp.test.support; + +using namespace mcpplibs::mtest; +namespace s = mcpp::test::support; +namespace fs = std::filesystem; + +namespace { + +bool failed(const e2e::outcome& r) { return r.started && r.status != 0; } + +E2E floors{"floors", [] { + "326 dependency standard floor"_test[tag{"requires:gcc"}] = [] { + // A module graph has one standard, the root's; a dependency that declared a higher one is + // built at the root's, and that is reported (degraded, an error under --strict), naming + // the package, the level and the one-place fix. Silent when it declared nothing, the + // graph's own level, or once the root is raised. + s::needs({"gcc"}); + const auto tmp = temp_dir(); + s::project lib{tmp / "lib", {{"mcpp.toml", "[package]\nname = \"flib\"\nversion = \"0.1.0\"\nstandard = \"c++26\"\n[targets.flib]\nkind = \"lib\"\n"}, + {"src/flib.cppm", "export module flib;\nexport int f(){ return 1; }\n"}}}; + s::project app{tmp / "app", {{"mcpp.toml", "[package]\nname = \"fapp\"\nversion = \"0.1.0\"\nstandard = \"c++23\"\n[dependencies]\nflib = { path = \"../lib\" }\n"}, + {"src/main.cpp", "import flib;\nint main(){ return f()==1 ? 0 : 1; }\n"}}}; + auto warn = app.mcpp({"build"}); + expect(fatal(e2e::succeeded(warn))) << "the floor check must be degraded, not an error"; + expect(e2e::contains(s::text(warn), "flib") && e2e::contains(s::text(warn), "c++26")) << "the diagnostic does not name the package and the level"; + expect(e2e::contains(s::text(warn), "workspace.package")) << "the hint does not name the one-place fix"; + app.remove("target"); + expect(failed(app.mcpp({"build", "--strict"}))) << "--strict did not promote the degradation"; + + auto rebuild_silent = [&](std::string_view lib_manifest, std::string_view why) { + lib.write("mcpp.toml", lib_manifest); + app.remove("target"); + auto r = app.mcpp({"build"}); + expect(fatal(e2e::succeeded(r))) << "build failed"; + expect(s::lacks(s::text(r), "declares standard")) << why; + }; + rebuild_silent("[package]\nname = \"flib\"\nversion = \"0.1.0\"\n[targets.flib]\nkind = \"lib\"\n", "fired for a dependency that declared nothing"); + rebuild_silent("[package]\nname = \"flib\"\nversion = \"0.1.0\"\nstandard = \"c++23\"\n[targets.flib]\nkind = \"lib\"\n", "fired for a dependency at the graph's own level"); + app.write("mcpp.toml", "[package]\nname = \"fapp\"\nversion = \"0.1.0\"\nstandard = \"c++26\"\n[dependencies]\nflib = { path = \"../lib\" }\n"); + rebuild_silent("[package]\nname = \"flib\"\nversion = \"0.1.0\"\nstandard = \"c++26\"\n[targets.flib]\nkind = \"lib\"\n", "the documented fix does not silence the diagnostic"); + }; + + "603 version floor"_test = [] { + // A package needing more of the machine than it has is refused before compiling, naming + // the requirement, both versions and who stated the fact; a met floor builds, and a + // floor nobody answered is silent. + const auto parent = temp_dir() / "new"; + fs::create_directories(parent); + auto mkfact = [&](std::string_view name, std::string_view fact) { + s::project{parent / name, {}}.write("mcpp.toml", std::format("[package]\nname = \"{0}\"\nversion = \"0.1.0\"\n[language]\nstandard = \"c++23\"\n[runtime]\nprovides = [\"{1}\"]\n[targets.{0}]\nkind = \"lib\"\n", name, fact)); + s::project{parent / name, {}}.write(std::format("src/{}.c", name), std::format("int {}_anchor(void){{return 0;}}\n", name)); + }; + auto mkneed = [&](std::string_view name, std::string_view need) { + s::project{parent / name, {}}.write("mcpp.toml", std::format("[package]\nname = \"{0}\"\nversion = \"0.1.0\"\n[language]\nstandard = \"c++23\"\n[[runtime.requirements]]\nkind = \"version-floor\"\nvalue = \"{1}\"\n[targets.{0}]\nkind = \"lib\"\n", name, need)); + s::project{parent / name, {}}.write(std::format("src/{}.c", name), std::format("int {}_anchor(void){{return 0;}}\n", name)); + }; + auto consumer = [&](std::vector deps) { + fs::remove_all(parent / "app"); + auto app = s::project::scaffold("app"); + std::string manifest = "[package]\nname=\"app\"\nversion=\"0.1.0\"\n[language]\nstandard=\"c++23\"\n[dependencies]\n"; + for (auto d : deps) manifest += std::format("{0} = {{ path = \"../{0}\" }}\n", d); + app.write("mcpp.toml", manifest); + for (const auto& f : s::glob(app.at("src"), "*.cppm")) fs::remove(f); + app.write("src/main.cpp", "int main(){return 0;}\n"); + return app; + }; + mkfact("driverfact", "cuda.driver=12.4"); + mkneed("toolkitnew", "cuda.driver >= 13.0"); + auto out = consumer({"driverfact", "toolkitnew"}).mcpp({"build"}); + expect(fatal(failed(out))) << "a floor above the stated fact was accepted"; + expect(e2e::contains(s::text(out), "cuda.driver")) << "refusal does not name what is short"; + expect(e2e::contains(s::text(out), "13.0")) << "refusal does not say what was needed"; + expect(e2e::contains(s::text(out), "12.4")) << "refusal does not say what is there"; + expect(e2e::contains(s::text(out), "driverfact")) << "refusal does not say who stated the fact"; + mkneed("toolkitok", "cuda.driver >= 12.0"); + expect(e2e::succeeded(consumer({"driverfact", "toolkitok"}).mcpp({"build"}))) << "a met floor was refused"; + mkneed("futureneed", "some.future.thing >= 4.2.1"); + expect(e2e::succeeded(consumer({"futureneed"}).mcpp({"build"}))) << "a floor nobody answered was refused"; + }; + + "822 a package states the oldest mcpp it supports"_test[tag{"requires:unix-shell"}][resource{"machine"}] = [] { + // `[package] mcpp = ">=V"` states the oldest mcpp release a package supports: an engine + // below it stops first, naming the package, the floor, its own release and the way to + // upgrade; a bare release or a non-release is explained; a workspace floor reaches its + // members; an unknown [lib] key is named. + s::needs({"unix-shell"}); + const auto tmp = temp_dir(); + std::string have; + { + auto v = s::mcpp({"--version"}, tmp); + std::istringstream words{s::last_line(v.out)}; + words >> have >> have; + } + s::project p{tmp / "p", {{"src/main.cpp", "int main() { return 0; }\n"}}}; + auto manifest = [&](std::string_view floor) { + p.write("mcpp.toml", std::format("[package]\nname = \"floor822\"\nversion = \"0.1.0\"\nmcpp = {}\n", floor)); + }; + manifest("\">=2999.1.1\""); + auto f1 = p.mcpp({"build"}); + expect(fatal(failed(f1))) << "F1: a floor above " << have << " built"; + expect(e2e::contains(s::text(f1), std::format("requires mcpp >= 2999.1.1; this is mcpp {}", have))) << "F1: the message does not name the floor and this release"; + expect(e2e::contains(s::text(f1), "xlings install mcpp@2999.1.1")) << "F1: the message does not say how to upgrade"; + manifest(std::format("\">={}\"", have)); + expect(e2e::succeeded(p.mcpp({"build"}))) << "F2: a floor equal to this release did not build"; + manifest("\">=2026.1.1\""); + expect(e2e::succeeded(p.mcpp({"build"}))) << "F2: a floor below this release did not build"; + manifest(std::format("\"{}\"", have)); + auto f3 = p.mcpp({"build"}); + expect(failed(f3)) << "F3: a bare release was accepted"; + expect(e2e::contains(s::text(f3), std::format("names one release exactly; a floor is written \">={}\"", have))) << "F3: the bare release is not explained"; + manifest("\">=banana\""); + auto f3b = p.mcpp({"build"}); + expect(failed(f3b)) << "F3: a non-release was accepted"; + expect(e2e::contains(s::text(f3b), "'banana' is not an mcpp release")) << "F3: the non-release is not explained"; + + s::project ws{tmp / "ws", {{"mcpp.toml", "[workspace]\nmembers = [\"app\"]\n[workspace.package]\nversion = \"0.1.0\"\nmcpp = \">=2999.1.1\"\n"}, + {"app/mcpp.toml", "[package]\nname = \"app822\"\n"}, + {"app/src/main.cpp", "int main() { return 0; }\n"}}}; + auto f4 = ws.mcpp({"build", "-p", "app"}); + expect(failed(f4)) << "F4: the workspace floor did not reach the member"; + expect(s::has_line(s::text(f4), "package 'app822'.*requires mcpp >= 2999\\.1\\.1")) << "F4: the member is not named with the inherited floor"; + + p.write("mcpp.toml", "[package]\nname = \"floor822\"\nversion = \"0.1.0\"\n\n[lib]\npth = \"src/x.cppm\"\n"); + auto f5 = p.mcpp({"build"}); + expect(fatal(e2e::succeeded(f5))) << "F5: the build failed"; + expect(e2e::contains(s::text(f5), "[lib] has unsupported key 'pth' (ignored). Supported keys: path.")) << "F5: [lib] did not name the unknown key"; + }; +}}; + +} // namespace diff --git a/tests/e2e/deps/git.cpp b/tests/e2e/deps/git.cpp new file mode 100644 index 000000000..37270acd3 --- /dev/null +++ b/tests/e2e/deps/git.cpp @@ -0,0 +1,443 @@ +// Git dependencies: the clone cache, the lock, branches and their refresh; one identity declared +// twice; a repository's members; the fetch report; transitive branches; checkouts answering a +// version; the default branch (formerly tests/e2e/24, 661, 713, 816, 943, 944, 1048). +import std; +import mcpplibs.mtest; +import mcpp.test.support; + +using namespace mcpplibs::mtest; +namespace s = mcpp::test::support; +namespace fs = std::filesystem; + +namespace { + +// git in `dir`, as the scripts' `git -c user.name=... -c user.email=...`; its last line. +std::string git(const fs::path& dir, std::vector args) { + const auto program = s::on_path("git"); + if (!program) skip("git is not installed"); + args.insert(args.begin(), {"-C", dir.string(), "-c", "user.name=test", "-c", "user.email=test@local"}); + auto r = s::run(*program, args); + expect(fatal(e2e::succeeded(r))) << "git failed"; + return s::last_line(r.out); +} + +// `mcpp new ` whose files become those of `dir` (a repository's work tree), a library +// exporting `.greet` that prints `said`. +void library_repository(const fs::path& dir, std::string_view name, std::string_view said, bool branch) { + fs::create_directories(dir); + git(dir, {"init", "--quiet"}); + if (branch) git(dir, {"checkout", "-B", "main", "--quiet"}); + auto made = s::project::scaffold(name); + for (const auto& e : fs::directory_iterator(made.dir())) + fs::rename(e.path(), dir / e.path().filename()); + s::project lib{dir, {}}; + lib.write("src/greet.cppm", std::format("export module {}.greet;\nimport std;\nexport auto greet() -> void {{ std::println(\"{}\"); }}\n", name, said)); + lib.remove("src/main.cpp"); + lib.write("mcpp.toml", std::format("[package]\nname = \"{0}\"\nversion = \"0.1.0\"\n\n[language]\nstandard = \"c++23\"\nmodules = true\nimport_std = true\n\n[modules]\nsources = [\"src/**/*.cppm\"]\n\n[targets.{0}]\nkind = \"lib\"\n", name)); + git(dir, {"add", "-A"}); + git(dir, {"commit", "--quiet", "-m", "init"}); +} + +bool failed(const e2e::outcome& r) { return r.started && r.status != 0; } + +E2E git_deps{"git", [] { + "24 git dependency"_test[tag{"requires:fresh-sandbox"}] = [] { + // A git dependency is cloned to $MCPP_HOME/git// and treated as a path dependency; + // a rebuild does not clone again; a branch is locked (`git+...branch=main`), replayed + // offline from the lock, kept at the locked commit when the clone cache is evicted, and + // advanced by `mcpp update`. HOME is the case's own, so the caches resolve there. + s::needs({"fresh-sandbox"}); + const auto tmp = temp_dir(); + const auto mhome = s::case_home(s::home::inherited); + fs::create_directories(tmp / "home"); + auto opts = [&](std::vector>> extra = {}) { + s::options o{.where = s::home::inherited, .env = {{"HOME", (tmp / "home").string()}}}; + for (auto& e : extra) o.env.push_back(std::move(e)); + return o; + }; + auto program = [](const s::project& p, std::string_view name) { + auto bins = s::glob(p.at("target"), std::format("*/*/bin/{}", name)); + expect(fatal(!bins.empty())) << "no program built"; + return s::run(bins.front()).out; + }; + auto app_manifest = [](std::string_view name, std::string_view dep, std::string_view source) { + return std::format("[package]\nname = \"{0}\"\nversion = \"0.1.0\"\n\n[language]\nstandard = \"c++23\"\nmodules = true\nimport_std = true\n\n[modules]\nsources = [\"src/**/*.cppm\", \"src/**/*.cpp\"]\n\n[targets.{0}]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n\n[dependencies.{1}]\n{2}\n", name, dep, source); + }; + library_repository(tmp / "origin", "mylibA", "hello from git dep", false); + const auto head = git(tmp / "origin", {"rev-parse", "HEAD"}); + s::project app{tmp / "myapp", {{"src/main.cpp", "import std;\nimport mylibA.greet;\nint main() { greet(); return 0; }\n"}}}; + app.write("mcpp.toml", app_manifest("myapp", "mylibA", std::format("git = \"{}\"\nrev = \"{}\"", (tmp / "origin").generic_string(), head))); + app.mcpp({"build"}, opts()); + expect(e2e::contains(program(app, "myapp"), "hello from git dep")) << "git dep module not invoked"; + expect(fatal(fs::is_directory(mhome / "git"))) << "$MCPP_HOME/git not created"; + expect(!fs::is_empty(mhome / "git")) << "git cache empty"; + expect(s::lacks(s::text(app.mcpp({"build"}, opts())), "Cloning")) << "re-cloned on rebuild"; + + // A branch. + library_repository(tmp / "branch-origin", "branchlib", "branch dep v1", true); + s::project bapp{tmp / "branchapp", {{"src/main.cpp", "import std;\nimport branchlib.greet;\nint main() { greet(); return 0; }\n"}}}; + bapp.write("mcpp.toml", app_manifest("branchapp", "branchlib", std::format("git = \"{}\"\nbranch = \"main\"", (tmp / "branch-origin").generic_string()))); + bapp.mcpp({"build"}, opts()); + expect(e2e::contains(program(bapp, "branchapp"), "branch dep v1")) << "branch dep v1 not invoked"; + bapp.mcpp({"clean"}, opts()); + auto b2 = bapp.mcpp({"build"}, opts({{"MCPP_OFFLINE", "1"}})); + expect(fatal(e2e::succeeded(b2))) << "offline rebuild with lock + cache present failed"; + expect(e2e::contains(s::text(b2), "from mcpp.lock")) << "branch dep not resolved from mcpp.lock on rebuild"; + expect(s::lacks(s::text(b2), "Cloning")) << "branch dep re-cloned on rebuild"; + const auto lock = bapp.read("mcpp.lock"); + expect(e2e::contains(lock, "source = \"git+")) << "git dep lock source is not marked as git"; + expect(e2e::contains(lock, "branch=main")) << "git dep lock source does not record branch ref"; + expect(s::lacks(lock, "source = \"index+")) << "git dep lock source incorrectly uses index source"; + // v2 upstream; the clone cache evicted: the lock holds v1. + s::project{tmp / "branch-origin", {}}.write("src/greet.cppm", "export module branchlib.greet;\nimport std;\nexport auto greet() -> void { std::println(\"branch dep v2\"); }\n"); + git(tmp / "branch-origin", {"add", "-A"}); + git(tmp / "branch-origin", {"commit", "--quiet", "-m", "v2"}); + fs::remove_all(mhome / "git"); + bapp.mcpp({"clean"}, opts()); + expect(fatal(e2e::succeeded(bapp.mcpp({"build"}, opts())))) << "rebuild after git-cache eviction"; + expect(e2e::contains(program(bapp, "branchapp"), "branch dep v1")) << "git-cache eviction silently advanced the locked branch"; + // `mcpp update` advances it. + bapp.mcpp({"update", "branchlib"}, opts()); + bapp.mcpp({"clean"}, opts()); + bapp.mcpp({"build"}, opts()); + expect(e2e::contains(program(bapp, "branchapp"), "branch dep v2")) << "branch dep did not refresh after mcpp update"; + }; + + "661 one identity two declarations"_test[tag{"requires:gcc"}] = [] { + // Two declarations of one dependency identity: the root's reference wins over a + // library's, warned and locked; with no root opinion the first-resolved declaration wins + // for every edge; a kind clash between two non-root requesters is refused with the root + // hint; a root pin below a library's SemVer floor is refused naming both; the same + // reference declared twice is silent, under --strict too. + s::needs({"gcc"}); + const auto tmp = temp_dir(); + const auto fw_git = tmp / "framework-git"; + const auto fw_host = fw_git.generic_string(); + s::project fw{fw_git, {{"mcpp.toml", "[package]\nname = \"framework\"\nversion = \"0.1.0\"\n[build]\nsources = [\"src/*.c\"]\n[targets.framework]\nkind = \"lib\"\n"}}}; + git(fw_git, {"init", "--quiet"}); + auto commit_marker = [&](int marker, std::string_view message) { + fw.write("src/framework.c", std::format("int framework_marker(void) {{ return {}; }}\n", marker)); + if (message.empty()) return std::string{}; + git(fw_git, {"add", "-A"}); + git(fw_git, {"commit", "--quiet", "-m", std::string(message)}); + return git(fw_git, {"rev-parse", "HEAD"}); + }; + const auto rev_a = commit_marker(101, "A"); + const auto rev_b = commit_marker(102, "B"); + const auto rev_c = commit_marker(103, "C"); + commit_marker(199, ""); + const auto fw_v020 = tmp / "framework-v020"; + s::project{fw_v020, {{"mcpp.toml", "[package]\nname = \"framework\"\nversion = \"0.2.0\"\n[build]\nsources = [\"src/*.c\"]\n[targets.framework]\nkind = \"lib\"\n"}, + {"src/framework.c", "int framework_marker(void) { return 20; }\n"}}}; + auto make_lib = [](const fs::path& dir, std::string_view name, std::string_view extra) { + s::project lib{dir, {}}; + lib.write("mcpp.toml", std::format("[package]\nname = \"{0}\"\nversion = \"0.1.0\"\n\n[build]\nsources = [\"src/*.c\"]\n\n[targets.{0}]\nkind = \"lib\"\n\n{1}\n", name, extra)); + lib.write(std::format("src/{}.c", name), std::format("extern int framework_marker(void);\nint {}_marker(void) {{ return framework_marker(); }}\n", name)); + }; + auto git_at = [&](std::string_view rev) { return std::format("[dependencies.framework]\ngit = \"{}\"\nrev = \"{}\"", fw_host, rev); }; + auto app = [&](const fs::path& dir, std::string_view deps, std::string_view main) { + return s::project{dir, {{"mcpp.toml", std::format("[package]\nname = \"app\"\nversion = \"0.1.0\"\n[build]\nsources = [\"src/*.cpp\"]\n[targets.app]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n{}", deps)}, + {"src/main.cpp", main}}}; + }; + auto printing = [](std::vector libs) { + std::string decls, format, args; + for (auto l : libs) { + decls += std::format("extern \"C\" int {}_marker(void);\n", l); + format += std::format("{}{}=%d", format.empty() ? "" : " ", l); + args += std::format(", {}_marker()", l); + } + return std::format("#include \n{}int main() {{\n std::printf(\"{}\\n\"{});\n return 0;\n}}\n", decls, format, args); + }; + + // 1. The root's git rev wins over a library's, warned, locked, built against A. + make_lib(tmp / "case1/libb", "libb", git_at(rev_b)); + auto a1 = app(tmp / "case1/app", std::format("{}\n[dependencies.libb]\npath = \"../libb\"\n", git_at(rev_a)), printing({"libb"})); + auto r1 = a1.mcpp({"run"}); + expect(fatal(e2e::succeeded(r1))) << "case 1: exit " << r1.status; + expect(e2e::contains(s::text(r1), "app")) << "case 1: warning does not name the root"; + expect(e2e::contains(s::text(r1), "libb")) << "case 1: warning does not name the library"; + expect(e2e::contains(s::text(r1), rev_a)) << "case 1: warning does not name rev A"; + expect(e2e::contains(s::text(r1), rev_b)) << "case 1: warning does not name rev B"; + expect(e2e::contains(s::text(r1), "libb=101")) << "case 1: program did not print A's marker"; + expect(e2e::contains(a1.read("mcpp.lock"), rev_a)) << "case 1: mcpp.lock does not record A"; + + // 2. The root's path wins over a library's git rev; the program sees the working tree. + make_lib(tmp / "case2/libg", "libg", git_at(rev_b)); + auto a2 = app(tmp / "case2/app", std::format("[dependencies.framework]\npath = \"{}\"\n[dependencies.libg]\npath = \"../libg\"\n", fw_host), printing({"libg"})); + auto r2 = a2.mcpp({"run"}); + expect(fatal(e2e::succeeded(r2))) << "case 2: exit " << r2.status; + expect(e2e::contains(s::text(r2), "libg")) << "case 2: warning does not name the library"; + expect(e2e::contains(s::text(r2), "libg=199")) << "case 2: program did not print the working tree's marker"; + + // 3. No root opinion: the first-dequeued declaration wins for both edges. + make_lib(tmp / "case3/libb", "libb", git_at(rev_b)); + make_lib(tmp / "case3/libc", "libc", git_at(rev_c)); + auto a3 = app(tmp / "case3/app", "[dependencies.libb]\npath = \"../libb\"\n[dependencies.libc]\npath = \"../libc\"\n", printing({"libb", "libc"})); + auto r3 = a3.mcpp({"run"}); + expect(fatal(e2e::succeeded(r3))) << "case 3: exit " << r3.status; + expect(e2e::contains(s::text(r3), "libb")) << "case 3: warning does not name libb"; + expect(e2e::contains(s::text(r3), "libc")) << "case 3: warning does not name libc"; + expect(e2e::contains(s::text(r3), "libb=102 libc=102")) << "case 3: both libraries did not converge on the first-resolved framework (B)"; + + // 4. A kind clash between two non-root requesters is refused, with the hint. + make_lib(tmp / "case4/libd", "libd", std::format("[dependencies.framework]\npath = \"{}\"", fw_v020.generic_string())); + make_lib(tmp / "case4/libe", "libe", git_at(rev_b)); + auto a4 = app(tmp / "case4/app", "[dependencies.libd]\npath = \"../libd\"\n[dependencies.libe]\npath = \"../libe\"\n", "int main() { return 0; }\n"); + auto r4 = a4.mcpp({"build"}); + expect(fatal(failed(r4))) << "case 4: expected a refusal but the build succeeded"; + expect(e2e::contains(s::text(r4), "Pick one")) << "case 4: refusal message missing 'Pick one'"; + expect(e2e::contains(s::text(r4), "in the root to settle it")) << "case 4: refusal is missing the new root hint"; + expect(e2e::contains(s::text(r4), "libd")) << "case 4: refusal does not name libd"; + expect(e2e::contains(s::text(r4), "libe")) << "case 4: refusal does not name libe"; + + // 5. A root pin below a library's SemVer floor is refused, naming both. + s::project{tmp / "case5/local-index", {{"pkgs/f/framework.lua", R"cpp(package = { + spec = "1", + name = "framework", + description = "SemVer side of the 661 fixture -- never actually fetched", + licenses = {"MIT"}, + type = "package", + xpm = { + linux = { + ["0.5.0"] = { + url = "https://example.invalid/framework-0.5.0.tar.gz", + sha256 = "0000000000000000000000000000000000000000000000000000000000000000", + }, + }, + }, + mcpp = { + language = "c++23", + import_std = false, + sources = { "src/*.c" }, + targets = { ["framework"] = { kind = "lib" } }, + deps = {}, + }, +} +)cpp"}}}; + make_lib(tmp / "case5/libv", "libv", "[dependencies]\nframework = \">=0.3\""); + auto a5 = app(tmp / "case5/app", std::format("[indices]\ndefault = {{ path = \"{}\" }}\n[dependencies.framework]\npath = \"{}\"\n[dependencies.libv]\npath = \"../libv\"\n", (tmp / "case5/local-index").generic_string(), fw_v020.generic_string()), "int main() { return 0; }\n"); + auto r5 = a5.mcpp({"build"}); + expect(fatal(failed(r5))) << "case 5: expected a refusal but the build succeeded"; + expect(e2e::contains(s::text(r5), "0.2.0")) << "case 5: refusal does not name the checkout's version"; + expect(e2e::contains(s::text(r5), ">=0.3")) << "case 5: refusal does not name the requirement"; + expect(e2e::contains(s::text(r5), "app")) << "case 5: refusal does not name the root"; + expect(e2e::contains(s::text(r5), "libv")) << "case 5: refusal does not name the library"; + + // 6. The same reference declared twice is silent, and --strict stays green. + make_lib(tmp / "case6/libf", "libf", git_at(rev_a)); + auto a6 = app(tmp / "case6/app", std::format("{}\n[dependencies.libf]\npath = \"../libf\"\n", git_at(rev_a)), printing({"libf"})); + auto r6 = a6.mcpp({"build", "--strict"}); + expect(fatal(e2e::succeeded(r6))) << "case 6: --strict refused an identical, repeated declaration"; + expect(s::lacks(s::text(r6), "wins")) << "case 6: an identical reference declared twice still produced an override warning"; + }; + + "713 a git dependency selects a repository member"_test[tag{"requires:gcc"}] = [] { + // A git dependency whose key names a package other than the repository's root package + // selects the `[workspace] members` entry declaring it; a second declaration of one + // dependency by one consumer keeps its requests; a git dependency's compile banner names + // its reference. + s::needs({"gcc"}); + const auto tmp = temp_dir(); + const s::options linked{.where = s::home::linked}; + s::project repo{tmp / "repo", {{"mcpp.toml", "[package]\nname = \"fw\"\nnamespace = \"spike\"\nversion = \"0.1.0\"\n[targets.fw]\nkind = \"lib\"\n[build]\nsources = [\"src/*.cpp\"]\n[workspace]\nmembers = [\"tool\"]\n"}, + {"src/fw.cpp", "int fw_answer() { return 42; }\n"}, + {"tool/mcpp.toml", "[package]\nname = \"fw-installer\"\nnamespace = \"spike\"\nversion = \"0.1.0\"\n[targets.fw-installer]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n[dependencies]\nspike.fw = { path = \"..\" }\n"}, + {"tool/src/main.cpp", "#include \nint fw_answer();\nint main() { std::printf(\"fw-installer ran %d\\n\", fw_answer()); return 0; }\n"}}}; + git(repo.dir(), {"init", "-q"}); + git(repo.dir(), {"add", "-A"}); + git(repo.dir(), {"commit", "-qm", "fixture"}); + const auto rev = git(repo.dir(), {"rev-parse", "HEAD"}); + const auto url = "file://" + repo.dir().generic_string(); + auto write_app = [&](std::string_view name, std::string_view manifest) { + return s::project{tmp / name, {{"mcpp.toml", manifest}, {"src/main.cpp", "int main() { return 0; }\n"}, + {"build.mcpp", R"cpp(import std; +import mcpp; +int main() { + const char* a = mcpp::dep_bin("spike.fw-installer", "fw-installer"); + mcpp::warning((std::string("DEPBIN tool=[") + (a ? a : "") + "]").c_str()); + return 0; +} +)cpp"}}}; + }; + auto member = write_app("member", std::format("[package]\nname = \"member\"\nversion = \"0.1.0\"\n[targets.member]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n[dependencies]\nspike.fw-installer = {{ git = \"{}\", rev = \"{}\", tools = [\"fw-installer\"] }}\n", url, rev)); + auto b1 = member.mcpp({"build"}, linked); + expect(fatal(e2e::succeeded(b1))) << "a git dependency naming a repository member was refused"; + std::smatch m; + const auto t1 = s::text(b1); + const bool named = std::regex_search(t1, m, std::regex{R"(DEPBIN tool=\[([^\]]*))"}); + const std::string tool = named ? m[1].str() : ""; + expect(fatal(!tool.empty())) << "the member's tool was not provided"; + expect(eq(s::last_line(s::run(tool).out), std::string{"fw-installer ran 42"})) << "the member's tool did not run"; + + auto both = write_app("both", std::format("[package]\nname = \"both\"\nversion = \"0.1.0\"\n[targets.both]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n[dependencies]\nspike.fw = {{ git = \"{0}\", rev = \"{1}\" }}\nspike.fw-installer = {{ git = \"{0}\", rev = \"{1}\", tools = [\"fw-installer\"] }}\n", url, rev)); + both.write("src/main.cpp", "int fw_answer();\nint main() { return fw_answer() == 42 ? 0 : 1; }\n"); + auto b2 = both.mcpp({"build", "-v"}, linked); + expect(fatal(e2e::succeeded(b2))) << "the root and a member by one revision did not build"; + expect(s::lacks(s::text(b2), "declared identity is used")) << "the member's key adopted the root's identity"; + expect(s::has_line(s::text(b2), R"(DEPBIN tool=\[[^\]])")) << "the member's tools request was lost"; + expect(s::no_line(s::text(b2), R"(Compiling spike\.fw v( |$))")) << "the git banner printed an empty version"; + expect(s::has_line(s::text(b2), std::format(R"(spike\.fw v[^ ]+ \(git rev {}\))", rev.substr(0, 12)))) << "the git banner does not name the reference"; + expect(e2e::succeeded(both.mcpp({"run"}, linked))) << "the application over the git root does not run"; + + // A path dependency declared in two tables, the second asking for its tools. + s::project{tmp / "local", {{"mcpp.toml", "[package]\nname = \"fw-installer\"\nnamespace = \"spike\"\nversion = \"0.1.0\"\n[targets.fw-installer]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n[dependencies]\n\n"}, + {"src/main.cpp", "#include \nint main() { std::puts(\"local tool\"); return 0; }\n"}}}; + auto twice = write_app("twice", "[package]\nname = \"twice\"\nversion = \"0.1.0\"\n[targets.twice]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n[dependencies]\nspike.fw-installer = { path = \"../local\" }\n[build-dependencies]\nspike.fw-installer = { path = \"../local\", tools = [\"fw-installer\"] }\n"); + auto b3 = twice.mcpp({"build"}, linked); + expect(fatal(e2e::succeeded(b3))) << "a dependency declared in two tables did not build"; + expect(s::has_line(s::text(b3), R"(DEPBIN tool=\[[^\]])")) << "the second declaration's tools request was dropped"; + }; + + "816 a git dependency reports its fetch"_test[tag{"requires:unix-shell"}] = [] { + // Fetching a git dependency is reported with the renderer every other acquisition uses: + // a start and a finish line when stdout is not a terminal, without CR or ESC bytes. + s::needs({"unix-shell"}); + const auto tmp = temp_dir(); + s::project repo{tmp / "repo", {{"mcpp.toml", "[package]\nnamespace = \"probe816\"\nname = \"gdep\"\nversion = \"0.1.0\"\n[targets.gdep]\nkind = \"lib\"\n"}, + {"src/gdep.cppm", "export module probe816.gdep;\nexport int gdep_value() { return 7; }\n"}}}; + git(repo.dir(), {"init", "-q", "-b", "main", "."}); + git(repo.dir(), {"add", "-A"}); + git(repo.dir(), {"commit", "-qm", "init"}); + const auto url = "file://" + repo.dir().generic_string(); + s::project consumer{tmp / "consumer", {{"src/main.cpp", "import probe816.gdep;\nint main() { return gdep_value() == 7 ? 0 : 1; }\n"}}}; + consumer.write("mcpp.toml", std::format("[package]\nname = \"consumer\"\nversion = \"0.1.0\"\n[dependencies]\n\"probe816.gdep\" = {{ git = \"{}\", branch = \"main\" }}\n", url)); + auto b = consumer.mcpp({"build"}); + expect(fatal(e2e::succeeded(b))) << "the build failed"; + expect(e2e::contains(s::text(b), "Fetching " + url)) << "A: no start line for the fetch"; + auto escaped = std::regex_replace(url, std::regex{R"([.^$|()\[\]{}*+?\\])"}, R"(\$&)"); + expect(s::has_line(s::text(b), "Fetching " + escaped + ".* done")) << "A: no finish line for the fetch"; + expect(s::lacks(s::text(b), "\r")) << "B: the log carries a carriage return"; + expect(s::lacks(s::text(b), "\x1b")) << "B: the log carries an ESC byte"; + }; + + "943 a transitive git branch is locked"_test[tag{"requires:gcc"}] = [] { + // A git branch reached through another package is locked as the root's own are: a later + // commit on it is not taken until `mcpp update`. + s::needs({"gcc"}); + const auto tmp = temp_dir(); + auto repo = [](const fs::path& dir) { + git(dir, {"init", "--quiet", "-b", "main"}); + git(dir, {"add", "-A"}); + git(dir, {"commit", "--quiet", "-m", "init"}); + }; + s::project leaf{tmp / "leaf", {{"mcpp.toml", "[package]\nname = \"leaf\"\nversion = \"0.1.0\"\n[targets.leaf]\nkind = \"lib\"\n"}, + {"src/leaf.cppm", "export module leaf;\nexport int leaf_value() { return 1; }\n"}}}; + repo(leaf.dir()); + s::project mid{tmp / "mid", {{"src/mid.cppm", "export module mid;\nimport leaf;\nexport int mid_value() { return leaf_value(); }\n"}}}; + mid.write("mcpp.toml", std::format("[package]\nname = \"mid\"\nversion = \"0.1.0\"\n[targets.mid]\nkind = \"lib\"\n[dependencies]\nleaf = {{ git = \"file://{}\", branch = \"main\" }}\n", leaf.dir().generic_string())); + repo(mid.dir()); + s::project app{tmp / "app", {{"src/main.cpp", "import mid;\n#include \nint main() { std::printf(\"value %d\\n\", mid_value()); }\n"}}}; + app.write("mcpp.toml", std::format("[package]\nname = \"app\"\nversion = \"0.1.0\"\n[targets.app]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n[dependencies]\nmid = {{ git = \"file://{}\", branch = \"main\" }}\n", mid.dir().generic_string())); + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "the first build"; + const auto leaf_commit = git(leaf.dir(), {"rev-parse", "HEAD"}); + expect(fatal(e2e::contains(app.read("mcpp.lock"), "branch=main@" + leaf_commit))) << "A: mcpp.lock does not hold the transitive branch's commit"; + leaf.write("src/leaf.cppm", "export module leaf;\nexport int leaf_value() { return 2; }\n"); + git(leaf.dir(), {"commit", "--quiet", "-am", "two"}); + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "B: the second build"; + auto run = app.mcpp({"run"}); + expect(fatal(e2e::succeeded(run))) << "B: the program did not run"; + expect(e2e::contains(s::text(run), "value 1")) << "B: the build moved to the branch's new tip"; + expect(fatal(e2e::succeeded(app.mcpp({"update"})))) << "C: mcpp update"; + auto run2 = app.mcpp({"run"}); + expect(fatal(e2e::succeeded(run2))) << "C: the program did not run after the update"; + expect(e2e::contains(s::text(run2), "value 2")) << "C: the build after mcpp update did not take the branch's tip"; + }; + + "944 a root checkout answers a version the index lacks"_test[tag{"requires:gcc"}] = [] { + // A dependency's request by version is answered by the root's path or git checkout of + // the package, whether or not the index carries it; an unsatisfied requirement is still + // refused. + s::needs({"gcc"}); + const auto tmp = temp_dir(); + const auto host = tmp.generic_string(); + const std::string os = s::on_linux() ? "linux" : s::on_macos() ? "macos" : "windows"; + s::project ws{tmp / "ws", {{"mcpp.toml", "[workspace]\nmembers = [\"m944leaf\"]\n"}, + {"m944leaf/mcpp.toml", "[package]\nname = \"m944leaf\"\nversion = \"0.1.0\"\n\n[targets.m944leaf]\nkind = \"lib\"\n"}, + {"m944leaf/src/leaf.cppm", "export module m944leaf;\nexport int leaf() { return 1; }\n"}}}; + git(ws.dir(), {"init", "--quiet", "-b", "main"}); + git(ws.dir(), {"add", "-A"}); + git(ws.dir(), {"commit", "--quiet", "-m", "init"}); + s::project mid{tmp / "mid", {{"src/mid.cppm", "export module m944mid;\nimport m944leaf;\nexport int mid() { return leaf(); }\n"}}}; + auto mid_requires = [&](std::string_view version) { + mid.write("mcpp.toml", std::format("[package]\nname = \"m944mid\"\nversion = \"0.1.0\"\n[targets.m944mid]\nkind = \"lib\"\n[target.'cfg(os = \"{}\")'.dependencies]\nm944leaf = \"{}\"\n", os, version)); + }; + auto app = [&](std::string_view dir, std::string_view leaf) { + s::project a{tmp / dir, {{"src/main.cpp", "import m944mid;\nint main() { return mid() - 1; }\n"}}}; + a.write("mcpp.toml", std::format("[package]\nname = \"app\"\nversion = \"0.1.0\"\n[targets.app]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n[dependencies]\nm944mid = {{ path = \"{0}/mid\" }}\n[target.'cfg(os = \"{1}\")'.dependencies]\nm944leaf = {2}\n", host, os, leaf)); + return a; + }; + mid_requires("0.1.0"); + auto a = app("a", std::format("{{ path = \"{}/ws/m944leaf\" }}", host)).mcpp({"build"}); + expect(fatal(e2e::succeeded(a))) << "A: the root's path checkout did not answer"; + expect(s::lacks(s::text(a), "no package found")) << "A: the index was asked"; + expect(e2e::succeeded(app("b", std::format("{{ git = \"file://{}/ws\", branch = \"main\" }}", host)).mcpp({"build"}))) << "B: the root's git checkout did not answer"; + mid_requires("0.2.0"); + auto c = app("c", std::format("{{ path = \"{}/ws/m944leaf\" }}", host)).mcpp({"build"}); + expect(fatal(failed(c))) << "C: a requirement the checkout does not satisfy was accepted"; + expect(e2e::contains(s::text(c), "requires 0.2.0")) << "C: the refusal does not name the requirement"; + }; + + "1048 a git reference follows the default branch"_test[tag{"requires:elf"}][tag{"requires:llvm"}] = [] { + // A git dependency naming no reference follows the repository's default branch (here + // `trunk`), reported, locked with its commit and read offline; two references are + // refused; `--platform git+#tag=` takes a platform from its repository. + s::needs({"elf", "llvm"}); + const auto tmp = temp_dir(); + auto repo = [](const fs::path& dir, std::string_view message) { + git(dir, {"-c", "init.defaultBranch=trunk", "init", "-q", "."}); + git(dir, {"add", "-A"}); + git(dir, {"commit", "-qm", std::string(message)}); + }; + s::project lib{tmp / "lib", {{"mcpp.toml", "[package]\nname = \"gitlib\"\nversion = \"0.1.0\"\n[targets.gitlib]\nkind = \"lib\"\n"}, + {"src/lib.cpp", "int gitlib_value() { return 9; }\n"}}}; + repo(lib.dir(), "init"); + const auto lib_url = lib.dir().generic_string(); + s::project app{tmp / "app", {{"src/main.cpp", "int gitlib_value(); int main() { return gitlib_value(); }\n"}}}; + app.write("mcpp.toml", std::format("[package]\nname = \"app\"\nversion = \"0.1.0\"\n[dependencies]\ngitlib = {{ git = \"{}\" }}\n", lib_url)); + auto b = app.mcpp({"build"}); + expect(fatal(e2e::succeeded(b))) << "the build failed"; + expect(s::has_line(s::text(b), "the default branch of .* is trunk")) << "the default branch is not reported"; + auto bins = s::glob(app.at("target"), "*/*/bin/app"); + expect(fatal(!bins.empty())) << "no program built"; + auto ran = s::run(bins.front()); + expect(eq(ran.status, 9)) << "the program returned " << ran.status << ", not 9"; + expect(s::has_line(app.read("mcpp.lock"), R"(source = "git\+.*#branch=trunk@[0-9a-f]{40}")")) << "the lock does not record the branch and its commit"; + app.remove("target"); + expect(e2e::succeeded(app.mcpp({"build", "--offline"}))) << "the offline build did not read the lock"; + app.write("mcpp.toml", std::format("[package]\nname = \"app\"\nversion = \"0.1.0\"\n[dependencies]\ngitlib = {{ git = \"{}\", tag = \"v1\", branch = \"trunk\" }}\n", lib_url)); + expect(e2e::contains(s::text(app.mcpp({"build"})), "names 2 of rev / tag / branch; a git dependency names at most one")) << "two refs were not refused"; + + s::project platform{tmp / "toy-platform", {}}; + platform.write("mcpp.toml", std::format(R"cpp([package] +name = "toy-platform" +version = "0.1.0" +[platform] +toolchain = "llvm@{}" +[[platform.define]] +triple = "x86_64-toyos" +clang-triple = "x86_64-unknown-linux-gnu" +object-format = "elf" +execution = "linux" +[[platform.provider]] +serves = ["x86_64-toyos"] +[platform.build] +cflags = ["-ffreestanding", "-fno-stack-protector", "-fno-pic"] +[platform.link] +driver = "compiler" +[platform.layers] +"c-abi" = false +"c++-abi" = false +"compiler-runtime" = false +"kernel-abi" = false +)cpp", s::llvm_version())); + repo(platform.dir(), "toy"); + git(platform.dir(), {"tag", "v1"}); + s::project bare{tmp / "bare", {{"mcpp.toml", "[package]\nname = \"bare\"\nversion = \"0.1.0\"\n[build]\nsources = [\"src/*.c\"]\nldflags = [\"-nostdlib\", \"-static\", \"-no-pie\"]\n[targets.bare]\nkind = \"bin\"\nmain = \"src/main.c\"\n"}, + {"src/main.c", "void _start(void) { __asm__ volatile(\"mov $60, %eax\\n mov $7, %edi\\n syscall\"); }\n"}}}; + auto p = bare.mcpp({"build", "--target", "x86_64-toyos", "--platform", std::format("git+{}#tag=v1", platform.dir().generic_string())}); + expect(fatal(e2e::succeeded(p))) << "the platform from git did not build its row"; + expect(e2e::contains(s::text(p), "Platform toy-platform 0.1.0")) << "the platform is not reported"; + }; +}}; + +} // namespace diff --git a/tests/e2e/deps/graph.cpp b/tests/e2e/deps/graph.cpp new file mode 100644 index 000000000..cae57a23a --- /dev/null +++ b/tests/e2e/deps/graph.cpp @@ -0,0 +1,260 @@ +// The dependency graph as a whole: a miss anywhere in it asks for a refresh, an upgrade restages, +// one name in two namespaces, merges and overrides (formerly tests/e2e/945, 948, 980, 1018, 1066, +// 1071). +import std; +import mcpplibs.mtest; +import mcpp.test.support; + +using namespace mcpplibs::mtest; +namespace s = mcpp::test::support; +namespace fs = std::filesystem; + +namespace { + +bool failed(const e2e::outcome& r) { return r.started && r.status != 0; } + +E2E graph{"graph", [] { + "945 a miss anywhere in the graph asks for a refresh"_test[tag{"requires:gcc"}][tag{"requires:fresh-sandbox"}] = [] { + // A request by version the local index cannot answer asks for a refresh wherever it is + // in the graph, not only in the root's [dependencies]: declined offline, declined inside + // the debounce window, and the refusal names the selector and the index. + s::needs({"gcc", "fresh-sandbox"}); + const auto index = s::case_home(s::home::inherited) / "indices/mcpplibs"; + fs::create_directories(index / "pkgs/w"); + s::project idx{index, {{".xlings-index-version", "abc1234\n"}, {".mcpp-index-updated", ""}}}; + fs::last_write_time(idx.at(".mcpp-index-updated"), fs::file_time_type::clock::now() - std::chrono::days{30}); + const auto tmp = temp_dir(); + s::project{tmp / "mid", {{"mcpp.toml", "[package]\nname = \"mid\"\nversion = \"0.1.0\"\n[targets.mid]\nkind = \"lib\"\n[dependencies]\nabsentpkg = \"1.0.0\"\n"}, + {"src/mid.cppm", "export module mid;\nexport int mid_value() { return 1; }\n"}}}; + s::project app{tmp / "app", {{"mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n[targets.app]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n[dependencies]\nmid = { path = \"../mid\" }\n"}, + {"src/main.cpp", "int main() { return 0; }\n"}}}; + auto a = app.mcpp({"build"}, {.where = s::home::inherited, .env = {{"MCPP_VERBOSE", "1"}, {"MCPP_OFFLINE", "1"}}}); + expect(fatal(failed(a))) << "A: the build resolved a package no index carries"; + expect(s::has_line(s::text(a), R"(skip refresh for (mcpplibs[.:])?absentpkg: offline mode)")) << "A: the miss in a path dependency did not reach the refresh policy"; + idx.write(".mcpp-index-updated", ""); + auto b = app.mcpp({"build"}, {.where = s::home::inherited, .env = {{"MCPP_VERBOSE", "1"}}}); + expect(fatal(failed(b))) << "B: the build resolved a package no index carries"; + expect(s::has_line(s::text(b), R"(skip refresh for (mcpplibs[.:])?absentpkg: index was just refreshed)")) << "B: the debounce did not decide the miss in a path dependency"; + expect(s::no_line(s::text(b), "Refreshing +package index")) << "B: a sync inside the debounce window"; + expect(e2e::contains(s::text(b), "dependency 'absentpkg': no package found for exact selector")) << "C: the refusal does not name the selector"; + expect(e2e::contains(s::text(b), "index: local index abc1234")) << "C: the refusal does not name the index"; + }; + + "948 an upgraded dependency is restaged at the same size"_test[tag{"requires:gcc"}][tag{"requires:fresh-sandbox"}] = [] { + // A dependency upgraded to a version the build cache already holds is staged over the + // previous version's files, whatever their size (here the BMI's size does not change). + s::needs({"gcc", "fresh-sandbox"}); + const auto tmp = temp_dir(); + const s::options inherited{.where = s::home::inherited}; + s::project{tmp / "local-index", {{"pkgs/c/constant.lua", R"cpp(package = { + spec = "1", + name = "constant", + description = "One constant", + licenses = {"MIT"}, + type = "package", + xpm = { + linux = { + ["1.0.0"] = { + url = "https://example.invalid/constant-1.0.0.tar.gz", + sha256 = "0000000000000000000000000000000000000000000000000000000000000000", + }, + ["1.0.1"] = { + url = "https://example.invalid/constant-1.0.1.tar.gz", + sha256 = "0000000000000000000000000000000000000000000000000000000000000000", + }, + }, + }, + mcpp = { + language = "c++23", + import_std = true, + sources = { "src/**/*.cppm" }, + targets = { ["constant"] = { kind = "lib" } }, + deps = {}, + }, +} +)cpp"}}}; + auto make_project = [&](std::string_view dir, std::string_view version) { + s::project p{tmp / dir, {{"src/main.cpp", "import std;\nimport constant;\nint main() { std::println(\"{} {}\", value, stated()); return 0; }\n"}}}; + for (auto [v, n] : {std::pair{"1.0.0", 41}, std::pair{"1.0.1", 42}}) + p.write(std::format(".mcpp/.xlings/data/xpkgs/local-dev.constant/{}/src/constant.cppm", v), + std::format("export module constant;\nexport constexpr int value = {};\nexport int stated() {{ return value; }}\n", n)); + p.write("mcpp.toml", std::format("[package]\nname = \"{0}\"\nversion = \"0.1.0\"\n[indices]\nlocal-dev = {{ path = \"{1}\" }}\n[dependencies]\n\"local-dev.constant\" = \"{2}\"\n[targets.{0}]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n", dir, (tmp / "local-index").generic_string(), version)); + return p; + }; + auto prints = [](const s::project& p, std::string_view name) { + auto bins = s::glob(p.at("target"), std::format("*/*/bin/{}", name)); + expect(fatal(!bins.empty())) << name << " built no program"; + auto out = s::run(bins.front()).out; + while (!out.empty() && out.back() == '\n') out.pop_back(); + return out; + }; + auto omega = make_project("omega", "1.0.1"); + expect(fatal(e2e::succeeded(omega.mcpp({"build"}, inherited)))) << "omega did not build"; + expect(fatal(eq(prints(omega, "omega"), std::string{"42 42"}))) << "omega's output"; + auto alpha = make_project("alpha", "1.0.0"); + expect(fatal(e2e::succeeded(alpha.mcpp({"build"}, inherited)))) << "alpha did not build"; + expect(fatal(eq(prints(alpha, "alpha"), std::string{"41 41"}))) << "alpha's output"; + std::optional bmi; + for (const auto& f : s::find_files(alpha.at("target"), "constant\\.gcm")) + if (!bmi && f.generic_string().find("gcm.cache") != std::string::npos) bmi = f; + expect(fatal(bmi.has_value())) << "no BMI of the dependency in the build directory"; + const auto before = fs::file_size(*bmi); + alpha.write("mcpp.toml", std::format("[package]\nname = \"alpha\"\nversion = \"0.1.0\"\n[indices]\nlocal-dev = {{ path = \"{}\" }}\n[dependencies]\n\"local-dev.constant\" = \"1.0.1\"\n[targets.alpha]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n", (tmp / "local-index").generic_string())); + auto up = alpha.mcpp({"build"}, inherited); + expect(fatal(e2e::succeeded(up))) << "the upgraded build failed"; + expect(fatal(s::has_line(s::text(up), R"(Cached local-dev\.constant v1\.0\.1)"))) << "1.0.1 was not served from the cache, so nothing here is staged"; + expect(fatal(eq(fs::file_size(*bmi), before))) << "the BMI's size changed; the case is not the same-size one"; + expect(eq(prints(alpha, "alpha"), std::string{"42 42"})) << "alpha's output after the upgrade"; + std::optional cached; + for (const auto& f : s::find_files(s::case_home(s::home::inherited) / "build-cache", "constant\\.gcm")) + if (!cached && f.generic_string().find("constant@1.0.1") != std::string::npos) cached = f; + expect(cached.has_value() && s::read_file(*bmi) == s::read_file(*cached)) << "the staged BMI is not 1.0.1's"; + }; + + "980 an ambiguous dependency name is no packages"_test[tag{"requires:elf"}][tag{"requires:gcc"}] = [] { + // Two dependencies whose manifests both say `rules`: the bare name is no package's — not + // published, not warned about on every run, MCPP_DEP_AMBIGUOUS_RULES names both — and + // each qualified name still reaches its own directory; dep_dir("rules") is refused. + s::needs({"elf", "gcc"}); + const auto tmp = temp_dir(); + for (std::string_view ns : {"openmui", "gongfu"}) { + s::project rules{tmp / std::format("{}-rules", ns), {}}; + rules.write("mcpp.toml", std::format("[package]\nnamespace = \"{}\"\nname = \"rules\"\nversion = \"0.1.0\"\n[modules]\nsources = [\"src/**/*.cpp\"]\n[targets.rules]\nkind = \"lib\"\n", ns)); + rules.write("src/r.cpp", std::format("int {}_rules_touch() {{ return 1; }}\n", ns)); + } + s::project app{tmp / "app", {{"src/main.cpp", "import std;\nextern const char* seen();\nint main() { std::println(\"{}\", seen()); }\n"}, + {"build.mcpp", R"cpp(#include +#include +#include +import mcpp; +int main() { + auto env = [](const char* n) { const char* v = std::getenv(n); return std::string(v ? v : ""); }; + std::string line = "bare=" + env("MCPP_DEP_RULES_DIR") + + " ambiguous=" + env("MCPP_DEP_AMBIGUOUS_RULES") + + " openmui=" + env("MCPP_DEP_OPENMUI_RULES_DIR") + + " gongfu=" + env("MCPP_DEP_GONGFU_RULES_DIR"); + std::string out = "src/seen.cpp"; + std::FILE* f = std::fopen(out.c_str(), "w"); + std::fprintf(f, "const char* seen() { return R\"(%s)\"; }\n", line.c_str()); + std::fclose(f); + mcpp::generated("src/seen.cpp"); + return 0; +} +)cpp"}, + {"mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n[modules]\nsources = [\"src/**/*.cpp\"]\n[dependencies]\nopenmui.rules = { path = \"../openmui-rules\" }\ngongfu.rules = { path = \"../gongfu-rules\" }\n[targets.app]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n"}}}; + auto b = app.mcpp({"build"}); + expect(fatal(e2e::succeeded(b))) << "build failed"; + expect(s::lacks(s::text(b), "collides on")) << "an ambiguous name still warns"; + const auto seen = s::last_line(app.mcpp({"run"}).out); + expect(e2e::contains(seen, "bare=")) << "the bare name is bound: " << seen; + expect(e2e::contains(seen, "ambiguous=gongfu.rules,openmui.rules") || e2e::contains(seen, "ambiguous=openmui.rules,gongfu.rules")) << "MCPP_DEP_AMBIGUOUS_RULES does not name both: " << seen; + expect(s::has_line(seen, "openmui=.*openmui-rules")) << "openmui.rules lost its directory: " << seen; + expect(s::has_line(seen, "gongfu=.*gongfu-rules")) << "gongfu.rules lost its directory: " << seen; + app.write("build.mcpp", "import mcpp;\nint main() { return mcpp::dep_dir(\"rules\")[0] ? 0 : 1; }\n"); + app.remove("src/seen.cpp"); + auto ask = app.mcpp({"build"}); + expect(fatal(failed(ask))) << "dep_dir(\"rules\") answered"; + expect(e2e::contains(s::text(ask), "dep_dir(\"rules\") names more than one dependency")) << "the program did not say the name is ambiguous"; + expect(e2e::contains(s::text(ask), "openmui.rules") && e2e::contains(s::text(ask), "gongfu.rules")) << "the refusal does not name both packages"; + }; + + "1018 a package and a namespaced package of one name both link"_test = [] { + // `foo` and `ns.foo` are two packages, each reached by the edge that names it (mcpp#896). + s::project ws{{{"mcpp.toml", "[workspace]\nmembers = [\"foo\", \"nsfoo\", \"app\"]\n"}, + {"foo/mcpp.toml", "[package]\nname = \"foo\"\nversion = \"0.1.0\"\n[targets.foo]\nkind = \"lib\"\n[build]\nsources = [\"src/*.cppm\", \"src/*.cpp\"]\n"}, + {"nsfoo/mcpp.toml", "[package]\nnamespace = \"ns\"\nname = \"foo\"\nversion = \"0.1.0\"\n[targets.ns-foo]\nkind = \"lib\"\n[build]\nsources = [\"src/*.cppm\", \"src/*.cpp\"]\n"}, + {"app/mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n[targets.app]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n[dependencies]\nfoo = { path = \"../foo\" }\nns.foo = { path = \"../nsfoo\" }\n"}, + {"foo/src/foo.cppm", "export module foo;\nexport namespace foo { int one(); }\n"}, + {"foo/src/foo.cpp", "module foo;\nnamespace foo { int one() { return 1; } }\n"}, + {"nsfoo/src/foo.cppm", "export module ns.foo;\nexport namespace ns::foo { int two(); }\n"}, + {"nsfoo/src/foo.cpp", "module ns.foo;\nnamespace ns::foo { int two() { return 2; } }\n"}, + {"app/src/main.cpp", "import std;\nimport foo;\nimport ns.foo;\nint main() { std::println(\"sum {}\", foo::one() + ns::foo::two()); }\n"}}}; + auto r = ws.mcpp({"run", "-p", "app"}); + expect(fatal(e2e::succeeded(r))) << "the member reaching foo and ns.foo did not link and run"; + expect(e2e::contains(s::text(r), "sum 3")) << "the program's output"; + }; + + "1066 a merged version withdraws its requests"_test = [] { + // The app asks for mcpplibs.os ^0.4.1 (the newest, over openkal 0.25); a path library pins + // 0.4.1 (over openkal 0.24). The merge takes os down to 0.4.1, and the requests the newer + // version made leave the graph with it. + const auto tmp = temp_dir(); + s::project{tmp / "anchor", {{"mcpp.toml", "[package]\nname = \"anchor\"\nversion = \"0.1.0\"\n[targets.anchor]\nkind = \"lib\"\n[dependencies]\nmcpplibs.os = \"0.4.1\"\n"}, + {"src/anchor.cppm", "export module anchor;\nexport int anchor_answer() { return 1; }\n"}}}; + s::project app{tmp / "app", {{"src/main.cpp", "import anchor;\nint main() { return anchor_answer() == 1 ? 0 : 1; }\n"}}}; + app.write("mcpp.toml", std::format("[package]\nname = \"app\"\nversion = \"0.1.0\"\n[dependencies]\nmcpplibs.os = \"^0.4.1\"\nanchor = {{ path = \"{}/anchor\" }}\n", tmp.generic_string())); + auto plan = app.mcpp({"build", "--configure-only"}); + expect(fatal(e2e::succeeded(plan))) << "the merged graph was refused"; + expect(s::lacks(s::text(plan), "irreconcilable")) << "a request of a replaced version was kept"; + expect(s::has_line(s::text(plan), R"(Merged mcpplibs\.os 0\.4\.[2-9] ⨯ 0\.4\.1 → v0\.4\.1)")) << "os was not merged down to the pinned version"; + // `grep -A2 '^[package.""]' mcpp.lock`: the header and the two lines after it. + const auto lock = app.read("mcpp.lock"); + auto entry = [&](std::string_view name) { + std::string out; + std::istringstream in{lock}; + int left = 0; + for (std::string line; std::getline(in, line);) { + if (line == std::format("[package.\"{}\"]", name)) left = 3; + if (left > 0) { out += line + "\n"; --left; } + } + return out; + }; + expect(e2e::contains(entry("openkal"), "version = \"0.24.0\"")) << "openkal is not the 0.24 the pinned os asks for"; + for (auto [name, series] : {std::pair{"openkal-linux", "0.26"}, std::pair{"openkal-macos", "0.23"}, std::pair{"openkal-windows", "0.21"}}) + expect(s::lacks(entry(name), std::format("version = \"{}.", series))) << name << " " << series << " of the replaced version is in the graph"; + }; + + "1071 the root overrides a dependency for the whole graph"_test = [] { + // Two requests no merge reconciles are refused naming the override line; the root's + // `override = true` chooses one version for the whole graph, names the replaced request + // once, is locked and checked by --locked; a library's override is ignored, reported. + const auto tmp = temp_dir(); + s::project anchor{tmp / "anchor", {{"mcpp.toml", "[package]\nname = \"anchor\"\nversion = \"0.1.0\"\n[targets.anchor]\nkind = \"lib\"\n[dependencies]\nmcpplibs.os = \"0.4.1\"\n"}, + {"src/anchor.cppm", "export module anchor;\nexport int anchor_answer() { return 1; }\n"}}}; + s::project app{tmp / "app", {{"src/main.cpp", "import anchor;\nint main() { return anchor_answer() == 1 ? 0 : 1; }\n"}}}; + auto write_app = [&](std::string_view os) { + app.write("mcpp.toml", std::format("[package]\nname = \"app\"\nversion = \"0.1.0\"\n[dependencies]\nmcpplibs.os = {}\nanchor = {{ path = \"{}/anchor\" }}\n", os, tmp.generic_string())); + }; + // The locked version of mcpplibs.os, and " override" when the entry says so (python3 read + // it for the script): the entry is the text up to the next `[`. + auto locked_os = [&] { + const auto lock = app.read("mcpp.lock"); + const auto at = lock.find("[package.\"mcpplibs.os\"]"); + if (at == std::string::npos) return std::string{}; + const auto start = at + std::string_view{"[package.\"mcpplibs.os\"]"}.size(); + const auto body = lock.substr(start, lock.find('[', start) - start); + std::smatch v; + std::string out = std::regex_search(body, v, std::regex{R"re(version\s*=\s*"([^"]+)")re"}) ? v[1].str() : ""; + if (s::has_line(body, R"(^override\s*=\s*true)")) out += " override"; + return out; + }; + write_app("\"^0.4.4\""); + auto a = app.mcpp({"build", "--configure-only"}); + expect(fatal(failed(a))) << "A: two requests no merge reconciles were accepted"; + expect(e2e::contains(s::text(a), "irreconcilable versions")) << "A: the refusal does not name the conflict"; + expect(s::has_line(s::text(a), R"(mcpplibs\.os = \{ version = "[0-9.]*", override = true \})")) << "A: the refusal does not name the override"; + write_app("{ version = \"0.4.4\", override = true }"); + auto b = app.mcpp({"build", "--configure-only"}); + expect(fatal(e2e::succeeded(b))) << "B: the override was refused"; + expect(s::has_line(s::text(b), R"(Overrode +mcpplibs\.os 0\.4\.1 \(requested by [^)]*anchor[^)]*\) -> 0\.4\.4 \(\[dependencies\] of app\))")) << "B: the replaced request is not named"; + expect(eq(s::count_lines(s::text(b), "Overrode"), std::size_t{1})) << "B: the replaced request is named more than once"; + expect(eq(locked_os(), std::string{"0.4.4 override"})) << "B: mcpp.lock does not record the override"; + write_app("{ version = \"0.4.4\", override = true }"); + expect(fatal(e2e::succeeded(app.mcpp({"build", "--configure-only"})))) << "C: the override was refused"; + { + std::istringstream in{app.read("mcpp.lock")}; + std::string out; + for (std::string line; std::getline(in, line);) out += (line == "override = true" ? "" : line) + "\n"; + app.write("mcpp.lock", out); + } + auto c3 = app.mcpp({"build", "--configure-only", "--locked"}); + expect(failed(c3)) << "C: --locked accepted a lock without the override"; + expect(e2e::contains(s::text(c3), "mcpplibs.os 0.4.4 -> 0.4.4 (override)")) << "C: the drift does not name the override"; + anchor.write("mcpp.toml", "[package]\nname = \"anchor\"\nversion = \"0.1.0\"\n[targets.anchor]\nkind = \"lib\"\n[dependencies]\nmcpplibs.os = { version = \"0.4.1\", override = true }\n"); + auto d = app.mcpp({"build", "--configure-only"}); + expect(fatal(e2e::succeeded(d))) << "D: the build failed"; + expect(e2e::contains(s::text(d), "`override` on 'mcpplibs.os' is ignored")) << "D: a library's override is not reported"; + }; +}}; + +} // namespace diff --git a/tests/e2e/deps/identity.cpp b/tests/e2e/deps/identity.cpp new file mode 100644 index 000000000..77ec0644a --- /dev/null +++ b/tests/e2e/deps/identity.cpp @@ -0,0 +1,125 @@ +// A path dependency's identity is its manifest's; the resolved graph is recorded and printed +// (formerly tests/e2e/679, 682). +import std; +import mcpplibs.mtest; +import mcpp.test.support; + +using namespace mcpplibs::mtest; +namespace s = mcpp::test::support; +namespace fs = std::filesystem; + +namespace { + +bool failed(const e2e::outcome& r) { return r.started && r.status != 0; } + +// The first resolution.json under `root` outside the build programs' trees. +std::optional resolution(const fs::path& root) { + for (const auto& f : s::find_files(root, "resolution\\.json")) + if (f.generic_string().find("/.build-mcpp/") == std::string::npos) return f; + return std::nullopt; +} + +E2E identity{"identity", [] { + "679 a path dependency takes its manifests identity"_test = [] { + // A `path` or `git` dependency's identity is the one its manifest declares, whatever key + // reached it: one package, compiled once, recorded with both keys; two identities over one + // source are refused before scanning; a namespace mismatch is one grouped warning. + const auto tmp = temp_dir(); + const std::string fw_manifest = "[package]\nnamespace = \"huxdemo\"\nname = \"fw\"\nversion = \"0.1.0\"\n[targets.fw]\nkind = \"lib\"\n"; + const std::string fw_module = "export module fw;\nexport int fw_anchor() { return 7; }\n"; + const std::string comp_manifest = "[package]\nnamespace = \"huxdemo\"\nname = \"comp\"\nversion = \"0.1.0\"\n[targets.comp]\nkind = \"lib\"\n[dependencies]\nfw = { path = \"../fw\" }\n"; + const std::string comp_module = "export module comp;\nimport fw;\nexport int comp_anchor() { return fw_anchor(); }\n"; + const std::string main = "import fw;\nimport comp;\nint main() { return fw_anchor() + comp_anchor() == 14 ? 0 : 1; }\n"; + s::project{tmp / "fw", {{"mcpp.toml", fw_manifest}, {"src/fw.cppm", fw_module}}}; + s::project comp{tmp / "comp", {{"mcpp.toml", comp_manifest}, {"src/comp.cppm", comp_module}}}; + s::project app{tmp / "app", {{"mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n[dependencies]\nhuxdemo.fw = { path = \"../fw\" }\nhuxdemo.comp = { path = \"../comp\" }\n"}, + {"src/main.cpp", main}}}; + auto a = app.mcpp({"build"}); + expect(fatal(e2e::succeeded(a))) << "A: build failed"; + expect(fatal(e2e::succeeded(app.mcpp({"run"})))) << "A: the program did not exit 0"; + expect(s::no_line(s::text(a), "declared identity is used|that identity is used")) << "A: a bare key produced an identity warning"; + std::size_t units = 0; + for (const auto& f : s::find_files(app.at("target"), "fw\\.m\\.o")) + if (f.generic_string().find("/obj/") != std::string::npos) ++units; + expect(eq(units, std::size_t{1})) << "A: fw.cppm compiled " << units << " times"; + const auto json = resolution(app.at("target")); + expect(fatal(json.has_value())) << "A: no resolution.json"; + std::string keys; + { + const auto doc = s::json_file(*json); + std::vector fws; + for (const auto& p : doc["graph"]["packages"]) + if (p["package"]["name"] == "fw") fws.push_back(p); + keys = std::to_string(fws.size()); + if (!fws.empty()) { + std::vector ks; + for (const auto& r : fws[0]["requested_by"]) ks.push_back(r["key"].get()); + std::ranges::sort(ks); + keys += " "; + for (std::size_t i = 0; i < ks.size(); ++i) keys += (i ? " " : "") + ks[i]; + } + } + expect(eq(keys, std::string{"1 fw huxdemo.fw"})) << "A: graph record reads '" << keys << "'"; + + comp.write("mcpp.toml", "[package]\nnamespace = \"huxdemo\"\nname = \"comp\"\nversion = \"0.1.0\"\n[targets.comp]\nkind = \"lib\"\n[dependencies]\nhuxdemo.fw = { path = \"../fw\" }\n"); + app.remove("target"); + auto b = app.mcpp({"build"}); + expect(fatal(e2e::succeeded(b))) << "B: build failed"; + expect(s::lacks(s::text(b), "declared identity is used")) << "B: matching keys produced an identity warning"; + + s::project{tmp / "nons/fw", {{"mcpp.toml", "[package]\nname = \"fw\"\nversion = \"0.1.0\"\n[targets.fw]\nkind = \"lib\"\n"}, {"src/fw.cppm", fw_module}}}; + s::project nons{tmp / "nons/app", {{"mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n[dependencies]\na.fw = { path = \"../fw\" }\nb.fw = { path = \"../fw\" }\n"}, + {"src/main.cpp", "import fw;\nint main() { return 0; }\n"}}}; + auto c = nons.mcpp({"build"}); + expect(fatal(failed(c))) << "C: two identities over one source built"; + expect(e2e::contains(s::text(c), "one source is reached as two packages")) << "C: the refusal does not name the two identities"; + expect(s::no_line(s::text(c), "already provided|is provided by package")) << "C: the refusal came from the scanner, not before scanning"; + + // D: comp (bare `fw` key again) and fw both reached under acme while they declare huxdemo. + s::project grp{tmp / "grp/app", {{"libs/fw/mcpp.toml", fw_manifest}, {"libs/fw/src/fw.cppm", fw_module}, + {"libs/comp/mcpp.toml", comp_manifest}, {"libs/comp/src/comp.cppm", comp_module}, + {"mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n[dependencies]\nacme.fw = { path = \"libs/fw\" }\nacme.comp = { path = \"libs/comp\" }\n"}, + {"src/main.cpp", main}}}; + auto d = grp.mcpp({"build"}); + expect(fatal(e2e::succeeded(d))) << "D: build failed"; + const auto warned = s::lines_with(s::text(d), "declared identity is used"); + expect(fatal(eq(warned.size(), std::size_t{1}))) << "D: expected one grouped warning, saw " << warned.size(); + expect(e2e::contains(warned[0], "mcpp.toml names 2 dependencies in namespace acme")) << "D: the warning does not name the manifest by its relative path and the count"; + expect(e2e::contains(warned[0], "declare huxdemo")) << "D: the warning does not name the declared namespace"; + expect(e2e::contains(warned[0], "acme.fw") && e2e::contains(warned[0], "acme.comp")) << "D: the warning does not list the keys"; + expect(e2e::contains(s::text(d), "[dependencies.huxdemo] table, for example")) << "D: the hint is not written in TOML"; + expect(s::lacks(s::text(d), "libs/comp/mcpp.toml names")) << "D: the bare key inside libs/comp was reported"; + }; + + "682 why deps prints the resolved graph"_test = [] { + // `resolution.json` records the resolved dependency graph and `mcpp why deps` prints it, + // path dependencies included. + const auto tmp = temp_dir(); + s::project{tmp / "fw", {{"mcpp.toml", "[package]\nnamespace = \"demo\"\nname = \"fw\"\nversion = \"0.1.0\"\n[targets.fw]\nkind = \"lib\"\n"}, + {"src/fw.cppm", "export module fw;\nexport int fw_anchor() { return 41; }\n"}}}; + s::project app{tmp / "app", {{"mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n[dependencies]\ndemo.fw = { path = \"../fw\" }\n"}, + {"src/main.cpp", "import fw;\nint main() { return fw_anchor() == 41 ? 0 : 1; }\n"}}}; + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "build failed"; + auto why = app.mcpp({"why", "deps"}); + expect(fatal(e2e::succeeded(why))) << "A: why deps failed"; + expect(s::has_line(s::text(why), "^dependency graph:")) << "A: no graph section"; + expect(e2e::contains(s::text(why), "demo.fw@0.1.0")) << "A: the path dependency is not listed"; + expect(e2e::contains(s::text(why), "requested by mcpplibs.app@0.1.0 as 'demo.fw' in [dependencies]")) << "A: the request is not listed"; + expect(e2e::contains(s::text(why), "linked static (default)")) << "A: the link form is not listed"; + const auto json = resolution(app.at("target")); + expect(fatal(json.has_value())) << "B: no resolution.json"; + const auto doc = s::json_file(*json); + expect(eq(doc["schema_version"].get(), 2)) << "B: schema_version"; + expect(doc.contains("toolchain") && doc.contains("runtime")) << "B: the record lacks toolchain or runtime"; + const auto& pkgs = doc["graph"]["packages"]; + expect(fatal(!pkgs.empty())) << "B: no packages"; + expect(pkgs[0]["root"] == true && pkgs[0]["requested_by"].empty()) << "B: the first package is not the root: " << pkgs[0].dump(); + std::vector fw; + for (const auto& p : pkgs) + if (p["package"]["canonical"] == "demo.fw@0.1.0") fw.push_back(p); + expect(eq(fw.size(), std::size_t{1})) << "B: demo.fw@0.1.0 recorded " << fw.size() << " times"; + expect(!fw.empty() && fw[0]["package"]["source"].get().starts_with("path+")) << "B: the path dependency's source is not path+"; + }; +}}; + +} // namespace diff --git a/tests/e2e/deps/lock.cpp b/tests/e2e/deps/lock.cpp new file mode 100644 index 000000000..43d6ee626 --- /dev/null +++ b/tests/e2e/deps/lock.cpp @@ -0,0 +1,362 @@ +// Versions as the index's keys, and what mcpp.lock records of them, one entry per identity, written +// optimistically, keyed independently of the members planned, kept across configuration groups +// (formerly tests/e2e/196, 203, 844, 939, 990, 1027). +import std; +import mcpplibs.mtest; +import mcpp.test.support; + +using namespace mcpplibs::mtest; +namespace s = mcpp::test::support; +namespace fs = std::filesystem; + +namespace { + +const s::options inherited{.where = s::home::inherited}; + +bool failed(const e2e::outcome& r) { return r.started && r.status != 0; } + +// The index descriptor of mcpplibs.cmdline 1.0.0 (844, 939): never fetched, its payload seeded. +std::string cmdline_descriptor(std::string_view description) { + return std::format(R"cpp(package = {{ + spec = "1", + namespace = "mcpplibs", + name = "cmdline", + description = "{}", + licenses = {{"MIT"}}, + type = "package", + xpm = {{ linux = {{ ["1.0.0"] = {{ + url = "https://example.invalid/cmdline-1.0.0.tar.gz", + sha256 = "0000000000000000000000000000000000000000000000000000000000000000", + }} }} }}, + mcpp = {{ + language = "c++23", + import_std = false, + sources = {{ "src/cmdline.cppm" }}, + targets = {{ ["cmdline"] = {{ kind = "lib" }} }}, + deps = {{}}, + }}, +}} +)cpp", description); +} + +// The index descriptor of mcpplibs. 1.0.0 for every OS (990, 1027): its payload seeded. +std::string widget_descriptor(std::string_view name) { + return std::format(R"cpp(package = {{ + spec = "1", + namespace = "mcpplibs", + name = "{0}", + description = "a package of the default namespace from a local index", + licenses = {{"MIT"}}, + type = "package", + xpm = {{ + linux = {{ ["1.0.0"] = {{ url = "https://example.invalid/w.tar.gz" }} }}, + macosx = {{ ["1.0.0"] = {{ url = "https://example.invalid/w.tar.gz" }} }}, + windows = {{ ["1.0.0"] = {{ url = "https://example.invalid/w.zip" }} }}, + }}, + mcpp = {{ + language = "c++23", + import_std = false, + sources = {{ "src/{0}.cppm" }}, + targets = {{ ["{0}"] = {{ kind = "lib" }} }}, + deps = {{}}, + }}, +}} +)cpp", name); +} + +E2E lock{"lock", [] { + "196 version identity and lock"_test[tag{"requires:gcc"}][tag{"requires:fresh-sandbox"}] = [] { + // mcpp#363: a resolved version is an index key (an address re-rendered from parsed + // numbers did not exist), and mcpp.lock records what was resolved, stably, whatever the + // command, without dev-dependencies. + s::needs({"gcc", "fresh-sandbox"}); + s::project p{{"src/main.cpp", "import gadget;\nint main(){ return gadget_value() == 42 ? 0 : 1; }\n"}}; + auto mk_pkg = [&](std::string_view name, std::string_view table, std::string_view mod = "gadget") { + p.write(std::format("local-index/pkgs/a/acme.{}.lua", name), + std::format("package = {{\n spec = \"1\", namespace = \"acme\", name = \"acme.{0}\",\n description = \"version-model fixture\", licenses = {{\"MIT\"}}, type = \"package\",\n xpm = {{ linux = {1}, macosx = {1}, windows = {1} }},\n mcpp = {{ language = \"c++23\", import_std = false,\n sources = {{ \"src/{2}.cppm\" }},\n targets = {{ [\"{2}\"] = {{ kind = \"lib\" }} }}, deps = {{}} }},\n}}\n", name, table, mod)); + }; + mk_pkg("im", "{ [\"1.92.8\"] = { url = \"u\", sha256 = \"0\" },\n [\"1.92.8-docking\"] = { url = \"u\", sha256 = \"0\" } }"); + mk_pkg("jdk", "{ [\"latest\"] = { ref = \"25.0.4.7.1\" },\n [\"25.0.4\"] = { ref = \"25.0.4.7.1\" },\n [\"25.0.4.7.1\"] = { url = \"u\", sha256 = \"0\" } }"); + mk_pkg("kh", "{ [\"latest\"] = { ref = \"pre-v0.0.5\" },\n [\"pre-v0.0.5\"] = { url = \"u\", sha256 = \"0\" } }"); + mk_pkg("tie", "{ [\"1.0.0+a\"] = { url = \"u\", sha256 = \"0\" },\n [\"1.0.0+b\"] = { url = \"u\", sha256 = \"0\" } }"); + mk_pkg("dv", "{ [\"1.0.0\"] = { url = \"u\", sha256 = \"0\" } }", "devkit"); + // Payloads seeded so that only the right key can install. + auto seed = [&](std::string_view name, std::string_view version, std::string_view mod = "gadget") { + p.write(std::format(".mcpp/.xlings/data/xpkgs/acme.{}/{}/src/{}.cppm", name, version, mod), + std::format("export module {0};\nexport int {0}_value() {{ return 42; }}\n", mod)); + }; + seed("im", "1.92.8"); + seed("jdk", "25.0.4.7.1"); + seed("dv", "1.0.0", "devkit"); + auto use_dep = [&](std::string_view name, std::string_view constraint) { + p.write("mcpp.toml", std::format("[package]\nname = \"proj\"\nversion = \"0.1.0\"\n\n[indices]\nacme = {{ path = \"local-index\" }}\n\n[dependencies.acme]\n{} = \"{}\"\n\n[targets.proj]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n", name, constraint)); + p.remove("mcpp.lock"); + }; + auto build = [&](std::vector args = {"build"}) { return p.mcpp(std::move(args), inherited); }; + + use_dep("im", "^1.92.8"); + auto b1 = build(); + expect(fatal(e2e::succeeded(b1))) << "^1.92.8 did not build"; + expect(s::has_line(s::text(b1), R"(^\s*Resolved acme.im .* → v1\.92\.8$)")) << "^1.92.8 must resolve to 1.92.8, never the -docking branch"; + use_dep("im", "1.92.8-docking"); + auto b2 = build(); + expect(failed(b2)) << "expected the unseeded -docking payload to fail"; + expect(e2e::contains(s::text(b2), "1.92.8-docking")) << "exact pre-release must be addressed literally, not truncated"; + use_dep("jdk", "^25.0"); + auto b3 = build(); + expect(fatal(e2e::succeeded(b3))) << "^25.0 did not build"; + expect(s::has_line(s::text(b3), R"(^\s*Resolved acme.jdk .* → v25\.0\.4\.7\.1$)")) << "^25.0 must resolve to the real five-segment key"; + use_dep("kh", "*"); + auto b4 = build(); + expect(failed(b4)) << "a range over unorderable keys must not succeed"; + expect(e2e::contains(s::text(b4), "not ordered versions")) << "expected an error naming the unorderable keys"; + expect(e2e::contains(s::text(b4), "acme.kh = \"pre-v0.0.5\"")) << "the error must show the exact pin that works"; + expect(s::lacks(s::text(b4), "no valid versions in index")) << "the old message blames the index for a package it publishes fine"; + use_dep("kh", "pre-v0.0.5"); + auto b5 = build(); + expect(failed(b5)) << "expected the unseeded payload to fail"; + expect(s::lacks(s::text(b5), "not ordered versions")) << "an exact unorderable key must resolve, not be rejected"; + use_dep("kh", "=pre-v0.0.5"); + auto b5b = build(); + expect(failed(b5b)) << "expected the unseeded payload to fail"; + expect(s::no_line(s::text(b5b), "invalid version constraint|not ordered versions")) << "'=' must resolve to that key"; + use_dep("tie", "^1.0"); + auto b6 = build(); + expect(failed(b6)) << "a tie must not silently pick one"; + expect(e2e::contains(s::text(b6), "compare EQUAL")) << "expected the tie to be named"; + expect(e2e::contains(s::text(b6), "1.0.0+a") && e2e::contains(s::text(b6), "1.0.0+b")) << "both tied keys must be listed"; + use_dep("tie", "=1.0.0+b"); + auto b6b = build(); + expect(failed(b6b)) << "expected the unseeded payload to fail"; + expect(s::lacks(s::text(b6b), "compare EQUAL")) << "an exact build-metadata pin must not be reported as a tie"; + expect(e2e::contains(s::text(b6b), "1.0.0+b")) << "the exact key must be addressed literally"; + + // The lock records the resolved version, and is stable. + use_dep("im", "^1.92.8"); + auto b7 = build({"build", "-v"}); + expect(fatal(e2e::succeeded(b7))) << "rebuild failed"; + const auto lock1 = p.read("mcpp.lock"); + expect(e2e::contains(lock1, "version = \"1.92.8\"")) << "the lock must record the resolved version"; + expect(s::lacks(lock1, "^")) << "a lock that records a range locks nothing"; + expect(s::has_line(s::text(b7), R"((Compiling|Cached) +acme\.im v1\.92\.8)")) << "the dependency banner must announce the resolved version"; + expect(s::lacks(s::text(b7), "acme.im v^")) << "the banner must never print a constraint as a version"; + expect(e2e::contains(lock1, "does not yet pin future builds")) << "the lock must state that it does not pin yet"; + expect(fatal(e2e::succeeded(build()))) << "second build failed"; + expect(eq(p.read("mcpp.lock"), lock1)) << "mcpp.lock is not stable across builds"; + + // The lock does not depend on the command, and holds no dev-dependency. + p.write("tests/t_dev.cpp", "import devkit;\nint main(){ return devkit_value()==42?0:1; }\n"); + p.write("mcpp.toml", "[package]\nname = \"proj\"\nversion = \"0.1.0\"\n\n[indices]\nacme = { path = \"local-index\" }\n\n[dependencies.acme]\nim = \"^1.92.8\"\n\n[dev-dependencies.acme]\ndv = \"^1.0\"\n\n[targets.proj]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n"); + p.remove("mcpp.lock"); + expect(fatal(e2e::succeeded(build()))) << "build with a dev-dep failed"; + const auto lock_build = p.read("mcpp.lock"); + auto c2 = build({"test"}); + expect(fatal(e2e::succeeded(c2))) << "test with a dev-dep failed"; + expect(s::lacks(s::text(c2), "acme.im v^")) << "mcpp test announced a constraint as a version"; + expect(eq(p.read("mcpp.lock"), lock_build)) << "mcpp test rewrote mcpp.lock — the lock must not depend on the command"; + expect(fatal(e2e::succeeded(build()))) << "build after test failed"; + expect(eq(p.read("mcpp.lock"), lock_build)) << "mcpp build rewrote mcpp.lock after mcpp test"; + expect(e2e::contains(s::text(c2), "Resolved acme.dv")) << "the dev-dependency was never resolved — the check above is vacuous"; + expect(s::lacks(p.read("mcpp.lock"), "acme.dv")) << "dev-dependencies must not be recorded in mcpp.lock"; + expect(e2e::contains(p.read("mcpp.lock"), "dev-dependencies are excluded")) << "the lock must state that dev-dependencies are excluded"; + }; + + "203 exact selector lock migration"_test[tag{"requires:gcc"}][tag{"requires:fresh-sandbox"}] = [] { + // A compact dotted selector used to try mcpplibs. first; during the migration release + // an existing lock keeps the identity it selected. Both identities exist, so index order + // cannot make the assertion pass. + s::needs({"gcc", "fresh-sandbox"}); + const auto tmp = temp_dir(); + auto descriptor = [](std::string_view ns, std::string_view desc) { + return std::format("package = {{\n spec = \"1\",\n namespace = \"{0}\",\n name = \"lua\",\n description = \"{1}\",\n licenses = {{\"MIT\"}},\n type = \"package\",\n xpm = {{ linux = {{ [\"1.0.0\"] = {{\n url = \"https://example.invalid/{0}-lua.tar.gz\",\n sha256 = \"0000000000000000000000000000000000000000000000000000000000000000\",\n }} }} }},\n mcpp = {{\n language = \"c++23\",\n import_std = false,\n sources = {{ \"src/lua.cppm\" }},\n targets = {{ [\"lua\"] = {{ kind = \"lib\" }} }},\n deps = {{}},\n }},\n}}\n", ns, desc); + }; + s::project index{tmp / "index", {{"pkgs/c/capi.lua.lua", descriptor("capi", "new exact identity")}, + {"pkgs/m/mcpplibs.capi.lua.lua", descriptor("mcpplibs.capi", "old prefixed identity")}}}; + s::project app{tmp / "app", {{".mcpp/.xlings/data/xpkgs/mcpplibs.capi-x-lua/1.0.0/src/lua.cppm", "export module selected.lua;\nexport int selected_value() { return 41; }\n"}, + {".mcpp/.xlings/data/xpkgs/capi-x-lua/1.0.0/src/lua.cppm", "export module selected.lua;\nexport int selected_value() { return 42; }\n"}, + {".mcpp/.xlings/data/xpkgs/mcpplibs.capi-x-lua/1.0.0/.mcpp_ok", "ok\n"}, + {".mcpp/.xlings/data/xpkgs/capi-x-lua/1.0.0/.mcpp_ok", "ok\n"}, + {"src/main.cpp", "import selected.lua;\nint main() { return selected_value() == 41 ? 0 : 1; }\n"}, + {"mcpp.lock", "# Existing pre-exact-selector resolution anchor.\nversion = 2\n\n[package.\"capi.lua\"]\nnamespace = \"mcpplibs.capi\"\nversion = \"1.0.0\"\nsource = \"index+mcpplibs.capi@1.0.0\"\nhash = \"fnv1a:migration-fixture\"\n"}}}; + app.write("mcpp.toml", std::format("[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[indices]\ndefault = {{ path = \"{0}\" }}\ncapi = {{ path = \"{0}\" }}\n\n[dependencies]\ncapi.lua = \"1.0.0\"\n", index.dir().generic_string())); + auto b = app.mcpp({"build"}, inherited); + expect(fatal(e2e::succeeded(b))) << "build failed"; + expect(e2e::contains(s::text(b), "keeping the locked identity")) << "expected selector migration warning"; + expect(e2e::contains(s::text(b), "mcpplibs.capi.lua")) << "warning must include the locked selector"; + expect(fatal(e2e::succeeded(app.mcpp({"run"}, inherited)))) << "run failed"; + expect(e2e::contains(app.read("mcpp.lock"), "namespace = \"mcpplibs.capi\"")) << "lock identity changed during migration"; + }; + + "844 a planned build keeps one lock entry per identity"_test[tag{"requires:gcc"}][tag{"requires:fresh-sandbox"}] = [] { + // A planned build of a workspace root keeps one mcpp.lock entry per identity, spelled as + // the root's manifest spells it (F11): it keeps a lock in the 2026.9.28.3 spelling, + // drops a second entry for the same identity, and does not rewrite an unchanged lock. + s::needs({"gcc", "fresh-sandbox"}); + s::project ws{{{"local-index/pkgs/c/cmdline.lua", cmdline_descriptor("fixture for the one-entry-per-identity lock (F11)")}, + {".mcpp/.xlings/data/xpkgs/mcpplibs-x-cmdline/1.0.0/src/cmdline.cppm", "export module fixture.cmdline;\nexport int cmdline_value() { return 7; }\n"}, + {".mcpp/.xlings/data/xpkgs/mcpplibs-x-cmdline/1.0.0/.mcpp_ok", "ok\n"}, + {"mcpp.toml", "[package]\nname = \"ws\"\nversion = \"0.1.0\"\n[workspace]\nmembers = [\"m\"]\n[indices]\ndefault = { path = \"local-index\" }\n[dependencies.mcpplibs]\ncmdline = \"1.0.0\"\n[targets.ws]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n"}, + {"src/main.cpp", "import fixture.cmdline;\nint main() { return cmdline_value() == 7 ? 0 : 1; }\n"}, + {"m/mcpp.toml", "[package]\nname = \"m\"\nversion = \"0.1.0\"\n[targets.m]\nkind = \"lib\"\n[build]\nsources = [\"src/m.cppm\"]\n"}, + {"m/src/m.cppm", "export module m;\nexport int m_value() { return 1; }\n"}}}; + // The lock's hash: FNV-1a 64 of the identity (python3 computed it for the script). + auto digest = [](std::string_view s) { + std::uint64_t h = 0xcbf29ce484222325ull; + for (unsigned char b : s) { h ^= b; h *= 0x100000001b3ull; } + return std::format("fnv1a:{:016x}", h); + }; + auto entries = [&] { return s::count_matching(ws.read("mcpp.lock"), R"(^\[package\.)"); }; + const auto before = std::format(R"cpp(# Auto-generated by mcpp. Do not edit by hand. +# Records what this build resolved. It does not yet pin future builds: +# index dependencies are re-resolved from their constraints each time. +# dev-dependencies are excluded: only `mcpp test` resolves them, and this +# file must not change depending on which command ran last. +version = 2 + +[package."mcpplibs.cmdline"] +namespace = "mcpplibs" +version = "1.0.0" +source = "index+mcpplibs@1.0.0" +hash = "{}" + +)cpp", digest("mcpplibs:mcpplibs.cmdline@1.0.0")); + ws.write("mcpp.lock", before); + expect(fatal(e2e::succeeded(ws.mcpp({"build"}, inherited)))) << "the planned build did not succeed"; + expect(fatal(eq(ws.read("mcpp.lock"), before))) << "(a) a planned build rewrote a lock in the 2026.9.28.3 spelling"; + ws.write("mcpp.lock", before + std::format("[package.\"cmdline\"]\nnamespace = \"mcpplibs\"\nversion = \"1.0.0\"\nsource = \"index+mcpplibs@1.0.0\"\nhash = \"{}\"\n\n", digest("mcpplibs:cmdline@1.0.0"))); + expect(fatal(eq(entries(), std::size_t{2}))) << "the fixture lock does not hold two entries"; + auto touch_main = [&](std::string_view line) { ws.write("src/main.cpp", ws.read("src/main.cpp") + std::string(line) + "\n"); }; + touch_main("// touched"); + expect(fatal(e2e::succeeded(ws.mcpp({"build"}, inherited)))) << "the second planned build did not succeed"; + expect(eq(entries(), std::size_t{1})) << "(b) the lock still holds " << entries() << " entries for one identity"; + expect(eq(ws.read("mcpp.lock"), before)) << "(b) the surviving entry is not the one 2026.9.28.3 wrote"; + const auto stamp = std::chrono::file_clock::from_sys(std::chrono::sys_seconds{std::chrono::seconds{1000000000}}); + fs::last_write_time(ws.at("mcpp.lock"), stamp); + touch_main("// touched again"); + expect(fatal(e2e::succeeded(ws.mcpp({"build"}, inherited)))) << "the third planned build did not succeed"; + expect(eq(ws.read("mcpp.lock"), before)) << "(c) a repeated planned build changed the lock"; + expect(fs::last_write_time(ws.at("mcpp.lock")) == stamp) << "(c) a repeated planned build rewrote an unchanged lock"; + touch_main("// and again"); + s::options locked = inherited; + locked.env.push_back({"MCPP_LOCKED", "1"}); + expect(e2e::succeeded(ws.mcpp({"build"}, locked))) << "--locked reported drift on an unchanged lock"; + }; + + "939 a lock written meanwhile is not overwritten"_test[tag{"requires:gcc"}][tag{"requires:fresh-sandbox"}] = [] { + // mcpp.lock is written optimistically: a lock that changed while a build resolved (here + // the project's own build program appends to it) is not overwritten, and the build is + // refused naming lockfile-concurrent and the remedy; the next build succeeds. + s::needs({"gcc", "fresh-sandbox"}); + s::project p{{{"local-index/pkgs/c/cmdline.lua", cmdline_descriptor("fixture for the optimistic lock write")}, + {".mcpp/.xlings/data/xpkgs/mcpplibs-x-cmdline/1.0.0/src/cmdline.cppm", "export module fixture.cmdline;\nexport int cmdline_value() { return 7; }\n"}, + {".mcpp/.xlings/data/xpkgs/mcpplibs-x-cmdline/1.0.0/.mcpp_ok", "ok\n"}, + {"mcpp.toml", "[package]\nname = \"p\"\nversion = \"0.1.0\"\n[indices]\ndefault = { path = \"local-index\" }\n[dependencies.mcpplibs]\ncmdline = \"1.0.0\"\n"}, + {"src/main.cpp", "import fixture.cmdline;\nint main() { return cmdline_value() == 7 ? 0 : 1; }\n"}}}; + expect(fatal(e2e::succeeded(p.mcpp({"build"}, inherited)))) << "A: the first build failed"; + expect(e2e::contains(p.read("mcpp.lock"), "cmdline")) << "A: mcpp.lock does not record the dependency"; + p.write("build.mcpp", R"cpp(#include +int main() { + std::ofstream f("mcpp.lock", std::ios::app); + f << "# written by another run\n"; + return f ? 0 : 1; +} +)cpp"); + auto b = p.mcpp({"build"}, inherited); + expect(fatal(failed(b))) << "B: a lock changed meanwhile was overwritten (the build succeeded)"; + expect(e2e::contains(s::text(b), "lockfile-concurrent")) << "B: the refusal does not name lockfile-concurrent"; + expect(e2e::contains(s::text(b), "run the command again")) << "B: the refusal names no remedy"; + expect(e2e::contains(p.read("mcpp.lock"), "# written by another run")) << "B: the other run's lock was overwritten"; + p.remove("build.mcpp"); + expect(e2e::succeeded(p.mcpp({"build"}, inherited))) << "C: the build after the concurrent write failed"; + }; + + "990 a lock key does not depend on the selected members"_test[tag{"requires:gcc"}][tag{"requires:fresh-sandbox"}] = [] { + // Two members take one workspace dependency under two spellings; the lock entry's key and + // hash do not depend on which members a command plans, and the key is the workspace's + // own spelling. + s::needs({"gcc", "fresh-sandbox"}); + s::project ws{{{"local-index/pkgs/w/widget2.lua", widget_descriptor("widget2")}, + {".mcpp/.xlings/data/xpkgs/mcpplibs.widget2/1.0.0/src/widget2.cppm", "export module widget2;\nexport int widget2_value() { return 35; }\n"}, + {"mcpp.toml", "[workspace]\nmembers = [\"a\", \"c\"]\n[workspace.indices]\nmcpplibs = { path = \"local-index\" }\n[workspace.dependencies]\nwidget2 = \"1.0.0\"\n"}, + {"a/mcpp.toml", "[package]\nname = \"a\"\nversion = \"0.1.0\"\n[dependencies]\nwidget2.workspace = true\n[targets.a]\nkind = \"lib\"\n"}, + {"a/src/a.cppm", "export module a;\nimport widget2;\nexport int a_value() { return widget2_value() + 7; }\n"}, + {"c/mcpp.toml", "[package]\nname = \"c\"\nversion = \"0.1.0\"\n[dependencies.mcpplibs]\nwidget2.workspace = true\n[targets.c]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n"}, + {"c/src/main.cpp", "import widget2;\nint main() { return widget2_value() == 35 ? 0 : 1; }\n"}}}; + // The lock's widget2 entry: its header and its hash. + auto entry = [&] { + const auto lock = ws.read("mcpp.lock"); + std::string header, hash; + bool in = false; + std::istringstream lines{lock}; + for (std::string line; std::getline(lines, line);) { + if (std::regex_search(line, std::regex{R"(^\[package\..*widget2"\])"})) header += line + "\n"; + if (line.find("widget2\"]") != std::string::npos) in = true; + if (in && hash.empty() && line.starts_with("hash")) hash = line; + } + return header + hash; + }; + std::map seen; + const std::vector> selections{{"--workspace"}, {"-p", "a"}, {"-p", "c"}}; + for (const auto& sel : selections) { + ws.remove("mcpp.lock"); + std::vector args{"build"}; + args.insert(args.end(), sel.begin(), sel.end()); + args.push_back("--configure-only"); + expect(fatal(e2e::succeeded(ws.mcpp(args, inherited)))) << "mcpp build " << sel.back() << " --configure-only"; + seen[sel.back()] = entry(); + } + for (const auto& [sel, e] : seen) + expect(eq(e, seen["--workspace"])) << "the lock entry of widget2 depends on the members a command plans (" << sel << ")"; + expect(s::has_line(seen["--workspace"], R"(^\[package\."widget2"\])")) << "the key is not the workspace's own spelling (widget2)"; + }; + + "1027 a configuration group keeps the other groups lock entries"_test[tag{"requires:gcc"}][tag{"requires:fresh-sandbox"}] = [] { + // A workspace whose members take two configurations is planned one group at a time, + // and each plan writes the lock: a plan keeps the other groups' targets of an entry. + s::needs({"gcc", "fresh-sandbox"}); + s::project ws{{{"local-index/pkgs/w/widget3.lua", widget_descriptor("widget3")}, + {".mcpp/.xlings/data/xpkgs/mcpplibs.widget3/1.0.0/src/widget3.cppm", "export module widget3;\nexport int widget3_value() { return 36; }\n"}, + {"mcpp.toml", "[workspace]\nmembers = [\"a\", \"b\"]\n[workspace.indices]\nmcpplibs = { path = \"local-index\" }\n"}, + {"a/mcpp.toml", "[package]\nname = \"a\"\nversion = \"0.1.0\"\n[dependencies]\nmcpplibs.widget3 = \"1.0.0\"\n[targets.a]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n"}, + {"a/src/main.cpp", "import widget3;\nint main() { return widget3_value() == 36 ? 0 : 1; }\n"}, + {"b/mcpp.toml", "[package]\nname = \"b\"\nversion = \"0.1.0\"\nstandard = \"c++26\"\n[targets.b]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n"}, + {"b/src/main.cpp", "int main() { return 0; }\n"}}}; + expect(fatal(e2e::succeeded(ws.mcpp({"build", "--workspace", "--configure-only"}, inherited)))) << "the first configure failed"; + expect(fatal(e2e::contains(ws.read("mcpp.lock"), "widget3"))) << "the lock records no widget3 entry"; + // The widget3 entry's `targets` replaced by another configuration's (python3 did it for + // the script): the entry's lines are those up to the next line opening with `[`. + { + std::istringstream in{ws.read("mcpp.lock")}; + std::string out, line; + bool block = false, found = false; + auto close = [&] { if (block) out += "targets = [\"x86_64-openkal\"]\n"; block = false; }; + while (std::getline(in, line)) { + if (line.starts_with("[")) { + close(); + if (!found && std::regex_match(line, std::regex{R"(\[package\."[^"]*widget3"\])"})) block = found = true; + } else if (block && line.starts_with("targets = ")) { + continue; + } + out += line + "\n"; + } + close(); + expect(fatal(found)) << "no widget3 entry to rewrite"; + ws.write("mcpp.lock", out); + } + auto second = ws.mcpp({"build", "--workspace", "--configure-only"}, inherited); + expect(fatal(e2e::succeeded(second))) << "the second configure failed"; + std::string targets; + { + std::istringstream in{ws.read("mcpp.lock")}; + bool after = false; + for (std::string line; std::getline(in, line);) { + if (line.find("widget3\"]") != std::string::npos) after = true; + if (after && line.starts_with("targets")) { targets = line; break; } + } + } + expect(e2e::contains(targets, "x86_64-openkal")) << "the other configuration's target was dropped: " << targets; + expect(e2e::contains(targets, ",")) << "this configuration's target is missing: " << targets; + }; +}}; + +} // namespace diff --git a/tests/e2e/deps/names.cpp b/tests/e2e/deps/names.cpp new file mode 100644 index 000000000..e5501e964 --- /dev/null +++ b/tests/e2e/deps/names.cpp @@ -0,0 +1,243 @@ +// How a dependency is named: a package's name and namespace, bare names, identity-first +// resolution and the wire address, quoted dotted keys, a package named like a specification's +// field (formerly tests/e2e/161, 162, 163, 165, 1032, 1074). +import std; +import mcpplibs.mtest; +import mcpp.test.support; + +using namespace mcpplibs::mtest; +namespace s = mcpp::test::support; +namespace fs = std::filesystem; + +namespace { + +// A descriptor of `namespace`, `name` and one version for the three systems. +std::string descriptor(std::string_view ns, std::string_view name, std::string_view version, bool all_systems = true) { + const auto v = std::format("{{ [\"{}\"] = {{ url = \"u\", sha256 = \"h\" }} }}", version); + return std::format("package = {{\n spec = \"1\", namespace = \"{}\", name = \"{}\",\n xpm = {{ linux = {}{} }},\n mcpp = {{ schema = \"0.1\", sources = {{ \"*.cpp\" }} }},\n}}\n", + ns, name, v, all_systems ? std::format(",\n macosx = {0},\n windows = {0}", v) : ""); +} + +// sed -e 's/^\( * *= *\)"[^"]*"/\1""/' over every line of `text`. +std::string rewrite(const std::string& text, std::string_view key, std::string_view value) { + std::string out; + const std::regex re(std::format(R"re(^( *{} *= *)"[^"]*")re", key)); + for (auto line : std::views::split(std::string_view(text), '\n')) { + std::string l(line.begin(), line.end()); + out += std::regex_replace(l, re, std::format("$1\"{}\"", value), std::regex_constants::format_first_only) + "\n"; + } + if (!text.ends_with('\n') && !out.empty()) out.pop_back(); + return out; +} + +// The registry's asio descriptor, renamed; the case is skipped where the registry has none. +std::string renamed_asio(std::string_view ns, std::string_view name) { + const auto src = s::shared_home() / "indices/mcpplibs/pkgs/c/chriskohlhoff.asio.lua"; + if (!fs::is_regular_file(src)) + skip("no asio descriptor in registry"); + const auto out = rewrite(rewrite(s::read_file(src), "namespace", ns), "name", name); + expect(fatal(s::has_line(out, std::format(R"(^ *namespace *= *"{}")", ns)))) << "fixture namespace rewrite missed (upstream descriptor shape changed?)"; + expect(fatal(s::has_line(out, std::format(R"(^ *name *= *"{}")", std::regex_replace(std::string(name), std::regex(R"(\.)"), R"(\.)"))))) + << "fixture name rewrite missed (upstream descriptor shape changed?)"; + return out; +} + +E2E names{"names", [] { + "161 xpkg name form"_test = [] { + // SPEC-001 §3.2: `package.name` is a single atomic segment; the hierarchy belongs in + // `package.namespace` (the legacy fully qualified `compat.zlib` keeps working). A dotted + // short name is refused, in --json too, and a build fails before any download. (The script + // read the JSON with python3.) + s::project p{{"short.lua", descriptor("chriskohlhoff", "asio", "1.0.0", false)}, + {"nested.lua", descriptor("mcpplibs.capi", "lua", "1.0.0", false)}, + {"legacy.lua", descriptor("compat", "compat.zlib", "1.0.0", false)}, + {"bad.lua", descriptor("mcpplibs", "capi.lua", "1.0.0", false)}, + {"nons.lua", descriptor("", "mcpplibs.capi.lua", "1.0.0", false)}}; + for (auto ok : {"short.lua", "nested.lua", "legacy.lua"}) + expect(e2e::succeeded(p.mcpp({"xpkg", "parse", ok}))) << ok; + auto bad = p.mcpp({"xpkg", "parse", "bad.lua"}); + expect(bad.started && bad.status != 0) << "a short name containing '.' must be rejected"; + expect(e2e::contains(s::text(bad), "single atomic segment")); + expect(e2e::contains(s::text(bad), "mcpplibs.capi")); + expect(e2e::contains(s::text(bad), "\"lua\"")); + auto nons = p.mcpp({"xpkg", "parse", "nons.lua"}); + expect(nons.started && nons.status != 0) << "dotted name without a namespace must be rejected"; + auto json = p.mcpp({"xpkg", "parse", "--json", "bad.lua"}); + expect(json.started && json.status != 0) << "--json must still exit non-zero on violation"; + const auto j = s::json(json.out); + expect(fatal(j.contains("error"))) << json.out; + expect(e2e::contains(j["error"].get(), "atomic")); + // A build fails before any download. + p.write("idx/pkgs/d/thing.lua", descriptor("demo", "sub.thing", "1.0.0")); + p.write("app/mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[indices]\ndemo = { path = \"../idx\" }\n\n[dependencies.demo]\n\"sub.thing\" = \"1.0.0\"\n"); + p.write("app/src/main.cpp", "int main() { return 0; }\n"); + auto b = p.mcpp_in("app", {"build"}); + expect(b.started && b.status != 0) << "non-atomic short name must not build"; + expect(e2e::contains(s::text(b), "single atomic segment")); + expect(s::lacks(s::text(b), "Downloading")) << "must fail before any download"; + }; + + "162 bare name namespace scope"_test = [] { + // INV-RESOLVE (#278): a bare name resolves in exactly one place (mcpplibs; compat during + // the migration window, with a warning naming the exact replacement); every other + // namespace is written out; a refused add writes nothing and names what exists. + s::project p{{"idx/pkgs/a/acme.widget.lua", descriptor("acme", "acme.widget", "1.0.0")}, + {"idx/pkgs/c/compat.gadget.lua", descriptor("compat", "gadget", "2.0.0")}, + {"app/src/main.cpp", "int main() { return 0; }\n"}}; + auto write_manifest = [&](std::string_view deps) { + p.write("app/mcpp.toml", std::format("[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[indices]\nacme = {{ path = \"../idx\" }}\ncompat = {{ path = \"../idx\" }}\n{}\n", deps)); + }; + auto build = [&] { return p.mcpp_in("app", {"build"}); }; + write_manifest("[dependencies]\nwidget = \"1.0.0\""); + auto bare = build(); + expect(bare.started && bare.status != 0) << "a bare name must not reach the acme namespace"; + for (auto want : {"no package found for exact selector", "tried:", "acme.widget", "acme.widget (1.0.0)", "[dependencies.acme]"}) + expect(e2e::contains(s::text(bare), want)) << want; + write_manifest("[dependencies]\n\"acme.widget\" = \"1.0.0\""); + expect(s::lacks(s::text(build()), "no package found for exact selector")) << "dotted selector must resolve to (acme, widget)"; + write_manifest("[dependencies.acme]\nwidget = \"1.0.0\""); + expect(s::lacks(s::text(build()), "no package found for exact selector")) << "sub-table form must resolve to (acme, widget)"; + write_manifest("[dependencies]\ngadget = \"2.0.0\""); + const auto compat = s::text(build()); + expect(s::lacks(compat, "no package found for exact selector")) << "bare gadget must still reach compat during the migration window"; + expect(e2e::contains(compat, "deprecated bare-name search")) << "the bare-name fallback must announce itself"; + expect(e2e::contains(compat, "compat.gadget")) << "the warning must name the exact replacement"; + expect(e2e::contains(compat, "[dependencies.compat]")) << "the warning must show the manifest edit"; + write_manifest("[dependencies.compat]\ngadget = \"2.0.0\""); + const auto explicit_compat = s::text(build()); + expect(s::lacks(explicit_compat, "no package found for exact selector")) << "explicit compat.gadget must resolve"; + expect(s::lacks(explicit_compat, "deprecated bare-name search")) << "an already-exact selector must not warn"; + write_manifest("[dependencies.mcpplibs]\ngadget = \"2.0.0\""); + auto def = build(); + expect(def.started && def.status != 0) << "mcpplibs.gadget states an identity and must not reach compat"; + expect(e2e::contains(s::text(def), "no package found for exact selector")) << "expected the explicit not-found error"; + write_manifest(""); + auto add = p.mcpp_in("app", {"add", "acme.gadget@9.9.9"}, {.env = {{"MCPP_OFFLINE", "1"}}}); + expect(add.started && add.status != 0) << "adding a package the acme index does not serve must fail"; + expect(e2e::contains(s::text(add), "compat.gadget (2.0.0)")) << "add hint must name compat.gadget with its published version"; + expect(s::lacks(p.read("app/mcpp.toml"), "gadget")) << "a rejected add must not write the dependency"; + }; + + "163 identity first resolution"_test = [] { + // SPEC-001 §3.4, §6: a package is found by its declared identity (namespace, name), not + // its file name, and addressed on the wire by its literal `package.name`; two namespaces + // may hold one short name (with xlings >= 0.4.69, #381). The fixture is the registry's + // asio descriptor, renamed. + const auto asio = s::shared_home() / "indices/mcpplibs/pkgs/c/chriskohlhoff.asio.lua"; + if (!fs::is_regular_file(asio)) + skip("no asio descriptor in registry"); + s::project root{temp_dir(), {}}; + auto mkidx = [&](std::string_view dir, std::string_view rel, std::string_view ns, std::string_view name) { + root.write(std::format("{}/pkgs/{}", dir, rel), renamed_asio(ns, name)); + }; + auto mkapp = [&](std::string_view dir, std::string_view index, std::string_view path, std::string_view ns, std::string_view name) { + root.write(std::format("{}/mcpp.toml", dir), std::format("[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[indices]\n{} = {{ path = \"{}\" }}\n\n[dependencies.{}]\n{} = \"1.38.1\"\n", index, path, ns, name)); + root.write(std::format("{}/src/main.cpp", dir), "int main() { return 0; }\n"); + }; + mkidx("idx1", "a/acme.widget.lua", "acme", "widget"); + mkapp("app1", "acme", "../idx1", "acme", "widget"); + auto b1 = root.mcpp_in("app1", {"build", "-v"}); + expect(fatal(e2e::succeeded(b1))); + expect(s::has_line(s::text(b1), R"((Compiling|Cached) acme\.widget)")) << "acme.widget was neither compiled nor served from cache"; + expect(root.exists("app1/.mcpp/.xlings/data/xpkgs/acme-x-widget")) << "expected store dir acme-x-widget"; + mkidx("idx2", "a/acme.widget.lua", "acme", "acme.widget"); + mkapp("app2", "acme", "../idx2", "acme", "widget"); + expect(fatal(e2e::succeeded(root.mcpp_in("app2", {"build"})))); + expect(root.exists("app2/.mcpp/.xlings/data/xpkgs/acme-x-acme.widget")) << "expected store dir acme-x-acme.widget"; + mkidx("idx3", "z/totally-unrelated-name.lua", "acme", "widget"); + mkapp("app3", "acme", "../idx3", "acme", "widget"); + expect(e2e::succeeded(root.mcpp_in("app3", {"build"}))) << "descriptor must be discoverable by identity, not filename"; + // Step 4 needs the sandbox's xlings at 0.4.69 or later. + std::string version; + if (const auto xl = s::shared_home() / "registry/bin/xlings"; fs::is_regular_file(xl)) { + std::smatch m; + const auto out = s::run(xl, {"--version"}).out; + const auto first = out.substr(0, out.find('\n')); + if (std::regex_search(first, m, std::regex(R"([0-9]+\.[0-9]+\.[0-9]+)"))) version = m.str(); + } + auto at_least = [](const std::string& v) { + int a = 0, b = 0, c = 0; + if (std::sscanf(v.c_str(), "%d.%d.%d", &a, &b, &c) != 3) return false; + return std::tuple{a, b, c} >= std::tuple{0, 4, 69}; + }; + if (!at_least(version)) { + std::println("step 4 skipped: sandbox xlings {} < 0.4.69", version.empty() ? "unknown" : version); + return; + } + mkidx("idx4", "a/alpha.widget.lua", "alpha", "widget"); + mkidx("idx4", "b/beta.widget.lua", "beta", "widget"); + mkapp("app4a", "alpha", "../idx4", "alpha", "widget"); + expect(fatal(e2e::succeeded(root.mcpp_in("app4a", {"build"})))); + expect(root.exists("app4a/.mcpp/.xlings/data/xpkgs/alpha-x-widget")) << "alpha:widget should land in alpha-x-widget"; + mkapp("app4b", "beta", "../idx4", "beta", "widget"); + expect(fatal(e2e::succeeded(root.mcpp_in("app4b", {"build"})))) << "beta:widget unreachable — needs xlings >= 0.4.69 (#381)"; + expect(root.exists("app4b/.mcpp/.xlings/data/xpkgs/beta-x-widget")) << "beta:widget should land in beta-x-widget"; + }; + + "165 bare name cross namespace wire address"_test = [] { + // SPEC-001 §6: the descriptor the identity gate accepted supplies both halves of the wire + // address: an explicitly selected compat dependency is installed as compat:widget, never + // mcpplibs:widget (2026-07-25, 6 of 7 workflows red on main). + s::project p{{"idx/pkgs/c/compat.widget.lua", renamed_asio("compat", "widget")}, + {"app/mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[indices]\ncompat = { path = \"../idx\" }\n\n[dependencies.compat]\nwidget = \"1.38.1\"\n"}, + {"app/src/main.cpp", "int main() { return 0; }\n"}}; + auto b = p.mcpp_in("app", {"build"}, {.env = {{"MCPP_VERBOSE", "1"}}}); + const auto out = std::regex_replace(s::text(b), std::regex(R"(\\")"), "\""); + std::string targets; + const std::regex target_list(R"re("targets":\["[^"]*"\])re"); + for (std::sregex_iterator it(out.begin(), out.end(), target_list), end; it != end; ++it) + targets += it->str() + "\n"; + expect(fatal(!targets.empty())) << "no install target was emitted at all"; + expect(s::lacks(targets, "\"mcpplibs:widget@1.38.1\"")) << "wire address took its namespace from the REQUEST (mcpplibs:widget)"; + expect(fatal(e2e::contains(targets, "\"compat:widget@1.38.1\""))) << "expected the wire address compat:widget@1.38.1"; + expect(fatal(e2e::succeeded(b))) << "build failed even though the wire address was correct"; + bool store = false; + for (auto it = fs::recursive_directory_iterator(p.at("app/.mcpp")); it != fs::recursive_directory_iterator(); ++it) + store = store || (it->is_directory() && it->path().filename() == "compat-x-widget"); + expect(store) << "expected a store dir named compat-x-widget"; + }; + + "1032 a quoted dotted dependency key is deprecated"_test = [] { + // `"acme.lib" = ...` is read as the selector `acme.lib` and is the deprecated reading + // quoted-dotted-dependency-key: warned with the unquoted key, listed by `mcpp why compat`; + // the unquoted key, and a package beside its parent, are not warned about. + const auto tmp = temp_dir(); + s::project{tmp / "lib", {{"mcpp.toml", "[package]\nnamespace = \"acme\"\nname = \"lib\"\nversion = \"0.1.0\"\n"}, + {"src/lib.cppm", "export module acme.lib;\nexport int answer() { return 42; }\n"}}}; + s::project app{tmp / "app", {{"src/main.cpp", "import acme.lib;\nint main() { return answer() == 42 ? 0 : 1; }\n"}, + {"mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies]\n\"acme.lib\" = { path = \"../lib\" }\n"}}}; + auto quoted = app.mcpp({"build"}); + expect(fatal(e2e::succeeded(quoted))) << "the quoted key no longer builds"; + expect(e2e::contains(s::text(quoted), "\"acme.lib\" is deprecated since mcpp 2026.11.8.1 and removed from mcpp 2027.2")) << "no deprecation warning for the quoted key"; + expect(e2e::contains(s::text(quoted), "Write the key unquoted, `acme.lib = ...` instead")) << "the warning does not show the unquoted spelling of its own key"; + expect(e2e::contains(s::text(app.mcpp({"why", "compat"})), "quoted-dotted-dependency-key: [dependencies] \"acme.lib\"")) << "mcpp why compat does not list the use"; + app.write("mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies]\nacme.lib = { path = \"../lib\" }\n"); + auto unquoted = app.mcpp({"build"}); + expect(fatal(e2e::succeeded(unquoted))) << "the unquoted key does not build"; + expect(s::no_line(s::text(unquoted), "quoted-dotted-dependency-key|is deprecated since")) << "the unquoted key is warned about"; + s::project{tmp / "extra", {{"mcpp.toml", "[package]\nnamespace = \"acme.lib\"\nname = \"extra\"\nversion = \"0.1.0\"\n"}, + {"src/extra.cppm", "export module acme.lib.extra;\nexport int more() { return 1; }\n"}}}; + app.write("mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies]\nacme.lib = { path = \"../lib\" }\n\"acme.lib.extra\" = { path = \"../extra\" }\n"); + auto pair = app.mcpp({"build"}); + expect(fatal(e2e::succeeded(pair))) << "a package beside its parent does not build"; + expect(s::no_line(s::text(pair), "quoted-dotted-dependency-key|is deprecated since")) << "the only spelling of the pair is warned about"; + }; + + "1074 a namespace package named like a spec field"_test = [] { + // `gpp.version = { path = ... }` is the package `version` in the namespace `gpp`: only a + // string `path`/`version`/`git` or a boolean `workspace` makes a table one specification. + const auto tmp = temp_dir(); + s::project{tmp / "version", {{"mcpp.toml", "[package]\nnamespace = \"gpp\"\nname = \"version\"\nversion = \"0.1.0\"\n"}, + {"src/version.cppm", "export module gpp.version;\nexport int gpp_version() { return 7; }\n"}}}; + s::project app{tmp / "app", {{"mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n[dependencies]\ngpp.version = { path = \"../version\" }\n"}, + {"src/main.cpp", "import gpp.version;\nint main() { return gpp_version() == 7 ? 0 : 1; }\n"}}}; + auto r = app.mcpp({"run"}); + expect(fatal(e2e::succeeded(r))) << "the package gpp.version was not read as a package"; + expect(s::lacks(s::text(r), "must specify")) << "the namespace was read as one specification"; + auto db = app.mcpp({"emit", "build-database", "--format", "json"}); + expect(fatal(e2e::succeeded(db))) << "the build database was not described"; + expect(e2e::contains(db.out, "version.cppm")) << "the build database does not hold gpp.version"; + }; +}}; + +} // namespace diff --git a/tests/e2e/deps/path.cpp b/tests/e2e/deps/path.cpp new file mode 100644 index 000000000..69295b295 --- /dev/null +++ b/tests/e2e/deps/path.cpp @@ -0,0 +1,222 @@ +// Path dependencies and the manifest's dependency tables as `mcpp add` and `mcpp remove` write +// them (formerly tests/e2e/09, 12). +import std; +import mcpplibs.mtest; +import mcpp.test.support; + +using namespace mcpplibs::mtest; +namespace s = mcpp::test::support; +namespace fs = std::filesystem; + +namespace { + +// A package of a local index: `namespace`.`name` at `version` for the three systems. +std::string index_package(std::string_view ns, std::string_view name, std::string_view description, std::string_view version) { + return std::format(R"lua(package = {{ + spec = "1", + namespace = "{0}", + name = "{1}", + description = "{2}", + licenses = {{"MIT"}}, + type = "package", + xpm = {{ + linux = {{ ["{3}"] = {{ url = "https://example.invalid/x.tar.gz" }} }}, + macosx = {{ ["{3}"] = {{ url = "https://example.invalid/x.tar.gz" }} }}, + windows = {{ ["{3}"] = {{ url = "https://example.invalid/x.zip" }} }}, + }}, +}} +)lua", ns, name, description, version); +} + +E2E path{"path", [] { + "09 path dependency"_test[tag{"requires:elf"}][tag{"requires:gcc"}] = [] { + // Package B imports modules of package A through `[dependencies.A] path`: the + // multi-package scanner and link; a path whose manifest names another package is refused. + s::needs({"elf", "gcc"}); + auto a = s::project::scaffold("mylibA"); + a.write("src/greet.cppm", "export module mylibA.greet;\nimport std;\nexport auto greet(std::string_view who) -> void {\n std::println(\"Hello {} from mylibA!\", who);\n}\n"); + a.remove("src/main.cpp"); + const std::string lib_manifest = "[package]\nname = \"mylibA\"\nversion = \"0.1.0\"\n\n[language]\nstandard = \"c++23\"\nmodules = true\nimport_std = true\n\n[modules]\nsources = [\"src/**/*.cppm\"]\n\n[targets.mylibA]\nkind = \"lib\"\n"; + a.write("mcpp.toml", lib_manifest); + auto b = s::project::scaffold("myappB"); + b.write("src/main.cpp", "import std;\nimport mylibA.greet;\nint main() {\n greet(\"modular C++23\");\n std::println(\"path dep build OK\");\n return 0;\n}\n"); + auto app_manifest = [](std::string_view path) { + return std::format("[package]\nname = \"myappB\"\nversion = \"0.1.0\"\n\n[language]\nstandard = \"c++23\"\nmodules = true\nimport_std = true\n\n[modules]\nsources = [\"src/**/*.cppm\", \"src/**/*.cpp\"]\n\n[targets.myappB]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n\n[dependencies.mylibA]\npath = \"{}\"\n", path); + }; + b.write("mcpp.toml", app_manifest("../mylibA")); + b.mcpp({"build"}); + const auto out = s::text(b.mcpp({"run"})); + expect(e2e::contains(out, "Hello modular C++23 from mylibA")) << "dep module not invoked"; + expect(e2e::contains(out, "path dep build OK")) << "main println missing"; + std::string ninja; + for (const auto& f : s::find_files(b.at("target"), R"(build\.ninja)")) + if (!f.generic_string().contains("/.build-mcpp/")) ninja += s::read_file(f); + expect(s::has_line(ninja, "mylibA.*greet.cppm")) << "ninja missing dep package source"; + expect(s::has_line(ninja, "gcm.cache/[^ /]*mylibA/mylibA.greet.gcm")) << "ninja missing dep BMI"; + // A path whose manifest names another package. + const auto wrong = temp_dir() / "elsewhere/wrongname"; + fs::create_directories(wrong.parent_path()); + fs::copy(a.dir(), wrong, fs::copy_options::recursive); + s::project{wrong, {}}.write("mcpp.toml", std::regex_replace(lib_manifest, std::regex("name = \"mylibA\""), "name = \"differentname\"")); + b.write("mcpp.toml", app_manifest(wrong.generic_string())); + auto err = b.mcpp({"build"}); + expect(err.started && err.status != 0) << "expected name-mismatch error"; + expect(e2e::contains(s::text(err), "mismatch with declared name")) << "wrong error"; + }; + + "12 add command"_test = [] { + // `mcpp add` writes [dependencies]: default-namespace packages as bare keys, `ns.name` as a + // subtable, `ns:name` only as a deprecated alias; a package no readable index serves is + // refused before mcpp.toml is touched (#305), where absence is provable; `mcpp remove` + // stays within [dependencies]. + auto app = s::project::scaffold("myapp"); + app.write("index/pkgs/w/widget.lua", index_package("mcpplibs", "widget", "default-namespace package served by a local index", "0.1.0")); + app.write("index/pkgs/g/gadget.lua", index_package("mcpplibs", "gadget", "second default-namespace package", "0.2.0")); + app.write("index/pkgs/a/acme.util.lua", index_package("acme", "util", "nested-namespace package served by a local index", "2.0.0")); + app.write("index/pkgs/c/capi.lua.lua", index_package("capi", "lua", "new exact dotted identity", "5.4.7")); + app.write("index/pkgs/m/mcpplibs.capi.lua.lua", index_package("mcpplibs.capi", "lua", "old prefixed dotted candidate", "9.9.9")); + app.write("mcpp.toml", app.read("mcpp.toml") + "\n[indices]\ndefault = { path = \"index\" }\nacme = { path = \"index\" }\ncapi = { path = \"index\" }\n"); + auto manifest = [&] { return app.read("mcpp.toml"); }; + auto failed = [](const e2e::outcome& r) { return r.started && r.status != 0; }; + + app.mcpp({"add", "widget@0.1.0"}); + expect(s::has_line(manifest(), R"(^\[dependencies\])")) << "no [dependencies] section"; + expect(s::has_line(manifest(), R"(^widget = "0\.1\.0"$)")) << "widget entry missing or quoted"; + expect(s::no_line(manifest(), R"(^"widget")")) << "default-ns key should not be quoted"; + app.mcpp({"add", "mcpplibs.gadget@0.2.0"}); + expect(eq(s::count_matching(manifest(), R"(^\[dependencies\]$)"), std::size_t{1})) << "[dependencies] header duplicated"; + expect(s::has_line(manifest(), R"(^gadget = "0\.2\.0"$)")) << "gadget not set"; + app.mcpp({"add", "acme.util@2.0.0"}); + expect(s::has_line(manifest(), R"(^\[dependencies\.acme\]$)")) << "missing [dependencies.acme] section"; + expect(s::has_line(manifest(), R"(^util = "2\.0\.0"$)")) << "util entry missing"; + expect(s::no_line(manifest(), R"(^acme\.util = )")) << "dotted selector must not be stored as an ambiguous flat key"; + const auto alias = s::text(app.mcpp({"add", "acme:util@2.0.0"})); + expect(e2e::contains(alias, "deprecated")) << "missing migration warning"; + expect(e2e::contains(alias, "acme.util@2.0.0")) << "warning lacks canonical replacement"; + const auto exact = s::text(app.mcpp({"add", "capi.lua@9.9.9"})); + expect(e2e::contains(exact, "available: 5.4.7")) << "sibling stole exact selector"; + expect(e2e::contains(exact, "mcpplibs.capi.lua")) << "migration warning lacks old selector"; + expect(e2e::contains(exact, "capi.lua")) << "migration warning lacks new selector"; + app.mcpp({"add", "capi.lua@5.4.7"}); + expect(s::has_line(manifest(), R"(^\[dependencies\.capi\]$)")) << "missing [dependencies.capi] section"; + expect(s::has_line(manifest(), R"(^lua = "5\.4\.7"$)")) << "exact capi.lua entry missing"; + app.mcpp({"add", "mcpplibs.capi.lua@9.9.9"}); + expect(s::has_line(manifest(), R"(^\[dependencies\.mcpplibs\.capi\]$)")) << "nested namespace section missing"; + app.mcpp({"remove", "mcpplibs.capi.lua"}); + expect(s::no_line(manifest(), R"(^lua = "9\.9\.9"$)")) << "nested exact remove failed"; + app.write("mcpp.toml", manifest() + "\n[dependencies.legacy]\nold = \"0.1.0\"\n"); + app.mcpp({"remove", "legacy.old"}); + expect(s::no_line(manifest(), R"(^old = "0\.1\.0"$)")) << "legacy.old was not removed"; + auto bare = app.mcpp({"add", "bareword"}); + expect(failed(bare)) << "expected error for missing version"; + expect(e2e::contains(s::text(bare), "version required")) << "wrong error"; + expect(failed(app.mcpp({"add", ":@1.0"}))) << "expected error for empty package name"; + const auto before_invalid = manifest(); + for (auto invalid : {"ocornut..imgui@1.0.0", "acme/util@1.0.0", "acme\\util@1.0.0", "acme.util@", "acme.util@1.0@2.0", "widget@1.0\"bad"}) { + auto r = app.mcpp({"add", invalid}); + expect(failed(r)) << "expected error for invalid selector: " << invalid; + expect(s::text(r).contains("invalid") || s::text(r).contains("version required")) << "wrong error for " << invalid << ": " << s::text(r); + expect(eq(manifest(), before_invalid)) << "mcpp.toml mutated for invalid selector: " << invalid; + } + const auto before = manifest(); + auto missing = app.mcpp({"add", "definitely-not-a-real-package@9.9.9"}); + expect(failed(missing)) << "expected error for missing package"; + expect(e2e::contains(s::text(missing), "not found")) << "wrong error"; + expect(e2e::contains(s::text(missing), "tried:")) << "error should list the identities tried"; + expect(eq(manifest(), before)) << "mcpp.toml mutated for missing package"; + auto nope = app.mcpp({"add", "acme.nope@1.0.0"}); + expect(failed(nope)) << "expected error for missing acme package"; + expect(e2e::contains(s::text(nope), "(acme, nope)")) << "wrong error"; + expect(e2e::contains(s::text(nope), "route: local index 'acme': root present, pkgs present")) << "missing privacy-safe local-index route state"; + expect(eq(manifest(), before)) << "mcpp.toml mutated for missing package"; + app.mcpp({"add", "unknownidx.thing@1.0.0"}); + expect(s::has_line(manifest(), R"(^\[dependencies\.unknownidx\]$)")) << "unverifiable namespace should still be added"; + auto avail = app.mcpp({"add", "widget@9.9.9"}); + expect(e2e::contains(s::text(avail), "available: 0.1.0")) << "expected available-versions warning"; + expect(s::has_line(manifest(), R"(^widget = "9\.9\.9"$)")) << "widget@9.9.9 should still be written"; + + // A workspace member reads its root's [workspace.indices]. + const auto ws_dir = temp_dir() / "ws"; + fs::create_directories(ws_dir); + fs::copy(app.at("index"), ws_dir / "index", fs::copy_options::recursive); + expect(fatal(fs::is_regular_file(ws_dir / "index/pkgs/a/acme.util.lua"))) << "workspace index fixture copy is incomplete"; + s::project ws{ws_dir, {{"mcpp.toml", "[workspace]\nmembers = [\"m1\"]\n\n[workspace.indices]\nacme = { path = \"index\" }\n"}}}; + expect(fatal(e2e::succeeded(ws.mcpp({"new", "m1"})))); + auto added = ws.mcpp_in("m1", {"add", "acme.util@2.0.0"}); + expect(e2e::succeeded(added)) << "workspace member could not read its root-owned local index"; + expect(s::has_line(ws.read("m1/mcpp.toml"), R"(^util = "2\.0\.0"$)")) << "member should inherit workspace [indices]"; + auto mnope = ws.mcpp_in("m1", {"add", "acme.nope@1.0.0"}); + expect(failed(mnope)) << "expected error inside workspace member"; + expect(e2e::contains(s::text(mnope), "(acme, nope)")) << "wrong error"; + + // A legacy flat selector is migrated, not duplicated. + s::project upsert{temp_dir() / "upsert", {}}; + upsert.write("mcpp.toml", std::format("[package]\nname = \"upsert\"\nversion = \"0.1.0\"\n\n[indices]\nacme = {{ path = \"{}\" }}\n\n[dependencies]\n\"acme.util\" = \"1.0.0\"\n", app.at("index").generic_string())); + upsert.mcpp({"add", "acme.util@2.0.0"}); + expect(s::no_line(upsert.read("mcpp.toml"), R"(^"acme\.util" = )")) << "legacy flat selector was duplicated instead of migrated"; + expect(eq(s::count_matching(upsert.read("mcpp.toml"), R"(^util = "2\.0\.0"$)"), std::size_t{1})) << "canonical exact selector was not upserted once"; + // remove stays within [dependencies]. + upsert.write("mcpp.toml", "[package]\nname = \"remove-scope\"\nversion = \"0.1.0\"\n\n[dev-dependencies]\nwidget = \"0.2.0\"\n\n[dependencies]\nwidget = \"0.1.0\"\n"); + upsert.mcpp({"remove", "widget"}); + expect(s::has_line(upsert.read("mcpp.toml"), R"(^widget = "0\.2\.0"$)")) << "remove escaped [dependencies] and deleted the dev dependency"; + expect(s::no_line(upsert.read("mcpp.toml"), R"(^widget = "0\.1\.0"$)")) << "regular dependency was not removed"; + }; + + "27 namespace dependencies"_test[tag{"requires:fresh-sandbox"}] = [] { + // `[dependencies.] name = { path = ... }` is parsed and built; the legacy quoted + // `"."` key still round-trips; a flat key of the default namespace too. + s::needs({"fresh-sandbox"}); + const s::options home{.where = s::home::inherited}; + const auto tmp = temp_dir(); + auto lib = [&](std::string_view name, std::string_view module, std::string_view body) { + s::project p{tmp / std::format("{}-pkg/{}", name, name), {}}; + p.write("src/" + std::string(name) + ".cppm", std::format("export module {};\nimport std;\n{}\n", module, body)); + p.write("mcpp.toml", std::format("[package]\nname = \"{0}\"\nversion = \"0.1.0\"\n\n[modules]\nsources = [\"src/**/*.cppm\"]\n\n[targets.{0}]\nkind = \"lib\"\n", name)); + return p.dir().generic_string(); + }; + const auto util = lib("util", "acme.util", "export int answer() { return 42; }"); + auto app = s::project::scaffold("app"); + app.write("src/main.cpp", "import std;\nimport acme.util;\nint main() { std::println(\"answer = {}\", answer()); return answer() == 42 ? 0 : 1; }\n"); + app.write("mcpp.toml", std::format("[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies.acme]\nutil = {{ path = \"{}\" }}\n", util)); + expect(fatal(e2e::succeeded(app.mcpp({"build"}, home)))) << "build failed"; + expect(eq(s::last_line(app.mcpp({"run"}, home).out), std::string("answer = 42"))); + app.write("mcpp.toml", std::format("[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies]\n\"acme.util\" = {{ path = \"{}\" }}\n", util)); + app.remove("target"); + expect(fatal(e2e::succeeded(app.mcpp({"build"}, home)))) << "legacy form build failed"; + expect(eq(s::last_line(app.mcpp({"run"}, home).out), std::string("answer = 42"))) << "legacy form"; + const auto util2 = lib("util2", "util2", "export int two() { return 2; }"); + app.write("src/main.cpp", "import std;\nimport util2;\nint main() { std::println(\"two = {}\", two()); return two() == 2 ? 0 : 1; }\n"); + app.write("mcpp.toml", std::format("[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies]\nutil2 = {{ path = \"{}\" }}\n", util2)); + app.remove("target"); + expect(fatal(e2e::succeeded(app.mcpp({"build"}, home)))) << "flat form build failed"; + expect(eq(s::last_line(app.mcpp({"run"}, home).out), std::string("two = 2"))) << "flat form"; + }; + + "31 transitive deps"_test[tag{"requires:musl"}] = [] { + // A path dependency that declares a path dependency is resolved whole (the consumer need + // not list the grandchild), and the grandchild's include_dirs reach its parent's compile; + // naming both is consistent. A home with the payloads only; a skip without a musl payload. + s::needs({"musl"}); + const s::options home{.where = s::home::payloads}; + const auto musl = s::payload_root("xim-x-musl-gcc", s::musl_gcc_version()) / "bin/x86_64-linux-musl-g++"; + if (!fs::is_regular_file(musl)) + skip("no reusable musl-gcc xpkg payload"); + const auto tmp = temp_dir(); + s::project gc{tmp / "grandchild/gc", {{"include/gc/gc.h", "#pragma once\ninline int gc_answer(void) { return 42; }\n"}, + {"src/gc.cppm", "export module gc;\n"}, + {"mcpp.toml", "[package]\nname = \"gc\"\nversion = \"0.1.0\"\n\n[build]\ninclude_dirs = [\"include\"]\n\n[targets.gc]\nkind = \"lib\"\n"}}}; + s::project ch{tmp / "child/ch", {{"src/ch.cppm", "module;\n#include \nexport module ch;\nexport int ch_answer() { return gc_answer(); }\n"}, + {"mcpp.toml", std::format("[package]\nname = \"ch\"\nversion = \"0.1.0\"\n\n[targets.ch]\nkind = \"lib\"\n\n[dependencies]\ngc = {{ path = \"{}\" }}\n", gc.dir().generic_string())}}}; + auto top = [&](std::string_view name, std::string_view extra) { + return s::project{tmp / name / name, {{"src/main.cpp", "import std;\nimport ch;\nint main() {\n std::println(\"answer={}\", ch_answer());\n return ch_answer() == 42 ? 0 : 1;\n}\n"}, + {"mcpp.toml", std::format("[package]\nname = \"{}\"\nversion = \"0.1.0\"\n\n[dependencies]\nch = {{ path = \"{}\" }}\n{}", name, ch.dir().generic_string(), extra)}}}; + }; + auto t = top("top", ""); + expect(fatal(e2e::succeeded(t.mcpp({"build"}, home)))) << "transitive build failed"; + expect(eq(s::last_line(t.mcpp({"run"}, home).out), std::string("answer=42"))); + auto t2 = top("top2", std::format("gc = {{ path = \"{}\" }}\n", gc.dir().generic_string())); + expect(e2e::succeeded(t2.mcpp({"build"}, home))) << "duplicate-but-consistent dep failed"; + }; +}}; + +} // namespace diff --git a/tests/e2e/deps/resolve.cpp b/tests/e2e/deps/resolve.cpp new file mode 100644 index 000000000..b1875a8e7 --- /dev/null +++ b/tests/e2e/deps/resolve.cpp @@ -0,0 +1,179 @@ +// Resolution of the dependency graph: versions merged or mangled, visibility, transitive edges +// (formerly tests/e2e/32, 33, 61). +import std; +import mcpplibs.mtest; +import mcpp.test.support; + +using namespace mcpplibs::mtest; +namespace s = mcpp::test::support; +namespace fs = std::filesystem; + +namespace { + +const s::options inherited{.where = s::home::inherited}; + +// An inherited home that also reads the user's mcpplibs index (indices/mcpplibs), as 32 and 33 +// set theirs up. +void with_user_index() { + const auto h = s::case_home(s::home::inherited); + fs::create_directories(h / "indices"); + const fs::path user = fs::path(s::env("HOME").value_or("")) / ".mcpp"; + for (const auto& src : {user / "indices/mcpplibs", user / "registry/data/mcpplibs", user / "registry/data/mcpp-index"}) { + std::error_code ec; + if (!fs::is_directory(src, ec)) + continue; + fs::create_directory_symlink(src, h / "indices/mcpplibs", ec); + if (ec) fs::copy(src, h / "indices/mcpplibs", fs::copy_options::recursive, ec); + break; + } +} + +// A package of a local index with an mcpp section: one library `target` of `source`, version 1.0.0. +std::string local_package(std::string_view ns, std::string_view name, std::string_view description, + std::string_view archive, std::string_view source, std::string_view target) { + return std::format(R"lua(package = {{ + spec = "1", + namespace = "{0}", + name = "{1}", + description = "{2}", + licenses = {{"MIT"}}, + type = "package", + xpm = {{ + linux = {{ + ["1.0.0"] = {{ + url = "https://example.invalid/{3}-1.0.0.tar.gz", + sha256 = "0000000000000000000000000000000000000000000000000000000000000000", + }}, + }}, + }}, + mcpp = {{ + language = "c++23", + import_std = false, + sources = {{ "{4}" }}, + targets = {{ ["{5}"] = {{ kind = "lib" }} }}, + deps = {{}}, + }}, +}} +)lua", ns, name, description, archive, source, target); +} + +E2E resolve{"resolve", [] { + "32 semver merge"_test[tag{"requires:fresh-sandbox"}] = [] { + // Two consumers of one package with overlapping constraints (exact and range) merge to + // one satisfying version instead of erroring out. + s::needs({"fresh-sandbox"}); + with_user_index(); + const auto tmp = temp_dir(); + s::project mylib{tmp / "mylib/mylib", {{"src/mylib.cppm", "export module mylib;\nexport int mylib_answer() { return 1; }\n"}, + {"mcpp.toml", "[package]\nname = \"mylib\"\nversion = \"0.1.0\"\n\n[targets.mylib]\nkind = \"lib\"\n\n[dependencies.mcpplibs]\ncmdline = \"=0.0.1\"\n"}}}; + s::project app{tmp / "app/app", {{"src/main.cpp", "import std;\nimport mylib;\nint main() {\n std::println(\"ok={}\", mylib_answer());\n return mylib_answer() == 1 ? 0 : 1;\n}\n"}, + {"mcpp.toml", std::format("[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies]\nmylib = {{ path = \"{}\" }}\n\n[dependencies.mcpplibs]\ncmdline = \">=0.0.1, <1\"\n", mylib.dir().generic_string())}}}; + auto b = app.mcpp({"build"}, inherited); + expect(fatal(e2e::succeeded(b))) << "compatible merge failed"; + expect(s::has_line(s::text(b), R"(Merged.*cmdline.*0\.0\.1|→ v0\.0\.1)")) << "no merge trace in build log"; + expect(eq(s::last_line(app.mcpp({"run"}, inherited).out), std::string("ok=1"))); + }; + + "33 multi version mangling"_test[tag{"requires:elf"}][tag{"requires:gcc"}] = [] { + // Two transitive consumers wanting non-overlapping versions of one package: the second + // copy's module name is mangled so both BMIs coexist; the header beside a staged source is + // staged with it. + s::needs({"elf", "gcc"}); + with_user_index(); + const auto tmp = temp_dir(); + s::project liba{tmp / "libA/libA", {{"src/libA.cppm", "export module libA;\nimport mcpplibs.cmdline;\nimport std;\nexport int libA_v() { return 1; }\n"}, + {"mcpp.toml", "[package]\nname = \"libA\"\nversion = \"0.1.0\"\n\n[targets.libA]\nkind = \"lib\"\n\n[dependencies.mcpplibs]\ncmdline = \"=0.0.1\"\n"}}}; + s::project libb{tmp / "libB/libB", {{"src/libB.cppm", "module;\nint libB_impl_v();\nexport module libB;\nimport mcpplibs.cmdline;\nimport std;\nexport int libB_v() { return libB_impl_v(); }\n"}, + {"src/libB_detail.h", "#pragma once\ninline int libB_detail_v() { return 2; }\n"}, + {"src/libB_impl.cpp", "#include \"libB_detail.h\"\nint libB_impl_v() { return libB_detail_v(); }\n"}, + {"mcpp.toml", "[package]\nname = \"libB\"\nversion = \"0.1.0\"\n\n[targets.libB]\nkind = \"lib\"\n\n[dependencies.mcpplibs]\ncmdline = \"=0.0.2\"\n"}}}; + s::project app{tmp / "app/app", {{"src/main.cpp", "import std;\nimport libA;\nimport libB;\nint main() {\n std::println(\"a={} b={}\", libA_v(), libB_v());\n return libA_v() + libB_v() == 3 ? 0 : 1;\n}\n"}, + {"mcpp.toml", std::format("[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies]\nlibA = {{ path = \"{}\" }}\nlibB = {{ path = \"{}\" }}\n", liba.dir().generic_string(), libb.dir().generic_string())}}}; + auto b = app.mcpp({"build"}, inherited); + expect(fatal(e2e::succeeded(b))) << "mangling build failed"; + expect(s::has_line(s::text(b), "Mangled.*cmdline")) << "expected 'Mangled' trace in build log"; + expect(s::find_file(app.at("target"), "mcpplibs.cmdline__v0_0_2__mcpp.gcm").has_value()) << "mangled BMI not found"; + expect(s::find_file(app.at("target"), "mcpplibs.cmdline.gcm").has_value()) << "primary BMI not found"; + expect(s::find_file(app.at("target/.mangled"), "libB_detail.h").has_value()) << "the header beside the staged source was not staged with it"; + expect(eq(s::last_line(app.mcpp({"run"}, inherited).out), std::string("a=1 b=2"))); + }; + + "61 dependency visibility"_test[tag{"requires:gcc"}] = [] { + // A private dependency is available while compiling its dependent, and its include + // directories do not leak to that package's consumers. + s::needs({"gcc"}); + const auto tmp = temp_dir(); + s::project hidden{tmp / "hidden", {{"include/hidden/value.hpp", "#pragma once\ninline int hidden_value() { return 7; }\n"}, + {"src/hidden.cppm", "module;\n#include \nexport module hidden;\nexport int hidden_answer() { return hidden_value(); }\n"}, + {"mcpp.toml", "[package]\nname = \"hidden\"\nversion = \"0.1.0\"\n\n[build]\ninclude_dirs = [\"include\"]\n\n[modules]\nsources = [\"src/*.cppm\"]\n\n[targets.hidden]\nkind = \"lib\"\n"}}}; + s::project facade{tmp / "facade", {{"src/facade.cppm", "export module facade;\nimport hidden;\nexport int facade_answer() { return hidden_answer(); }\n"}, + {"mcpp.toml", "[package]\nname = \"facade\"\nversion = \"0.1.0\"\n\n[modules]\nsources = [\"src/*.cppm\"]\n\n[targets.facade]\nkind = \"lib\"\n\n[dependencies]\nhidden = { path = \"../hidden\", visibility = \"private\" }\n"}}}; + s::project app{tmp / "app", {{"src/main.cpp", "import facade;\nint main() {\n return facade_answer() == 7 ? 0 : 1;\n}\n"}, + {"mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[targets.app]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n\n[dependencies]\nfacade = { path = \"../facade\" }\n"}}}; + expect(fatal(e2e::succeeded(app.mcpp({"build"}, inherited)))) << "private dep should compile its direct consumer"; + expect(e2e::succeeded(app.mcpp({"run"}, inherited))) << "app run failed"; + app.write("src/main.cpp", "#include \nimport facade;\nint main() {\n return hidden_value() == facade_answer() ? 0 : 1;\n}\n"); + auto leak = app.mcpp({"build"}, inherited); + expect(leak.started && leak.status != 0) << "private dependency include dir leaked to consumer"; + expect(e2e::contains(s::text(leak), "hidden/value.hpp")) << "consumer failed for an unexpected reason"; + }; + + "62 dotted dependency selector priority"_test[tag{"requires:gcc"}][tag{"requires:fresh-sandbox"}] = [] { + // A dotted selector in [dependencies] is exact: imgui.core -> (imgui, core), never + // mcpplibs.imgui/core. The package is provided, so the build stays offline. + s::needs({"gcc", "fresh-sandbox"}); + const auto tmp = temp_dir(); + s::project index{tmp / "imgui-index", {{"pkgs/i/imgui.core.lua", local_package("imgui", "imgui.core", "Exact dotted selector test package", "imgui-core", "src/core.cppm", "core")}}}; + s::project app{tmp / "project/app", {{".mcpp/.xlings/data/xpkgs/imgui-x-imgui.core/1.0.0/src/core.cppm", "export module imgui.core;\nexport int imgui_core_value() {\n return 42;\n}\n"}, + {"src/main.cpp", "import imgui.core;\nint main() {\n return imgui_core_value() == 42 ? 0 : 1;\n}\n"}}}; + app.write("mcpp.toml", std::format("[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[indices]\nimgui = {{ path = \"{}\" }}\n\n[dependencies]\nimgui.core = \"1.0.0\"\n\n[targets.app]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n", index.dir().generic_string())); + expect(fatal(e2e::succeeded(app.mcpp({"build"}, inherited)))) << "build failed"; + expect(fatal(e2e::succeeded(app.mcpp({"run"}, inherited)))) << "run failed"; + expect(e2e::contains(app.read("mcpp.lock"), "namespace = \"imgui\"")) << "expected lockfile to record resolved peer namespace imgui"; + }; + + "63 bare dependency peer root priority"_test[tag{"requires:gcc"}][tag{"requires:fresh-sandbox"}] = [] { + // A bare selector means the default mcpplibs namespace: `imgui = "1.0.0"` locks + // mcpplibs:imgui. + s::needs({"gcc", "fresh-sandbox"}); + const auto h = s::case_home(s::home::inherited); + s::project index{h / "indices/mcpplibs", {{".mcpp-index-updated", "ok\n"}, + {"pkgs/i/imgui.lua", local_package("mcpplibs", "imgui", "Default-namespace exact selector test package", "imgui", "src/imgui.cppm", "imgui")}}}; + s::project payload{h / "registry/data/xpkgs/mcpplibs-x-imgui/1.0.0", {{"src/imgui.cppm", "export module imgui;\nexport int imgui_value() {\n return 42;\n}\n"}, + {".mcpp_ok", "ok\n"}}}; + s::project app{temp_dir() / "project/app", {{"src/main.cpp", "import imgui;\nint main() {\n return imgui_value() == 42 ? 0 : 1;\n}\n"}, + {"mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies]\nimgui = \"1.0.0\"\n\n[targets.app]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n"}}}; + expect(fatal(e2e::succeeded(app.mcpp({"build"}, inherited)))) << "build failed"; + expect(fatal(e2e::succeeded(app.mcpp({"run"}, inherited)))) << "run failed"; + expect(e2e::contains(app.read("mcpp.lock"), "[package.\"imgui\"]")) << "expected imgui package lock entry"; + expect(e2e::contains(app.read("mcpp.lock"), "namespace = \"mcpplibs\"")) << "bare package must lock as the default mcpplibs namespace"; + }; + + "79 gtest regular dep feature main"_test = [] { + // #168: compat.gtest as a regular dependency (`mcpp add compat.gtest`) does not inject + // gtest_main into an app with its own main; `features = ["main"]` does; `add --dev` + // writes [dev-dependencies.compat]. + auto app = s::project::scaffold("app"); + app.mcpp({"add", "compat.gtest@1.15.2"}); + expect(fatal(s::has_line(app.read("mcpp.toml"), R"(^\[dependencies\.compat\])"))) << "add did not write [dependencies.compat]"; + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "#168 — build with regular-dep gtest failed"; + auto newest_graph = [&] { + std::optional best; + for (const auto& f : s::find_files(app.at("target"), R"(build\.ninja)")) + if (!f.generic_string().contains("/.build-mcpp/") && (!best || fs::last_write_time(f) > fs::last_write_time(*best))) best = f; + expect(fatal(best.has_value())) << "no build.ninja"; + return s::read_file(*best); + }; + expect(s::lacks(newest_graph(), "gtest_main")) << "gtest_main linked into app by default (would collide with main)"; + expect(e2e::contains(newest_graph(), "gtest-all")) << "gtest framework (gtest-all) not linked"; + app.write("mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies.compat]\ngtest = { version = \"1.15.2\", features = [\"main\"] }\n"); + app.write("src/main.cpp", "#include \nTEST(App, ok) { EXPECT_EQ(1 + 1, 2); }\n"); + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "features=[main] build failed"; + expect(e2e::contains(newest_graph(), "gtest_main")) << "features=[main] did not link gtest_main"; + auto lib = s::project::scaffold("libapp"); + lib.mcpp({"add", "--dev", "compat.gtest@1.15.2"}); + expect(s::has_line(lib.read("mcpp.toml"), R"(^\[dev-dependencies\.compat\])")) << "add --dev did not write [dev-dependencies.compat]"; + }; +}}; + +} // namespace diff --git a/tests/e2e/deps/tools.cpp b/tests/e2e/deps/tools.cpp new file mode 100644 index 000000000..7eb2817db --- /dev/null +++ b/tests/e2e/deps/tools.cpp @@ -0,0 +1,129 @@ +// A tool package declared by more than one package of a graph: one version, floors, ranges +// (formerly tests/e2e/627, 628, 629). +import std; +import mcpplibs.mtest; +import mcpp.test.support; + +using namespace mcpplibs::mtest; +namespace s = mcpp::test::support; +namespace fs = std::filesystem; + +namespace { + +const s::options fresh{.where = s::home::fresh}; + +bool failed(const e2e::outcome& r) { return r.started && r.status != 0; } + +// The build program of 627 and 629: records what `xpkg_dir("xim", "zoxide")` answered in the +// package's answered.txt (629's refuses an empty answer). +constexpr std::string_view recorder = R"cpp(#include +#include +import mcpp; +int main() { + const char* d = mcpp::xpkg_dir("xim", "zoxide"); + std::string out = std::string(mcpp::manifest_dir()) + "/answered.txt"; + std::FILE* f = std::fopen(out.c_str(), "w"); + if (f == nullptr) return 3; + std::fprintf(f, "%s\n", d == nullptr ? "" : d); + std::fclose(f); + return 0; +} +)cpp"; + +constexpr std::string_view refusing_recorder = R"cpp(#include +#include +import mcpp; +int main() { + const char* d = mcpp::xpkg_dir("xim", "zoxide"); + if (d == nullptr || d[0] == '\0') { + std::fprintf(stderr, + "a [feature-xlings] entry stated as a range was installed and then " + "answered as absent\n"); + return 2; + } + std::string out = std::string(mcpp::manifest_dir()) + "/answered.txt"; + std::FILE* f = std::fopen(out.c_str(), "w"); + if (f == nullptr) return 3; + std::fprintf(f, "%s\n", d); + std::fclose(f); + return 0; +} +)cpp"; + +// dep/ (the library `toolowner`, `extra` appended to its manifest) and app/ (the binary +// `consumer` depending on it by path, `app_extra` in its manifest). +struct graph { + s::project dep; + s::project app; +}; + +graph make_graph(std::string_view dep_extra, std::string_view dep_dependency, std::string_view app_extra) { + const auto tmp = temp_dir(); + graph g{s::project{tmp / "dep", {{"src/lib.cpp", "int dep_touch() { return 1; }\n"}}}, + s::project{tmp / "app", {{"src/main.cpp", "int main() { return 0; }\n"}}}}; + g.dep.write("mcpp.toml", std::format("[package]\nname = \"toolowner\"\nversion = \"0.1.0\"\n[modules]\nsources = [\"src/**/*.cpp\"]\n{}[targets.toolowner]\nkind = \"lib\"\n", dep_extra)); + g.app.write("mcpp.toml", std::format("[package]\nname = \"consumer\"\nversion = \"0.1.0\"\n[dependencies]\ntoolowner = {}\n{}[targets.consumer]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n", dep_dependency, app_extra)); + return g; +} + +E2E tools{"tools", [] { + "627 one package one version"_test[tag{"requires:gcc"}] = [] { + // A project and one of its dependencies both name `xim:zoxide`, at different versions: + // one package, so one version is installed (the declaration nearer the artifact), the + // dependency's build program is answered that version, and the override is reported. + s::needs({"gcc"}); + auto g = make_graph("[xlings.workspace]\n\"xim:zoxide\" = \"0.9.7\"\n", "{ path = \"../dep\" }", "[xlings.workspace]\n\"xim:zoxide\" = \"0.9.9\"\n"); + g.dep.write("build.mcpp", recorder); + auto b = g.app.mcpp({"build"}, fresh); + expect(fatal(e2e::succeeded(b))) << "the consumer did not build"; + const auto store = s::case_home(s::home::fresh) / "registry/data/xpkgs/xim-x-zoxide"; + expect(fatal(fs::is_directory(store))) << "zoxide was never installed (" << store.string() << ")"; + std::vector installed; // `ls -1`: the store's own entries + for (const auto& e : fs::directory_iterator(store)) installed.push_back(e.path()); + expect(eq(installed.size(), std::size_t{1})) << "two declarations of one package installed " << installed.size() << " versions"; + expect(fs::is_directory(store / "0.9.9")) << "the declaration nearer the artifact did not win"; + expect(fatal(g.dep.exists("answered.txt") && !g.dep.read("answered.txt").empty())) << "the dependency's build program recorded nothing"; + expect(e2e::contains(g.dep.read("answered.txt"), "0.9.9")) << "installed 0.9.9 and answered " << g.dep.read("answered.txt"); + expect(e2e::contains(s::text(b), "declared at two versions")) << "the override was not reported"; + expect(e2e::contains(s::text(b), "toolowner")) << "the report does not name the dependency that lost"; + }; + + "628 a pin below a stated floor is refused"_test = [] { + // No capability gate: the first leg refuses before anything is compiled and the second + // builds an ordinary library. + auto g = make_graph("[xlings.workspace]\n\"xim:zoxide\" = \">=0.9.9\"\n", "{ path = \"../dep\" }", "[xlings.workspace]\n\"xim:zoxide\" = \"0.9.7\"\n"); + auto b = g.app.mcpp({"build"}, fresh); + expect(fatal(failed(b))) << "a pin below the dependency's floor was accepted"; + for (std::string_view needle : {"xim:zoxide", "0.9.7", ">=0.9.9", "toolowner", "this project"}) + expect(e2e::contains(s::text(b), needle)) << "the refusal does not mention '" << needle << "'"; + expect(e2e::contains(s::text(b), "drop the pin")) << "the refusal does not state the way out"; + auto why = g.app.mcpp({"why", "toolchain", "--format", "json"}, fresh); + const auto data = s::json(why.out); + const auto reason = data.contains("data") && data["data"].contains("reason") ? data["data"]["reason"].get() : std::string{}; + expect(eq(reason, std::string{"tool-version-conflict"})) << "reason was '" << reason << "', expected tool-version-conflict"; + g.app.write("mcpp.toml", "[package]\nname = \"consumer\"\nversion = \"0.1.0\"\n[dependencies]\ntoolowner = { path = \"../dep\" }\n[xlings.workspace]\n\"xim:zoxide\" = \"0.9.9\"\n[targets.consumer]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n"); + expect(e2e::succeeded(g.app.mcpp({"build"}, fresh))) << "a pin that DOES satisfy the floor was still refused"; + }; + + "629 a version range is answered by xpkg dir"_test[tag{"requires:gcc"}] = [] { + // A range in an xlings address's version position is a constraint: installed, and + // answered by `xpkg_dir`; an unsatisfiable one is refused, naming it. + s::needs({"gcc"}); + auto g = make_graph("[features]\ndefault = []\nusestool = []\n[feature-xlings.usestool]\n\"xim:zoxide\" = \">=0.9.9\"\n", "{ path = \"../dep\", features = [\"usestool\"] }", ""); + g.dep.write("build.mcpp", refusing_recorder); + expect(fatal(e2e::succeeded(g.app.mcpp({"build"}, fresh)))) << "the consumer did not build"; + expect(fatal(g.dep.exists("answered.txt") && !g.dep.read("answered.txt").empty())) << "the build program recorded nothing"; + const auto answered = g.dep.read("answered.txt"); + expect(e2e::contains(answered, "xim-x-zoxide")) << "the recorded path does not name zoxide: " << answered; + expect(s::has_line(answered, R"(/(0\.9\.9|0\.10\.[0-9]+)$)")) << "the answer does not satisfy >=0.9.9: " << answered; + g.dep.write("mcpp.toml", "[package]\nname = \"toolowner\"\nversion = \"0.1.0\"\n[modules]\nsources = [\"src/**/*.cpp\"]\n[features]\ndefault = []\nusestool = []\n[feature-xlings.usestool]\n\"xim:zoxide\" = \">=99.0.0\"\n[targets.toolowner]\nkind = \"lib\"\n"); + fs::remove_all(g.app.at("target")); + g.dep.remove("answered.txt"); + auto u = g.app.mcpp({"build"}, fresh); + expect(fatal(failed(u))) << "an unsatisfiable range was accepted"; + expect(e2e::contains(s::text(u), "not found in the synced index")) << "refused, but not because the range is unsatisfiable"; + expect(e2e::contains(s::text(u), ">=99.0.0")) << "the refusal does not quote the range that could not be satisfied"; + }; +}}; + +} // namespace From 65b97724c9543daef375162f556aa8aa1de22999 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 13 Nov 2026 18:40:17 +0900 Subject: [PATCH 007/115] e2e: the actions subsystem as C++ cases (37 scripts) The 37 scripts MIGRATION.tsv assigns to `actions` (actions a build program declares and their roles, checks and their stamps, `prepare`, generated files, device sources and the actions that consume them, rule packages, project build hooks, what an action writes and reads, action progress) are 37 cases of the program e2e/actions (tests/e2e/actions/*.cpp, 10 files: checks, commands, declared, devices, generated, hooks, outputs, prepare, progress, rules). Each case is named by its script's sentence and keeps its number; its `# requires:` words are its tags. Scripts: 51 107 124 142 188 313 314 315 317 606 607 608 609 610 611 613 619 623 630 631 648 659 660 780 790 791 792 793 798 799 808 827 896 897 982 1013 1015. No workflow runs any of them by path; all 37 are removed. Old run (the scripts, against this branch's mcpp, Linux x86_64): 36 passed, 1 failed (790). New run (`mcpp test e2e/actions`): 36 passed, 1 failed (790), 0 skipped. 790 fails on this host in both runs for the reason 779 does (batch 2): its install.sh links a shared library with the `gcc` PATH names, an xlings subos gcc whose objects record NEEDED libc.so, a linker script the loader cannot load ("invalid ELF header" when the program runs, under the script); under `mcpp test` PATH names a subos gcc that does not find crti.o. tests/support gains archive_members and archive_symbols: an `ar` archive's members and its symbol index, read by this process (System V/GNU and BSD). Every assertion is kept. Where a reading changed: - 608's `ar t` and `nm` are read from the archive (its members and its symbol index); the script read nm only when PATH had it. - 606 read `mcpp why toolchain --format json` with jq; 808, 897 and 982 read JSON with python3 without requiring it. The cases use nlohmann::json. - 896's mtime in nanoseconds (python3) and 142/188/660's `stat %Y` are std::filesystem::last_write_time; 827's `find -newer ` compares the destination's time with the one it had. - 1013 and 1015 keep their Python drivers: the actions they declare run Python workers, and 1013 drives the build through a terminal and a process group. Each case writes its script's driver unchanged and runs python3 on it; both keep requiring python3. - 317's Ctrl-C leg sends SIGINT through `sh -c` as the script did (Unix only, as before). - 659's `type -P true` is the `true` PATH names; 799's `pwd -P` is the canonical path. - `mcpp run 2>&1 | grep '^X=' | tail -1` is the last such line of the program's standard output. --- ...n_progress_paths_are_utf8_on_every_host.sh | 50 -- tests/e2e/107_generated_files_toml.sh | 58 -- .../124_gen_source_out_of_root_object_path.sh | 100 ---- tests/e2e/142_generated_files_incremental.sh | 90 --- tests/e2e/188_build_actions.sh | 494 ----------------- .../e2e/313_check_stamp_on_every_platform.sh | 92 --- .../314_dependency_action_generated_header.sh | 158 ------ .../315_blocking_check_gates_compilation.sh | 105 ---- tests/e2e/317_project_build_hooks.sh | 522 ------------------ tests/e2e/51_package_generated_files.sh | 103 ---- ...rained_source_globs_narrow_to_the_build.sh | 123 ----- ...07_chained_actions_form_the_device_link.sh | 85 --- ...8_object_actions_reach_a_static_library.sh | 84 --- .../609_shader_sources_are_device_sources.sh | 162 ------ .../610_feature_controlled_rule_collection.sh | 202 ------- .../e2e/611_variant_switch_is_not_replayed.sh | 79 --- .../613_sycl_sources_are_device_sources.sh | 171 ------ ..._tool_may_not_be_conditioned_on_a_layer.sh | 60 -- ...623_device_sources_must_reach_an_action.sh | 83 --- .../630_a_rule_brings_its_own_environment.sh | 111 ---- ...ile_tracks_a_runtime_discovered_include.sh | 147 ----- ...8_an_install_hook_sees_the_build_target.sh | 96 ---- ...an_action_declaration_has_no_size_limit.sh | 209 ------- ...tion_names_the_engine_through_mcpp_self.sh | 124 ----- ...ck_moves_its_stamp_past_a_changed_input.sh | 81 --- ...e_action_populates_an_unknown_directory.sh | 139 ----- ...epare_action_that_creates_nothing_fails.sh | 69 --- ...ut_inside_a_prepare_directory_is_warned.sh | 110 ---- .../793_an_unknown_action_role_is_refused.sh | 101 ---- ...rule_applies_through_a_source_it_claims.sh | 74 --- ...799_an_action_runs_with_its_env_and_cwd.sh | 96 ---- ...808_device_source_is_not_a_compile_unit.sh | 87 --- .../827_many_placements_run_as_one_process.sh | 80 --- .../896_an_action_output_has_one_writer.sh | 83 --- ...figuration_has_its_own_generated_output.sh | 91 --- ...982_an_action_reads_the_builds_contract.sh | 59 -- tests/e2e/MIGRATION.tsv | 74 +-- tests/e2e/actions/checks.cpp | 130 +++++ tests/e2e/actions/commands.cpp | 284 ++++++++++ tests/e2e/actions/declared.cpp | 314 +++++++++++ tests/e2e/actions/devices.cpp | 303 ++++++++++ tests/e2e/actions/generated.cpp | 215 ++++++++ tests/e2e/actions/hooks.cpp | 240 ++++++++ tests/e2e/actions/outputs.cpp | 227 ++++++++ tests/e2e/actions/prepare.cpp | 217 ++++++++ .../progress.cpp} | 102 +++- tests/e2e/actions/rules.cpp | 226 ++++++++ tests/support/src/support.cpp | 87 +++ tests/support/src/support.cppm | 5 + 49 files changed, 2375 insertions(+), 4627 deletions(-) delete mode 100755 tests/e2e/1015_action_progress_paths_are_utf8_on_every_host.sh delete mode 100755 tests/e2e/107_generated_files_toml.sh delete mode 100755 tests/e2e/124_gen_source_out_of_root_object_path.sh delete mode 100755 tests/e2e/142_generated_files_incremental.sh delete mode 100755 tests/e2e/188_build_actions.sh delete mode 100755 tests/e2e/313_check_stamp_on_every_platform.sh delete mode 100755 tests/e2e/314_dependency_action_generated_header.sh delete mode 100755 tests/e2e/315_blocking_check_gates_compilation.sh delete mode 100644 tests/e2e/317_project_build_hooks.sh delete mode 100755 tests/e2e/51_package_generated_files.sh delete mode 100755 tests/e2e/606_constrained_source_globs_narrow_to_the_build.sh delete mode 100755 tests/e2e/607_chained_actions_form_the_device_link.sh delete mode 100755 tests/e2e/608_object_actions_reach_a_static_library.sh delete mode 100755 tests/e2e/609_shader_sources_are_device_sources.sh delete mode 100755 tests/e2e/610_feature_controlled_rule_collection.sh delete mode 100755 tests/e2e/611_variant_switch_is_not_replayed.sh delete mode 100755 tests/e2e/613_sycl_sources_are_device_sources.sh delete mode 100755 tests/e2e/619_a_tool_may_not_be_conditioned_on_a_layer.sh delete mode 100755 tests/e2e/623_device_sources_must_reach_an_action.sh delete mode 100755 tests/e2e/630_a_rule_brings_its_own_environment.sh delete mode 100755 tests/e2e/631_action_depfile_tracks_a_runtime_discovered_include.sh delete mode 100755 tests/e2e/648_an_install_hook_sees_the_build_target.sh delete mode 100755 tests/e2e/659_an_action_declaration_has_no_size_limit.sh delete mode 100755 tests/e2e/660_an_action_names_the_engine_through_mcpp_self.sh delete mode 100755 tests/e2e/780_a_passing_check_moves_its_stamp_past_a_changed_input.sh delete mode 100644 tests/e2e/790_a_prepare_action_populates_an_unknown_directory.sh delete mode 100644 tests/e2e/791_a_prepare_action_that_creates_nothing_fails.sh delete mode 100644 tests/e2e/792_a_rerun_input_inside_a_prepare_directory_is_warned.sh delete mode 100644 tests/e2e/793_an_unknown_action_role_is_refused.sh delete mode 100755 tests/e2e/798_a_rule_applies_through_a_source_it_claims.sh delete mode 100755 tests/e2e/799_an_action_runs_with_its_env_and_cwd.sh delete mode 100755 tests/e2e/808_device_source_is_not_a_compile_unit.sh delete mode 100755 tests/e2e/827_many_placements_run_as_one_process.sh delete mode 100755 tests/e2e/896_an_action_output_has_one_writer.sh delete mode 100755 tests/e2e/897_each_configuration_has_its_own_generated_output.sh delete mode 100755 tests/e2e/982_an_action_reads_the_builds_contract.sh create mode 100644 tests/e2e/actions/checks.cpp create mode 100644 tests/e2e/actions/commands.cpp create mode 100644 tests/e2e/actions/declared.cpp create mode 100644 tests/e2e/actions/devices.cpp create mode 100644 tests/e2e/actions/generated.cpp create mode 100644 tests/e2e/actions/hooks.cpp create mode 100644 tests/e2e/actions/outputs.cpp create mode 100644 tests/e2e/actions/prepare.cpp rename tests/e2e/{1013_optional_action_progress_preserves_outputs.sh => actions/progress.cpp} (56%) mode change 100755 => 100644 create mode 100644 tests/e2e/actions/rules.cpp diff --git a/tests/e2e/1015_action_progress_paths_are_utf8_on_every_host.sh b/tests/e2e/1015_action_progress_paths_are_utf8_on_every_host.sh deleted file mode 100755 index 00814e6eb..000000000 --- a/tests/e2e/1015_action_progress_paths_are_utf8_on_every_host.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env bash -# requires: python3 -# Native worker/environment/file transport, including a non-ASCII project -# directory. This avoids pty and Unix signals so Windows runs it too. -set -e -MCPP="${MCPP:-mcpp}" -if command -v cygpath >/dev/null 2>&1; then - export MCPP_PROGRESS_TEST_EXE="$(cygpath -w "$(command -v "$MCPP")")" -else - export MCPP_PROGRESS_TEST_EXE="$(command -v "$MCPP")" -fi -python3 - <<'PY' -from pathlib import Path -import json,os,subprocess,sys,tempfile -with tempfile.TemporaryDirectory(prefix='mcpp-progress-') as temporary: - root=Path(temporary)/'项目-进度';(root/'src').mkdir(parents=True) - (root/'mcpp.toml').write_text('[package]\nname="portable-progress"\nversion="0.1.0"\n',encoding='utf-8') - (root/'src/main.cpp').write_text('int main(){return 0;}\n',encoding='utf-8') - worker=root/'worker.py';proof=root/'target/proof.json' - worker.write_text('''from pathlib import Path -import json,os,sys,time -file=os.environ.get('MCPP_ACTION_PROGRESS_FILE','') -assert file, 'the enabled action has no execution channel' -p=Path(file) -for seq in range(2): - state=dict(v=1,seq=seq,phase='compile',done=seq,total=1) - staged=p.with_suffix('.tmp');staged.write_text(json.dumps(state),encoding='utf-8') - os.replace(staged,p);time.sleep(.25) -Path(sys.argv[1]).write_text(json.dumps(dict(channel=file,snapshot=state)),encoding='utf-8') -''',encoding='utf-8') - quote=lambda value:json.dumps(str(value),ensure_ascii=False) - (root/'build.mcpp').write_text('''import std; -import mcpp; -int main(){ - mcpp::action action; - action.id="portable-worker";action.role=mcpp::roles::check;action.progress=true; - const auto stamp=std::string(mcpp::out_dir())+"/worker.stamp"; - action.arg(PYTHON).arg(WORKER).arg(PROOF).input(WORKER).output(stamp.c_str()); - action.submit(); -} -'''.replace('PYTHON',quote(sys.executable)).replace('WORKER',quote(worker)).replace('PROOF',quote(proof)),encoding='utf-8') - exe=os.environ['MCPP_PROGRESS_TEST_EXE'] - result=subprocess.run([exe,'build'],cwd=root,env=dict(os.environ,MCPP_PROGRESS='plain'),capture_output=True,text=True,encoding='utf-8',errors='replace',timeout=180) - assert result.returncode==0,result.stdout+result.stderr - result=json.loads(proof.read_text(encoding='utf-8')) - assert '项目-进度' in result['channel'],result - assert result['snapshot']==dict(v=1,seq=1,phase='compile',done=1,total=1),result - assert not Path(result['channel']).exists(),'execution channel was not cleaned' -print('PASS: native action progress paths are UTF-8 and cleaned') -PY diff --git a/tests/e2e/107_generated_files_toml.sh b/tests/e2e/107_generated_files_toml.sh deleted file mode 100755 index ef8f961cb..000000000 --- a/tests/e2e/107_generated_files_toml.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# G6: mcpp.toml [generated_files] — path = "contents" entries materialize -# into the project root before the scan (multiline TOML strings), enter the -# fingerprint (content change → rebuild), and escaping paths hard-error. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT - -cd "$TMP" -"$MCPP" new genf > /dev/null -cd genf -rm -f src/main.cpp - -cat > mcpp.toml <<'EOF' -[package] -name = "genf" -version = "0.1.0" - -[generated_files] -"src/gen/answer.cpp" = """ -extern "C" int gen_answer() { return 41; } -""" -EOF - -cat > src/main.cpp <<'EOF' -import std; -extern "C" int gen_answer(); -int main() { - std::println("gen = {}", gen_answer()); - return 0; -} -EOF - -"$MCPP" build > build.log 2>&1 || { cat build.log; echo "build failed"; exit 1; } -[[ -f src/gen/answer.cpp ]] || { echo "generated file not materialized"; exit 1; } -out="$("$MCPP" run 2>&1 | tail -1)" -[[ "$out" == "gen = 41" ]] || { echo "unexpected output: $out"; exit 1; } - -# Content change re-materializes and rebuilds (content is in the fingerprint). -sed -i.bak 's/return 41/return 42/' mcpp.toml -"$MCPP" build > rebuild.log 2>&1 || { cat rebuild.log; echo "rebuild failed"; exit 1; } -out="$("$MCPP" run 2>&1 | tail -1)" -[[ "$out" == "gen = 42" ]] || { echo "stale after content change: $out"; exit 1; } - -# Escaping path must be rejected at parse time. -cat > mcpp.toml <<'EOF' -[package] -name = "genf" -version = "0.1.0" -[generated_files] -"../evil.cpp" = "int x;" -EOF -"$MCPP" build > escape.log 2>&1 && { echo "escaping path was accepted"; exit 1; } -grep -qi "generated_files" escape.log || { cat escape.log; echo "wrong error"; exit 1; } - -echo "OK" diff --git a/tests/e2e/124_gen_source_out_of_root_object_path.sh b/tests/e2e/124_gen_source_out_of_root_object_path.sh deleted file mode 100755 index 717c1b3b0..000000000 --- a/tests/e2e/124_gen_source_out_of_root_object_path.sh +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/env bash -# requires: elf gcc -# mcpp#239 (follow-up to #233): a dependency's build.mcpp can emit generated -# sources into OUT_DIR (/target/.build-mcpp/deps/@/out), -# which live OUTSIDE the dependency's package root. Their scanner relPath is -# `relative(, )` and therefore carries `..` -# components. #233's collision-disambiguation pasted that relPath straight -# into the object path: -# cc1plus: fatal error: opening output file -# obj//..//target/.../out/gen.o: No such file or directory -# i.e. `obj//../…` climbs out of obj/ (and with enough `..`, mirrors the -# whole absolute tree under CWD). The collision prefix must be sanitized so -# every object stays under obj/. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -# Dependency whose build.mcpp writes gen.cpp into OUT_DIR (absolute path, -# outside the package root). -mkdir -p gdep/src -cat > gdep/build.mcpp <<'EOF' -#include -#include -import mcpp; -int main() { - std::string p = std::string(mcpp::out_dir()) + "/gen.cpp"; - std::FILE* f = std::fopen(p.c_str(), "w"); - std::fputs("int gen_val() { return 7; }\n", f); - std::fclose(f); - mcpp::generated("gen.cpp"); - return 0; -} -EOF -cat > gdep/src/lib.cpp <<'EOF' -extern int gen_val(); -int gdep_use() { return gen_val(); } -EOF -cat > gdep/mcpp.toml <<'EOF' -[package] -name = "gdep" -version = "0.1.0" -[modules] -sources = ["src/**/*.cpp"] -[targets.gdep] -kind = "lib" -EOF - -# Consumer with its OWN src/gen.cpp — same basename as gdep's generated -# gen.cpp, forcing the disambiguation branch on the out-of-root abs source. -mkdir -p consumer/src -cat > consumer/src/gen.cpp <<'EOF' -int consumer_gen() { return 100; } -EOF -cat > consumer/src/main.cpp <<'EOF' -import std; -extern int gdep_use(); -extern int consumer_gen(); -int main() { - std::println("sum={}", gdep_use() + consumer_gen()); - return (gdep_use() == 7 && consumer_gen() == 100) ? 0 : 1; -} -EOF -cat > consumer/mcpp.toml <<'EOF' -[package] -name = "consumer" -version = "0.1.0" -[modules] -sources = ["src/**/*.cpp"] -[dependencies] -gdep = { path = "../gdep" } -[targets.consumer] -kind = "bin" -main = "src/main.cpp" -EOF - -cd consumer -"$MCPP" build > build.log 2>&1 || { - cat build.log - echo "FAIL: build failed (expected: out-of-root generated source must map under obj/)" - exit 1 -} - -ninja_file="$(find target -name build.ninja -not -path '*/.build-mcpp/*' | head -1)" -[[ -n "$ninja_file" ]] || { echo "no build.ninja generated"; exit 1; } - -# No object output may escape obj/ via `..` or an absolute root. -bad="$(grep -oE 'build [^ ]+\.o : cxx_object' "$ninja_file" | awk '{print $2}' \ - | grep -E '(^|/)\.\.(/|$)|^/' || true)" -if [[ -n "$bad" ]]; then - echo "FAIL: object path escapes obj/:" - echo "$bad" - exit 1 -fi - -out="$("$MCPP" run 2>&1 | tail -1)" -[[ "$out" == "sum=107" ]] || { echo "unexpected output: $out"; exit 1; } - -echo "OK" diff --git a/tests/e2e/142_generated_files_incremental.sh b/tests/e2e/142_generated_files_incremental.sh deleted file mode 100755 index 3c00c2e55..000000000 --- a/tests/e2e/142_generated_files_incremental.sh +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# Incremental x generated_files: an unchanged [generated_files] entry must NOT -# be rewritten when prepare_build runs — ninja is mtime-driven, so a gratuitous -# rewrite of a materialized header recompiles every TU that #includes it (via -# depfiles). A truly no-op rebuild is masked by the P0 fast-path (prepare is -# skipped), so the load-bearing case is: touch an UNRELATED source, which -# abandons the fast-path and re-runs prepare (and materialization). Assert the -# generated header keeps its mtime and the including TU is not recompiled; -# a content change must still rewrite and rebuild (no false-fresh). -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT - -mtime() { stat -c %Y "$1" 2>/dev/null || stat -f %m "$1"; } - -cd "$TMP" -"$MCPP" new genincr > /dev/null -cd genincr -rm -f src/main.cpp - -cat > mcpp.toml <<'EOF' -[package] -name = "genincr" -version = "0.1.0" - -[generated_files] -"src/gen/config.h" = """ -#define GEN_ANSWER 41 -""" -EOF - -cat > src/main.cpp <<'EOF' -#include "gen/config.h" -#include -extern int other(); -int main() { - std::printf("gen = %d\n", GEN_ANSWER + other()); - return 0; -} -EOF - -cat > src/other.cpp <<'EOF' -int other() { return 0; } -EOF - -"$MCPP" build > build.log 2>&1 || { cat build.log; echo "build failed"; exit 1; } -[[ -f src/gen/config.h ]] || { echo "generated file not materialized"; exit 1; } - -main_obj=$(find target -name 'main*.o' | head -1) -other_obj=$(find target -name 'other*.o' | head -1) -[[ -n "$main_obj" && -n "$other_obj" ]] || { echo "object files not found under target/"; exit 1; } - -gen_before=$(mtime src/gen/config.h) -main_before=$(mtime "$main_obj") - -sleep 1 # make a rewrite observable at 1s mtime granularity - -# Touch an unrelated TU: the fast-path is abandoned, prepare_build re-runs, -# and generated_files are re-materialized. -touch src/other.cpp -# (c) the incremental rebuild still succeeds ... -"$MCPP" build > rebuild.log 2>&1 || { cat rebuild.log; echo "incremental rebuild failed"; exit 1; } -# ... and other.o really was recompiled (prepare + compile did run). -other_after=$(mtime "$other_obj") -[[ "$other_after" -gt "$gen_before" ]] || { echo "other.o was not recompiled — test premise broken"; exit 1; } -# (a) the generated header was NOT rewritten (mtime preserved) ... -gen_after=$(mtime src/gen/config.h) -[[ "$gen_before" == "$gen_after" ]] || { echo "generated file rewritten on incremental rebuild ($gen_before -> $gen_after)"; exit 1; } -# (b) ... so main.o (which #includes it, untouched) was NOT recompiled. -main_after=$(mtime "$main_obj") -[[ "$main_before" == "$main_after" ]] || { echo "main.o recompiled on incremental rebuild ($main_before -> $main_after)"; exit 1; } - -# Content change: the file must be rewritten and the TU recompiled (no -# false-fresh — change detection lives in the fingerprint). -sed -i.bak 's/GEN_ANSWER 41/GEN_ANSWER 42/' mcpp.toml -sleep 1 -"$MCPP" build > change.log 2>&1 || { cat change.log; echo "rebuild after content change failed"; exit 1; } -gen_changed=$(mtime src/gen/config.h) -[[ "$gen_after" != "$gen_changed" ]] || { echo "generated file not rewritten after content change"; exit 1; } -grep -q "GEN_ANSWER 42" src/gen/config.h || { echo "generated file has stale content"; exit 1; } -# The content change alters the fingerprint, which may relocate the build -# dir — assert on the NEWEST main*.o, wherever it lives. -main_changed=$(find target -name 'main*.o' | while read -r f; do mtime "$f"; done | sort -n | tail -1) -[[ "$main_changed" -ge "$gen_changed" ]] || { echo "main.o not recompiled after content change"; exit 1; } -out="$("$MCPP" run 2>&1 | tail -1)" -[[ "$out" == "gen = 42" ]] || { echo "stale binary after content change: $out"; exit 1; } - -echo "OK" diff --git a/tests/e2e/188_build_actions.sh b/tests/e2e/188_build_actions.sh deleted file mode 100755 index 54e0393bb..000000000 --- a/tests/e2e/188_build_actions.sh +++ /dev/null @@ -1,494 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 188_build_actions.sh — `mcpp:action=`: a build program DECLARES work instead -# of doing it, and the work becomes an edge in the build graph. -# -# The distinction this tests is the architectural one: a build program is a -# good place to decide what a build looks like (CONFIGURATION) and a bad place -# to perform it (WORK). Work done inside the program is serial, whole-set and -# reported as "build.mcpp exited 1". Declared as a node it is incremental, -# parallel and attributable to the edge that failed. -# -# All four wirings of the one primitive: -# source — outputs join the compile set, and are REGENERATED when an input -# changes (the property the eager path can never have) -# check — outputs are a stamp; a failing check fails the build -# object — outputs join the LINK set (mcpp#365). Without it the link inputs -# were the one attachment point a build graph obviously has and -# this table could not express, so people routed around the graph -# through `[build].ldflags` and lost incrementality. -# artifact — inputs are link outputs, so ninja orders it after the link with -# no phase machinery at all -# -# Also: a malformed action is refused rather than silently skipped. -# -# See .agents/docs/2026-08-30-build-mcpp-extensibility-architecture.md §3.1. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -mkdir -p app/src app/data -cd app - -cat > mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" -EOF - -cat > src/main.cpp <<'EOF' -#include -int generated_value(); -int main() { std::printf("VALUE=%d\n", generated_value()); } -EOF - -echo "11" > data/value.txt - -# The "generator": reads data/value.txt, writes a .cpp returning that number. -cat > gen.sh <<'EOF' -#!/usr/bin/env bash -printf 'int generated_value() { return %s; }\n' "$(cat "$1")" > "$2" -EOF -chmod +x gen.sh - -# A check that passes or fails depending on a marker file. -cat > check.sh <<'EOF' -#!/usr/bin/env bash -# The marker path is passed in: ninja runs commands with cwd = the BUILD dir, -# not the project root, so a bare relative name would never be found. -[ -f "$2" ] && { echo "check: refusing"; exit 1; } -: > "$1" -EOF -chmod +x check.sh - -cat > pack.sh <<'EOF' -#!/usr/bin/env bash -# $1 = the linked binary, $2 = the artifact to produce -cp "$1" "$2" -EOF -chmod +x pack.sh - -cat > build.mcpp <<'EOF' -#include -#include -import mcpp; -int main() { - const std::string root = mcpp::manifest_dir(); - const std::string out = std::string(mcpp::out_dir()) + "/gen.cpp"; - - mcpp::action a; - a.id = "generate"; - a.role = "source"; - a.arg((root + "/gen.sh").c_str()).arg((root + "/data/value.txt").c_str()).arg(out.c_str()) - .input((root + "/data/value.txt").c_str()) - .output(out.c_str()) - .submit(); - - mcpp::action c; - c.id = "lint"; - c.role = "check"; - c.arg((root + "/check.sh").c_str()).arg("${mcpp.out_dir}/lint.stamp") - .arg((root + "/FAIL_THE_CHECK").c_str()) - .output("${mcpp.out_dir}/lint.stamp") - .submit(); - - mcpp::action p; - p.id = "package"; - p.role = "artifact"; - p.arg((root + "/pack.sh").c_str()).arg("${mcpp.target_file:app}").arg("${mcpp.out_dir}/app.pack") - .input("${mcpp.target_file:app}") - .output("${mcpp.out_dir}/app.pack") - .submit(); -} -EOF - -# ── 1. all three roles in one build ───────────────────────────────────────── -"$MCPP" build > b1.log 2>&1 || { cat b1.log; echo "FAIL: build with actions failed"; exit 1; } -out="$("$MCPP" run 2>&1 | grep '^VALUE=' | tail -1)" -[[ "$out" == "VALUE=11" ]] || { echo "FAIL: generated source not linked: $out"; exit 1; } - -OUTDIR=$(find target -name 'app.pack' -printf '%h\n' 2>/dev/null | head -1) -[ -n "$OUTDIR" ] || { cat b1.log; echo "FAIL: artifact action produced nothing"; exit 1; } -[ -f "$OUTDIR/lint.stamp" ] || { echo "FAIL: check action produced no stamp"; exit 1; } - -# ── 2. incrementality — the property a pre-pass cannot have ──────────────── -# Changing the generator's INPUT must regenerate, without build.mcpp re-running. -echo "23" > data/value.txt -"$MCPP" build > b2.log 2>&1 || { cat b2.log; echo "FAIL: rebuild failed"; exit 1; } -out="$("$MCPP" run 2>&1 | grep '^VALUE=' | tail -1)" -[[ "$out" == "VALUE=23" ]] || { - cat b2.log; echo "FAIL: action did not re-run when its input changed: $out"; exit 1; } - -# ...and an unrelated rebuild must NOT re-run it (that is the whole point). -# -# THIS ASSERTION USED TO BE VACUOUS, AND THAT IS WORTH THE COMMENT. It read -# `grep -q "GENERATE" b3.log && FAIL`, on the assumption that ninja's -# description line for the edge reaches this log. It does not: `mcpp build` -# passes `--quiet` to ninja whenever it is not `--verbose`, and surfaces -# ninja's captured stdout only on failure. A from-scratch build, which -# unquestionably runs the action, prints no such line either -- so the grep -# never matched and the check could not fail whether the action reran or not. -# -# The generated file's modification time is a direct measurement of the thing -# being asserted: the action rewrites it whenever it runs. -before=$(stat -c %Y "$(find target -name gen.cpp -print -quit)" 2>/dev/null \ - || stat -f %m "$(find target -name gen.cpp -print -quit)") -touch src/main.cpp -sleep 1 # coarser than any filesystem's mtime granularity here -"$MCPP" build > b3.log 2>&1 || { cat b3.log; echo "FAIL: rebuild failed"; exit 1; } -after=$(stat -c %Y "$(find target -name gen.cpp -print -quit)" 2>/dev/null \ - || stat -f %m "$(find target -name gen.cpp -print -quit)") -[[ -n "$before" && "$before" == "$after" ]] || { - cat b3.log - echo "FAIL: the action re-ran although its inputs were unchanged" - echo " (gen.cpp mtime $before -> $after)" - exit 1; } - -# ── 2b. changing the action's COMMAND also takes effect ──────────────────── -# Distinct from 2: there the action's declared INPUT changed and ninja noticed. -# Here nothing ninja tracks changed — only build.mcpp, which re-runs (its own -# source is part of its cache key), re-declares the action with a different -# command, and ninja re-runs the edge because the rule's command changed. Both -# halves have to work or an edited generator invocation is silently ignored. -sed -i 's|"/data/value.txt"|"/data/other.txt"|g' build.mcpp -echo "31" > data/other.txt -"$MCPP" build > b2b.log 2>&1 || { cat b2b.log; echo "FAIL: rebuild after editing build.mcpp failed"; exit 1; } -out="$("$MCPP" run 2>&1 | grep '^VALUE=' | tail -1)" -[[ "$out" == "VALUE=31" ]] || { - cat b2b.log; echo "FAIL: an edited action command did not take effect: $out"; exit 1; } - -# ── 3. a failing check fails the build ───────────────────────────────────── -touch FAIL_THE_CHECK -rm -f "$OUTDIR/lint.stamp" -if "$MCPP" build > b4.log 2>&1; then - cat b4.log; echo "FAIL: a failing check did not fail the build"; exit 1 -fi -rm -f FAIL_THE_CHECK - -# ── 2c. a companion output that is NOT a translation unit ────────────────── -# The single most natural generator shape: protoc emits foo.pb.cc AND foo.pb.h. -# Adopting every declared output into the compile set gave both the same object -# path and tripped "object path collision after uniqueness pass". The header -# must still be PRODUCED by the edge (things include it) but never compiled. -cat > genpair.sh <<'EOF' -#!/usr/bin/env bash -# $1 = .cc to write, $2 = .h to write -printf '#include "%s"\nint paired() { return 13; }\n' "$(basename "$2")" > "$1" -printf 'int paired();\n' > "$2" -EOF -chmod +x genpair.sh -cat >> src/main.cpp <<'EOF' -EOF -cat > build.mcpp <<'EOF' -#include -#include -import mcpp; -int main() { - const std::string root = mcpp::manifest_dir(); - const std::string out = mcpp::out_dir(); - mcpp::action a; - a.id = "pair"; a.role = "source"; - a.arg((root + "/genpair.sh").c_str()) - .arg((out + "/p.cc").c_str()) - .arg((out + "/p.h").c_str()) - .output((out + "/p.cc").c_str()) - .output((out + "/p.h").c_str()) // companion: produced, NOT compiled - .submit(); - mcpp::include_dir(out.c_str()); - // Keep the earlier generated source in the build so main.cpp still links. - mcpp::action g; - g.id = "generate"; g.role = "source"; - g.arg((root + "/gen.sh").c_str()).arg((root + "/data/other.txt").c_str()) - .arg((out + "/gen.cpp").c_str()) - .input((root + "/data/other.txt").c_str()) - .output((out + "/gen.cpp").c_str()) - .submit(); -} -EOF -rm -rf target -"$MCPP" build > b2c.log 2>&1 || { - cat b2c.log; echo "FAIL: a companion (non-source) action output broke the build"; exit 1; } -grep -q "object path collision" b2c.log && { - cat b2c.log; echo "FAIL: the header was adopted as a translation unit"; exit 1; } - -# ── 3b/3c: their own minimal project ─────────────────────────────────────── -# Separate from `app`, whose main.cpp deliberately depends on the generated -# symbol — swapping its build.mcpp out would fail the LINK and say nothing -# about what these two are actually testing. -mkdir -p "$TMP/edge/src" -cd "$TMP/edge" -cat > mcpp.toml <<'EOF' -[package] -name = "edge" -version = "0.1.0" -EOF -printf 'int main() {}\n' > src/main.cpp - -# ── 3b. a literal `$` in a command survives to the tool ──────────────────── -# Shell-quoting alone does not save it: ninja expands `$foo` BEFORE the shell -# runs, so a token carrying a `$` (a path containing one, `-Wl,-rpath,$ORIGIN`, -# an awk program) needs ninja escaping too. -cat > dollar.sh <<'EOF' -#!/usr/bin/env bash -# $1 must arrive containing a literal dollar sign -case "$1" in *'$'*) : > "$2";; *) echo "lost the dollar: [$1]" >&2; exit 1;; esac -EOF -chmod +x dollar.sh -cat > build.mcpp <<'EOF' -#include -#include -import mcpp; -int main() { - const std::string root = mcpp::manifest_dir(); - mcpp::action a; - a.id = "dollar"; a.role = "check"; - a.arg((root + "/dollar.sh").c_str()).arg("-Wl,-rpath,$ORIGIN") - .arg("${mcpp.out_dir}/dollar.stamp") - .output("${mcpp.out_dir}/dollar.stamp") - .submit(); -} -EOF -rm -rf target -"$MCPP" build > b3b.log 2>&1 || { - cat b3b.log; echo "FAIL: a literal \$ in an action command did not survive"; exit 1; } - -# ── 3c. an unknown target reference is an error, not an empty path ───────── -cat > build.mcpp <<'EOF' -#include -import mcpp; -int main() { - mcpp::action a; - a.id = "bad-ref"; a.role = "artifact"; - a.arg("/bin/true").arg("${mcpp.target_file:no_such_target}") - .input("${mcpp.target_file:no_such_target}") - .output("${mcpp.out_dir}/x.out") - .submit(); -} -EOF -rm -rf target -if "$MCPP" build > b3c.log 2>&1; then - cat b3c.log; echo "FAIL: an unknown target reference was accepted"; exit 1 -fi -grep -q "no_such_target" b3c.log || { - cat b3c.log; echo "FAIL: error does not name the unknown target"; exit 1; } - -# ── 3d. role = "object": outputs join the LINK set ───────────────────────── -# -# The fourth wiring (mcpp#365). Source attaches to the compile inputs, Artifact -# to the link outputs, Check to nothing — which left the link INPUTS, an -# obvious attachment point, inexpressible. The consequence was concrete: a -# pre-built object could only reach the linker by being named in -# `[build].ldflags`, where it is a flat string in the command rather than a file -# in the graph, so editing it produced "ninja: no work to do". -# -# The object is produced by THE COMPILER MCPP ITSELF RESOLVED, read out of the -# generated build.ninja. Reaching for the host's `cc` looks simpler and is not: -# on a machine with xlings shims installed, a bare `cc` can resolve to a -# dispatcher pointing at some other sandbox, and the object would either fail to -# build or be built by a compiler whose ABI has nothing to do with the link. -mkdir -p "$TMP/objrole/src" -cd "$TMP/objrole" -cat > mcpp.toml <<'EOF' -[package] -name = "objrole" -version = "0.1.0" -EOF -printf 'int main() { return 0; }\n' > src/main.cpp -"$MCPP" build > o0.log 2>&1 || { cat o0.log; echo "FAIL: probe build failed"; exit 1; } -OBJ_NINJA=$(find target -name build.ninja -not -path '*/.build-mcpp/*' | head -1) -# A command line, written into mkobj.sh as one: it may start with -# `env LD_LIBRARY_PATH=...` for a compiler without its own search path. -OBJ_CXX=$(sed -n 's/^cxx *= *//p' "$OBJ_NINJA" | head -1) -[ -n "$OBJ_CXX" ] || { cat "$OBJ_NINJA"; echo "FAIL: could not read the compiler out of build.ninja"; exit 1; } - -cat > src/main.cpp <<'EOF' -#include -extern "C" int blob_value(); -int main() { std::printf("BLOB=%d\n", blob_value()); return blob_value() == 7 ? 0 : 1; } -EOF -# extern "C" keeps the symbol unmangled without needing a C driver. -printf 'extern "C" int blob_value() { return 7; }\n' > blob.cpp -cat > mkobj.sh < build.mcpp <<'EOF' -#include -#include -import mcpp; -int main() { - const std::string root = mcpp::manifest_dir(); - const std::string out = mcpp::out_dir(); - mcpp::action o; - o.id = "blob"; o.role = "object"; - o.arg((root + "/mkobj.sh").c_str()) - .arg((root + "/blob.cpp").c_str()) - .arg((out + "/blob.o").c_str()) - .input((root + "/blob.cpp").c_str()) - .output((out + "/blob.o").c_str()) - .submit(); -} -EOF -"$MCPP" build > o1.log 2>&1 || { cat o1.log; echo "FAIL: role=object build failed"; exit 1; } -"$MCPP" run > o2.log 2>&1 || { cat o2.log; echo "FAIL: the object was not linked in"; exit 1; } -grep -q 'BLOB=7' o2.log || { cat o2.log; echo "FAIL: wrong value from the linked object"; exit 1; } - -# Tracked, unlike the ldflags workaround it replaces. -sleep 1 -printf 'extern "C" int blob_value() { return 9; }\n' > blob.cpp -"$MCPP" run > o3.log 2>&1 && { cat o3.log; echo "FAIL: expected the changed object to be relinked (main asserts ==7)"; exit 1; } -grep -q 'BLOB=9' o3.log || { cat o3.log; echo "FAIL: editing the object's input did not reach the link"; exit 1; } -printf 'extern "C" int blob_value() { return 7; }\n' > blob.cpp - -# An unknown target name is an error, not an edge that quietly attaches to -# nothing. (Artifact infers its target from ${mcpp.target_file:}; an object runs -# before the link and has to say the name.) -cat > build.mcpp <<'EOF' -#include -#include -import mcpp; -int main() { - const std::string root = mcpp::manifest_dir(); - const std::string out = mcpp::out_dir(); - mcpp::action o; - o.id = "blob"; o.role = "object"; - o.arg((root + "/mkobj.sh").c_str()) - .arg((root + "/blob.cpp").c_str()) - .arg((out + "/blob.o").c_str()) - .input((root + "/blob.cpp").c_str()) - .output((out + "/blob.o").c_str()) - .target("no_such_target") - .submit(); -} -EOF -rm -rf target -if "$MCPP" build > o4.log 2>&1; then - cat o4.log; echo "FAIL: role=object accepted an unknown target"; exit 1 -fi -grep -q "no_such_target" o4.log || { - cat o4.log; echo "FAIL: error does not name the unknown target"; exit 1; } - -# ...and EVERY name is checked, not just the case where none of them matched. -# A typo alongside a name that does exist is the shape a typo actually takes, -# and the check used to be gated on "nothing attached" — so `objrole` absorbed -# it and the misspelling was dropped without a word. -sed -i.bak 's/.target("no_such_target")/.target("objrole").target("no_such_target")/' build.mcpp -rm -f build.mcpp.bak -rm -rf target -if "$MCPP" build > o4b.log 2>&1; then - cat o4b.log - echo "FAIL: an unknown target was silently dropped because a sibling matched"; exit 1 -fi -grep -q "no_such_target" o4b.log || { - cat o4b.log; echo "FAIL: error does not name the unknown target"; exit 1; } - -# ── 3e. the default target set includes TEST binaries ────────────────────── -# -# `mcpp build` linking and `mcpp test` failing on the very symbol the action -# exists to provide is the worst shape this can take: the object is what the -# library under test needs, and test link units are DISCOVERED from tests/*.cpp -# so their names cannot be spelled in build.mcpp without breaking plain -# `mcpp build`, where those units do not exist. -mkdir -p "$TMP/objtest/src" "$TMP/objtest/tests" -cd "$TMP/objtest" -cat > mcpp.toml <<'EOF' -[package] -name = "objtest" -version = "0.1.0" -EOF -printf 'export module objtest.engine;\nextern "C" int blob_value();\nexport int get(){ return blob_value(); }\n' > src/engine.cppm -printf 'import objtest.engine;\nint main(){ return get()==7?0:1; }\n' > src/main.cpp -printf 'import objtest.engine;\nint main(){ return get()==7?0:1; }\n' > tests/t_engine.cpp -printf 'extern "C" int blob_value() { return 7; }\n' > blob.cpp -cp "$TMP/objrole/mkobj.sh" mkobj.sh -cat > build.mcpp <<'EOF' -#include -#include -import mcpp; -int main() { - const std::string root = mcpp::manifest_dir(); - const std::string out = mcpp::out_dir(); - mcpp::action o; - o.id = "blob"; o.role = "object"; - o.arg((root + "/mkobj.sh").c_str()) - .arg((root + "/blob.cpp").c_str()) - .arg((out + "/blob.o").c_str()) - .input((root + "/blob.cpp").c_str()) - .output((out + "/blob.o").c_str()) - .submit(); // no .target(): every image, tests included -} -EOF -"$MCPP" build > o5.log 2>&1 || { cat o5.log; echo "FAIL: objtest build failed"; exit 1; } -"$MCPP" test > o6.log 2>&1 || { - cat o6.log - echo "FAIL: the test binary did not receive the object (undefined symbol)"; exit 1; } -grep -q '1 passed' o6.log || { cat o6.log; echo "FAIL: the test did not run"; exit 1; } - -# ── 3f. an object with no consumer at all is reported, not silent ────────── -# -# The edge is deliberately kept out of `actionDefaults` (an object's outputs are -# supposed to be reachable through a link edge), so "no consumer" means the -# command never runs. Saying nothing there is the same failure `[resources]` -# reports as resources/no-image. -# -# THE ONLY TARGET IS GATED OFF, and that is what makes this case exist at -# all. A `kind = "lib"` target used to serve here, because a static library did -# not take an object action's outputs; it does now (design C-6, e2e 608), so a -# project with one is no longer a project with no consumer. A target behind an -# inactive `required_features` is absent from the build entirely, which is the -# one shape that leaves an object action with nowhere to go. -mkdir -p "$TMP/objnone/src" -cd "$TMP/objnone" -cat > mcpp.toml <<'EOF' -[package] -name = "objnone" -version = "0.1.0" - -[features] -gated = {} - -[targets.objnone] -kind = "lib" -required_features = ["gated"] -EOF -printf 'export module objnone;\nexport int f(){return 1;}\n' > src/objnone.cppm -cp "$TMP/objtest/blob.cpp" blob.cpp -cp "$TMP/objtest/mkobj.sh" mkobj.sh -cp "$TMP/objtest/build.mcpp" build.mcpp -"$MCPP" build > o7.log 2>&1 || { cat o7.log; echo "FAIL: objnone build failed"; exit 1; } -grep -q 'produces no target to put its outputs into' o7.log || { - cat o7.log; echo "FAIL: an object with no consumer must be reported"; exit 1; } - -# THE CONTROL: with the feature on, the target exists and the action is not -# reported. Without it the assertion above would pass on a build that had -# stopped emitting the diagnostic for every project. -"$MCPP" build --features gated > o7b.log 2>&1 || { - cat o7b.log; echo "FAIL: objnone build with the feature failed"; exit 1; } -grep -q 'produces no target to put its outputs into' o7b.log && { - cat o7b.log; echo "FAIL: the object was reported as having no consumer while its target existed" - exit 1; } - -cd "$TMP/edge" - -# ── 4. a malformed action is refused, not skipped ────────────────────────── -cat > build.mcpp <<'EOF' -#include -int main() { - std::printf("mcpp:protocol=1\n"); - // No `command`, no `outputs` — mcpp fixes the source set during prepare, - // so an output whose NAME is unknown cannot be built. - std::printf("mcpp:action={\"id\":\"broken\",\"role\":\"source\"}\n"); -} -EOF -rm -rf target -if "$MCPP" build > b5.log 2>&1; then - cat b5.log; echo "FAIL: a malformed action was accepted"; exit 1 -fi -grep -q "malformed action" b5.log || { - cat b5.log; echo "FAIL: no diagnostic for the malformed action"; exit 1; } - -echo "OK" diff --git a/tests/e2e/313_check_stamp_on_every_platform.sh b/tests/e2e/313_check_stamp_on_every_platform.sh deleted file mode 100755 index 654e06f1b..000000000 --- a/tests/e2e/313_check_stamp_on_every_platform.sh +++ /dev/null @@ -1,92 +0,0 @@ -#!/usr/bin/env bash -# 313_check_stamp_on_every_platform.sh — a `role = "check"` whose command never -# touches its stamp, on EVERY platform. -# -# WHY THIS EXISTS SEPARATELY FROM 188 AND 312. -# -# The engine writes a check's stamp so that an analyser -- which answers with an -# exit code and writes nothing -- can be a check without a wrapper script. The -# whole justification for doing it in the engine rather than in a script is that -# an action's command is an argv with no shell assumed, so the wrapper cannot be -# written portably: there is no `touch` on Windows. -# -# And every existing test of that mechanism declares `# requires: gcc`, so all -# of them SKIP on Windows. The feature's reason to exist was verified nowhere -# near the platform it exists for. This file carries no `# requires:` line at -# all, so it runs wherever the suite runs. -# -# To do that it must avoid a POSIX shell entirely, which the new mechanism makes -# possible: the check's command is `mcpp --version` -- an executable that is -# present by construction, exits 0, and writes no stamp. -set -e - -source "$(dirname "$0")/_host_path.sh" - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -mkdir -p src -cat > mcpp.toml <<'EOF' -[package] -name = "stampprobe" -version = "0.1.0" -EOF -cat > src/main.cpp <<'EOF' -#include -#ifndef CHECK_DECLARED -#error "the build program never ran" -#endif -int main() { std::printf("STAMP_PROBE_OK\n"); } -EOF - -MCPP_HOST="$(host_path "$MCPP")" -cat > build.mcpp < b1.log 2>&1 || { cat b1.log; echo "FAIL: build failed"; exit 1; } -out="$("$MCPP" run 2>&1 | tail -1)" -[[ "$out" == *STAMP_PROBE_OK* ]] || { - echo "FAIL: the program did not run: '$out'"; exit 1; } - -# THE criterion. Not the build's exit code: measured, ninja does not fail when a -# declared output goes unproduced -- it leaves the file absent and re-runs that -# edge on every build afterwards, so a build with no stamp mechanism at all -# stays green and merely redoes work. -# NOT `mapfile`. macOS ships bash 3.2, which does not have it, and this file -# runs there — `47_cdb_prebuilt_module_path_abs.sh` already says so in its own -# comment, and every other user of `mapfile` in this suite declares -# `# requires: gcc`, which macOS never satisfies. So the constraint was written -# down and the tests that could violate it could not reach macOS to find out. -# This one did, on its first CI run, with `mapfile: command not found`. -stamp_count=$(find target -name 'probe.stamp' | wc -l | tr -d '[:space:]') -if [ "$stamp_count" != "1" ]; then - find target -name 'probe.stamp' | sed 's/^/ /' - echo "FAIL: expected exactly one stamp written by the engine, found $stamp_count" - exit 1 -fi - -# And the other half: a satisfied edge is not re-run. Without the stamp this -# would repeat forever, which is the symptom the exit code hides. -"$MCPP" build > b2.log 2>&1 || { cat b2.log; echo "FAIL: no-op rebuild failed"; exit 1; } -if grep -q 'CHECK' b2.log; then - cat b2.log - echo "FAIL: the check re-ran with nothing changed — its stamp is not satisfying the edge" - exit 1 -fi - -echo "OK" diff --git a/tests/e2e/314_dependency_action_generated_header.sh b/tests/e2e/314_dependency_action_generated_header.sh deleted file mode 100755 index 30a84eec5..000000000 --- a/tests/e2e/314_dependency_action_generated_header.sh +++ /dev/null @@ -1,158 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 314_dependency_action_generated_header.sh — a package that generates its own -# header compiles after the generator, not alongside it (mcpp#534). -# -# THE SHAPE THAT WAS BROKEN. `role = "source"` was documented as "outputs join -# the compile set; the compile edge consumes them", and that is true of a -# generated `.cpp` and false of a generated `.h`. A header is never an edge -# input — it is reached through `-I`, and the depfile that would record it does -# not exist until a compile has already succeeded. So the action's node sat in -# build.ninja with nothing able to reach it: not in `default` (Source outputs -# are deliberately excluded), not in the goal phony (objects and link outputs -# only), and consumed by no edge. -# -# THE ASSERTION IS ON CONTENT, NOT EXISTENCE. `prepare_actions` used to -# write a zero-byte placeholder for every Source output including headers, so -# the file was on disk whether or not the generator ran — which is exactly why -# mcpp#534 was filed as an intermittent race when it was a deterministic -# never-runs. A test asserting `[[ -f gen.h ]]` would have passed against the -# defect. -# -# The dependency is a PATH dependency because that is the reported shape: the -# generator, the header and the consumer of the header are all inside one -# dependency package, which is the case `tests/examples/protobuf-protoc` does -# not cover (there the generated header is used by the ROOT project). -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT - -# ── the dependency: generates a header, and compiles a source that uses it ── -mkdir -p "$TMP/proto/src" -cat > "$TMP/proto/mcpp.toml" <<'EOF' -[package] -name = "proto" -version = "0.1.0" - -[targets.proto] -kind = "lib" -EOF - -cat > "$TMP/proto/gen.sh" <<'EOF' -#!/usr/bin/env bash -# Slow on purpose: if the ordering edge is missing, the compile wins the race -# every time rather than occasionally, so a red run means the defect and not -# the weather. -sleep 2 -printf '#define PROTO_ANSWER 42\n' > "$1" -EOF -chmod +x "$TMP/proto/gen.sh" - -# The lib root is a module interface, so this package really is a module -# library — keeping the lib-root convention in play rather than side-stepping -# it. It includes the generated header from its own implementation. -cat > "$TMP/proto/src/proto.cppm" <<'EOF' -module; -#include "proto_generated.h" -export module proto; -export int proto_answer() { return PROTO_ANSWER; } -EOF - -cat > "$TMP/proto/build.mcpp" <<'EOF' -#include -import mcpp; -int main() { - const std::string root = mcpp::manifest_dir(); - const std::string out = mcpp::out_dir(); - mcpp::action a; - a.id = "proto:header"; - a.role = "source"; - a.arg((root + "/gen.sh").c_str()) - .arg((out + "/proto_generated.h").c_str()) - .output((out + "/proto_generated.h").c_str()) // HEADER, and the only output - .submit(); - mcpp::include_dir(out.c_str()); -} -EOF - -# ── the consumer ──────────────────────────────────────────────────────────── -mkdir -p "$TMP/app/src" -cat > "$TMP/app/mcpp.toml" <<'EOF' -[package] -name = "app" -version = "0.1.0" - -[dependencies] -proto = { path = "../proto" } -EOF -cat > "$TMP/app/src/main.cpp" <<'EOF' -#include -import proto; -int main() { std::printf("ANSWER=%d\n", proto_answer()); } -EOF - -cd "$TMP/app" -"$MCPP" build > b.log 2>&1 || { - cat b.log - echo "FAIL: a dependency whose action generates its only header did not build" - exit 1; } - -out="$("$MCPP" run 2>&1 | tail -1)" -[[ "$out" == "ANSWER=42" ]] || { - echo "FAIL: expected ANSWER=42 from the generated header, got '$out'"; exit 1; } - -# The generator actually ran. Size, not existence — see the header comment. -hdr=$(find "$TMP" -name proto_generated.h | head -1) -[[ -n "$hdr" ]] || { echo "FAIL: no proto_generated.h anywhere"; exit 1; } -bytes=$(wc -c < "$hdr") -[[ "$bytes" -gt 0 ]] || { - echo "FAIL: $hdr is $bytes bytes — the placeholder, not the generator's output" - exit 1; } -grep -q 'PROTO_ANSWER' "$hdr" || { - echo "FAIL: $hdr exists and is non-empty but is not what gen.sh writes:" - cat "$hdr"; exit 1; } - -# The ordering is declared, not merely observed. A green run on a fast machine -# proves nothing on its own; this is the edge that makes it a property. -nj=$(find "$TMP/app/target" -name build.ninja | head -1) -[[ -n "$nj" ]] || { echo "FAIL: no build.ninja"; exit 1; } - -# The phony is named for the QUALIFIED package (`mcpp-actions-mcpplibs.proto`), -# because that is how `CompileUnit::packageName` spells it and the two are -# matched against each other. Read the name out rather than hardcoding it: a -# test that spelled it by hand would be asserting the naming policy, which is -# not what this test is about. -phony=$(grep -oE '^build (mcpp-actions-[^ ]*proto[^ ]*) : phony' "$nj" \ - | awk '{print $2}' | head -1) -[[ -n "$phony" ]] || { - echo "FAIL: no action phony was emitted for the dependency" - grep -nE '^build mcpp-actions' "$nj" || echo " (none at all)" - exit 1; } -count=$(grep -cE "^build $phony : phony" "$nj" || true) -[[ "$count" -eq 1 ]] || { - echo "FAIL: expected exactly one '$phony' edge, found $count"; exit 1; } - -# DENOMINATOR. "every edge that should carry it does" is vacuously true when no -# edge does, and that vacuum is precisely the pre-fix state — so the count of -# the dependency's compile edges is asserted separately from the count that -# carries the ordering. -# Matched on the RULE and the SOURCE, not on the object path. An object edge -# can carry implicit outputs before the colon (`build a.o | gcm.cache/x.gcm :`), -# so a pattern anchored on the output is a pattern that quietly matches nothing -# — which, for a denominator, is the one failure mode that matters. -rules='cxx_object|c_object|cxx_module|cxx_module_obj|cxx_module_bmi|cxx_scan|asm_object' -src='proto/src/proto\.cppm' -edges=$(grep -cE "^build .*: *($rules) .*$src" "$nj" || true) -ordered=$(grep -cE "^build .*: *($rules) .*$src.*\|\| *$phony" "$nj" || true) -[[ "$edges" -gt 0 ]] || { - echo "FAIL: no compile edge for the dependency was found at all — the" - echo " assertion below would have passed by describing nothing" - grep -nE '^build ' "$nj" | head -20 - exit 1; } -[[ "$ordered" -eq "$edges" ]] || { - echo "FAIL: $ordered of $edges dependency compile edges wait for the action" - grep -nE "^build .*: *($rules) .*$src" "$nj" - exit 1; } - -echo "PASS: 314 (generator ran before the compile; $ordered/$edges edges ordered)" diff --git a/tests/e2e/315_blocking_check_gates_compilation.sh b/tests/e2e/315_blocking_check_gates_compilation.sh deleted file mode 100755 index 722b54ce6..000000000 --- a/tests/e2e/315_blocking_check_gates_compilation.sh +++ /dev/null @@ -1,105 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 315_blocking_check_gates_compilation.sh — `blocking = true` on a check does -# what it has always been documented to do (mcpp#534). -# -# WHAT THIS DEFENDS. `blocking` was typed (`BuildAction::blocking`), emitted -# over the build-program protocol (`hostprogram.cppm`), parsed -# (`directives.cppm`), documented in both languages (`docs/30-build-mcpp.md` -# and its Chinese counterpart) and demonstrated in a shipped example -# (`examples/08-build-rules/rules-tidy`) — and read by nothing. The only -# order-only edge the ninja backend emitted was the staged-BMI one. So a check -# marked blocking ran alongside compilation exactly like a non-blocking one, -# and the flag was a no-op with a paper trail. -# -# ASSERTED ON THE ARTIFACT, NOT ON A LOG LINE. "the check failed" appears in -# the output either way; what distinguishes blocking from non-blocking is -# whether the object was PRODUCED. A grep over stderr would pass against the -# defect. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -mkdir -p "$TMP/app/src" -cd "$TMP/app" - -cat > mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" -EOF -printf '#include \nint main(){ std::printf("ok\\n"); }\n' > src/main.cpp - -# A check that fails, slowly. The sleep is what makes the difference -# observable: without the ordering edge the compile finishes long before the -# check does, so the object exists on disk when the build gives up. -cat > check.sh <<'EOF' -#!/usr/bin/env bash -sleep 2 -echo "the check says no" >&2 -exit 1 -EOF -chmod +x check.sh - -emit_build_mcpp() { # $1 = "true" | "false" - cat > build.mcpp < -import mcpp; -int main() { - const std::string root = mcpp::manifest_dir(); - mcpp::action a; - a.id = "gate"; - a.role = "check"; - a.blocking = $1; - a.arg((root + "/check.sh").c_str()) - .output("\${mcpp.out_dir}/gate.stamp") - .submit(); -} -EOF -} - -# ── blocking = true: the object must NOT be produced ──────────────────────── -emit_build_mcpp true -rm -rf target -if "$MCPP" build > b_blocking.log 2>&1; then - cat b_blocking.log - echo "FAIL: a failing blocking check let the build succeed"; exit 1 -fi -if find target -name 'main*.o' | grep -q .; then - echo "FAIL: a failing BLOCKING check did not stop the compile — the object exists:" - find target -name 'main*.o' - echo " (this is the pre-fix behaviour: 'blocking' was parsed and never read)" - exit 1 -fi - -# The edge that makes it a property rather than a race lost by the compiler. -nj=$(find target -name build.ninja -not -path '*/.build-mcpp/*' | head -1) -[[ -n "$nj" ]] || { echo "FAIL: no build.ninja"; exit 1; } -grep -qE '^build mcpp-actions-app : phony.*gate\.stamp' "$nj" || { - echo "FAIL: the blocking check's stamp is not in the package's action phony" - grep -nE '^build mcpp-actions' "$nj" || echo " (no phony at all)" - exit 1; } -ordered=$(grep -cE '^build [^:]*main[^:]*\.o *:.*\|\|.*mcpp-actions-app' "$nj" || true) -[[ "$ordered" -ge 1 ]] || { - echo "FAIL: the compile edge does not wait for the blocking check"; exit 1; } - -# ── blocking = false: the CONTROL. Same check, same failure, compile runs ─── -# -# Without this half, a build that refused to compile for any unrelated reason -# would pass the assertion above. What must differ between the two runs is -# exactly one line of build.mcpp. -emit_build_mcpp false -rm -rf target -"$MCPP" build > b_parallel.log 2>&1 || true # still fails: the check still fails -if ! find target -name 'main*.o' | grep -q .; then - cat b_parallel.log - echo "FAIL: a NON-blocking check also stopped the compile — 'blocking' is" - echo " not distinguishing anything, it is just always on" - exit 1 -fi -nj2=$(find target -name build.ninja -not -path '*/.build-mcpp/*' | head -1) -if grep -qE '^build mcpp-actions-app : phony.*gate\.stamp' "$nj2"; then - echo "FAIL: a NON-blocking check joined the ordering phony"; exit 1 -fi - -echo "PASS: 315 (blocking gates the compile; non-blocking does not)" diff --git a/tests/e2e/317_project_build_hooks.sh b/tests/e2e/317_project_build_hooks.sh deleted file mode 100644 index c32972f23..000000000 --- a/tests/e2e/317_project_build_hooks.sh +++ /dev/null @@ -1,522 +0,0 @@ -#!/usr/bin/env bash -# requires: -# Project-level build hooks (#496): lifecycle order, policy, timeout, the -# directory a hook runs in, and the two places nothing may fire. -set -e -source "$(dirname "$0")/_host_path.sh" - -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -mkdir -p "$TMP/app/src" "$TMP/app/nested" - -cat > "$TMP/app/src/main.cpp" <<'EOF' -int main() { return 0; } -EOF - -write_manifest() { - cat > "$TMP/app/mcpp.toml" -} - -# cmd.exe writes CRLF and /bin/sh writes LF, so the line ENDINGS are the host's -# and the line CONTENT is the hook's. Compare the content. -# expect_log ... -expect_log() { - local file=$1; shift - local want got - want=$(printf '%s\n' "$@") - got=$(tr -d '\r' < "$file" 2>/dev/null || true) - if [[ "$got" != "$want" ]]; then - echo "FAIL: $file" - echo "--- expected ---"; printf '%s\n' "$want" - echo "--- actual ---"; printf '%s\n' "$got" - exit 1 - fi -} - -# A WHOLE line, matched as one: a substring would still pass if the message -# were reworded around it. No pipe into grep — a `grep -q` that exits on the -# first match SIGPIPEs its producer, which reads as failure the moment anyone -# adds `set -o pipefail` to this file. -expect_line() { # expect_line - local file=$1 line=$2 - local body - body=$(tr -d '\r' < "$file" 2>/dev/null || true) - if [[ $'\n'"$body"$'\n' != *$'\n'"$line"$'\n'* ]]; then - cat "$file" - echo "FAIL: '$line' is not a line of $file" - exit 1 - fi -} - -# ── The success lifecycle, twice ───────────────────────────────────────── -# -# Twice on purpose: the second `mcpp build` is the one that would otherwise -# take the no-op fast path, which skips preparation and would skip the hooks -# with it. A single run cannot tell the two paths apart. -write_manifest <<'EOF' -[package] -name = "hookprobe" -version = "0.1.0" - -[hooks] -build_start = "echo start>>hooks.log" -build_failed = "echo failed>>hooks.log" -build_finished = "echo finished>>hooks.log" -timeout_seconds = 10 -enabled = true -side_effect = false -EOF - -# Invoked BELOW the project root: a hook's relative paths belong to the root, -# not to wherever the user happened to type `mcpp build`. -cd "$TMP/app/nested" -"$MCPP" build > success-1.log 2>&1 || { - cat success-1.log; echo "FAIL: hooked build failed"; exit 1; } -"$MCPP" build > success-2.log 2>&1 || { - cat success-2.log; echo "FAIL: second hooked build failed"; exit 1; } -cd "$TMP/app" -[[ ! -e nested/hooks.log ]] || { - echo "FAIL: hook ran in the invocation directory, not the project root"; exit 1; } -expect_log hooks.log start finished start finished - -# ── A compiler failure chooses build_failed, never build_finished ──────── -cat > src/main.cpp <<'EOF' -#error "intentional hook failure path" -int main() { return 0; } -EOF -: > hooks.log -rc=0 -"$MCPP" build > build-failed.log 2>&1 || rc=$? -[[ $rc -ne 0 ]] || { cat build-failed.log; echo "FAIL: broken source built"; exit 1; } -expect_log hooks.log start failed - -cat > src/main.cpp <<'EOF' -int main() { return 0; } -EOF - -# ── A failing hook is REPORTED and the build keeps its own result ─────── -# -# While `[hooks]` is experimental it may not decide whether a build succeeded. -# The build below succeeds with a hook that cannot run at all, and the two -# assertions are both needed: the exit code says the hook had no vote, and the -# warning says it was not silently swallowed. -write_manifest <<'EOF' -[package] -name = "hookprobe" -version = "0.1.0" - -[hooks] -build_finished = "mcpp-hook-command-that-does-not-exist" -EOF -"$MCPP" build > ignored-hook-failure.log 2>&1 || { - cat ignored-hook-failure.log - echo "FAIL: a failing hook changed the build result"; exit 1; } -grep -q "warning: hook 'build_finished' exited with status" ignored-hook-failure.log || { - cat ignored-hook-failure.log; echo "FAIL: the hook failure was not reported"; exit 1; } -# `if grep` rather than `grep && { }`: a NEGATIVE assertion written with `&&` -# leaves the list's exit status as grep's, and reading that under `set -e` is -# an argument this file should not be having. -if grep -q "error: hook 'build_finished'" ignored-hook-failure.log; then - cat ignored-hook-failure.log - echo "FAIL: an experimental hook failure was reported as an error"; exit 1 -fi - -# The explicit spelling of the current behaviour keeps working, so a manifest -# does not have to change when the feature is promoted. -write_manifest <<'EOF' -[package] -name = "hookprobe" -version = "0.1.0" - -[hooks] -build_finished = "mcpp-hook-command-that-does-not-exist" -side_effect = false -EOF -"$MCPP" build > explicit-false.log 2>&1 || { - cat explicit-false.log; echo "FAIL: side_effect=false was not accepted"; exit 1; } - -# ── `side_effect = true` is REFUSED, not quietly downgraded ───────────── -# -# Honouring it would give an experimental feature a veto over every build; -# ignoring it would leave the project believing its build is gated on a -# notifier when nothing is. Both silent options are worse than an error. -write_manifest <<'EOF' -[package] -name = "hookprobe" -version = "0.1.0" - -[hooks] -build_finished = "echo done" -side_effect = true -EOF -rc=0 -"$MCPP" build > side-effect-true.log 2>&1 || rc=$? -[[ $rc -ne 0 ]] || { - cat side-effect-true.log; echo "FAIL: side_effect=true was accepted"; exit 1; } -grep -q "experimental" side-effect-true.log || { - cat side-effect-true.log - echo "FAIL: the refusal does not say why"; exit 1; } - -# ── enabled = false switches off a table that still names commands ────── -write_manifest <<'EOF' -[package] -name = "hookprobe" -version = "0.1.0" - -[hooks] -build_start = "echo disabled>>disabled.log" -build_finished = "echo disabled>>disabled.log" -enabled = false -EOF -rm -f disabled.log -"$MCPP" build > disabled.log.out 2>&1 || { - cat disabled.log.out; echo "FAIL: disabled hook build failed"; exit 1; } -[[ ! -e disabled.log ]] || { - cat disabled.log; echo "FAIL: enabled=false still ran hooks"; exit 1; } - -# ── The timeout is a real bound, on both host shells ──────────────────── -case "$(uname -s)" in - MINGW*|MSYS*|CYGWIN*) HOST_WINDOWS=1 ;; - *) HOST_WINDOWS=0 ;; -esac -if [[ $HOST_WINDOWS -eq 1 ]]; then - slow_command="ping -n 6 127.0.0.1 >NUL" - pause_2s="ping -n 3 127.0.0.1 >NUL" - pause_5s="ping -n 6 127.0.0.1 >NUL" -else - slow_command="sleep 5" - pause_2s="sleep 2" - pause_5s="sleep 5" -fi -write_manifest < timeout.log 2>&1 || { - cat timeout.log; echo "FAIL: a timed-out hook changed the build result"; exit 1; } -# The bound is real even though the build survives it: without the timeout the -# command would have run for five seconds, and the line below would be absent. -expect_line timeout.log "warning: hook 'build_finished' timed out after 1s" - -# ── An invalid value fails the manifest, before any command runs ──────── -# -# And it fails as a MANIFEST error naming the key — `[hooks]` is a section of -# mcpp.toml, so a bad value there is reported by the same parser and in the -# same words as a bad value anywhere else in the file. -write_manifest <<'EOF' -[package] -name = "hookprobe" -version = "0.1.0" - -[hooks] -build_start = "echo should-not-run>>invalid.log" -timeout_seconds = 0 -EOF -rm -f invalid.log -rc=0 -"$MCPP" build > invalid-config.log 2>&1 || rc=$? -[[ $rc -ne 0 ]] || { cat invalid-config.log; echo "FAIL: invalid hooks were accepted"; exit 1; } -[[ ! -e invalid.log ]] || { cat invalid.log; echo "FAIL: invalid hook ran"; exit 1; } -grep -q "\[hooks\].timeout_seconds must be a positive integer" invalid-config.log || { - cat invalid-config.log; echo "FAIL: invalid hook diagnostic is missing"; exit 1; } - -# ── An unknown key is a warning, and the known ones still run ─────────── -: > hooks.log -write_manifest <<'EOF' -[package] -name = "hookprobe" -version = "0.1.0" - -[hooks] -build_start = "echo start>>hooks.log" -build_finsihed = "echo typo>>hooks.log" -EOF -"$MCPP" build > unknown-key.log 2>&1 || { - cat unknown-key.log; echo "FAIL: unknown hook key was fatal"; exit 1; } -grep -q "\[hooks\] has unsupported key 'build_finsihed'" unknown-key.log || { - cat unknown-key.log; echo "FAIL: unknown hook key was not reported"; exit 1; } -expect_log hooks.log start - -# ── Preparation failing fires NOTHING ─────────────────────────────────── -# -# The lifecycle is paired: build_finished/build_failed are only reachable after -# build_start has run. A project that cannot be prepared has not started -# building — and its hook program may be exactly what preparation would have -# installed, so "report the failure" is not available here. -: > hooks.log -write_manifest <<'EOF' -[package] -name = "hookprobe" -version = "0.1.0" - -[hooks] -build_start = "echo start>>hooks.log" -build_failed = "echo failed>>hooks.log" - -[dependencies] -absent = { path = "no/such/dependency" } -EOF -rc=0 -"$MCPP" build > prepare-failed.log 2>&1 || rc=$? -[[ $rc -ne 0 ]] || { - cat prepare-failed.log; echo "FAIL: a missing path dependency built"; exit 1; } -expect_log hooks.log - -# ── `during_build`: an interval closed by the build, not by the command ── -# -# The assertions below are about STATE, never about a log line. "mcpp said it -# stopped the command" passes whether or not anything stopped; "the file it was -# appending to has not grown in a second" does not. -if [[ $HOST_WINDOWS -eq 1 ]]; then - cat > writer.cmd <<'EOF' -@echo off -:loop -echo tick>>beat.log -ping -n 2 127.0.0.1 >NUL -goto loop -EOF - # `start /b` returns immediately, so the command mcpp starts EXITS and the - # writer is left behind it — the job object is the only thing that can - # still reach it. - writer_with_grandchild="start /b cmd /c writer.cmd" -else - cat > writer.sh <<'EOF' -while :; do echo tick>>beat.log; sleep 0.2; done -EOF - # The shell mcpp starts forks the writer and waits, so the writer is a - # GRANDCHILD. This is the case `kill(pid)` misses and `killpg` catches, and - # it is the only assertion that tells the fix from the bug. - writer_with_grandchild="sh writer.sh & wait" -fi - -beat_is_frozen() { # the file grew, and then stopped growing - local before after - before=$(wc -c < beat.log 2>/dev/null || echo 0) - sleep 1 - after=$(wc -c < beat.log 2>/dev/null || echo 0) - [[ "$before" != "0" && "$before" == "$after" ]] -} - -: > hooks.log -rm -f beat.log -write_manifest < during-build.log 2>&1 || { - cat during-build.log; echo "FAIL: build with during_build failed"; exit 1; } -beat_is_frozen || { - echo "FAIL: during_build kept running after the build (or never ran)" - wc -c beat.log 2>/dev/null; exit 1; } -# The interval closed BEFORE the terminal hook, which is the ordering that -# keeps a "build finished" sound from playing over the music it replaces. -expect_log hooks.log finished - -# ── `loop` restarts a command that ends before its interval does ──────── -rm -f runs.log -write_manifest < loop-on.log 2>&1 || { - cat loop-on.log; echo "FAIL: looped during_build failed the build"; exit 1; } -looped=$(wc -l < runs.log | tr -d ' ') -[[ "$looped" -ge 2 ]] || { - cat loop-on.log; echo "FAIL: loop=true ran $looped time(s), expected >= 2"; exit 1; } - -# Without `loop`, the same command runs exactly once — the denominator that -# makes the count above mean something. -rm -f runs.log -write_manifest < loop-off.log 2>&1 || { - cat loop-off.log; echo "FAIL: unlooped during_build failed the build"; exit 1; } -once=$(wc -l < runs.log | tr -d ' ') -[[ "$once" -eq 1 ]] || { - cat loop-off.log; echo "FAIL: loop=false ran $once time(s), expected 1"; exit 1; } - -# ── A command that cannot stay up stops, rather than spinning ─────────── -# -# `loop` on a typo is a fork bomb otherwise. The bound is five consecutive -# runs that end unsuccessfully within a second. -# -# The build must stay open long enough for the supervisor to reach that bound. -# Reaching it costs five spawns, four restart delays of 250ms, and up to 50ms -# of poll granularity after each exit: 1.25s before a single spawn is counted. -# Under a two-second pause that left 150ms per spawn, and a loaded macOS -# runner exceeded it (main, 2026-09-05: the build finished, the warning was -# never printed, and the test reported the bound as missing). Five seconds -# leaves 750ms per spawn; the property is then decided by the code and not by -# the runner's load. -write_manifest < loop-giveup.log 2>&1 || { - cat loop-giveup.log - echo "FAIL: a during_build that never ran changed the build result"; exit 1; } -grep -q "warning: hook 'during_build' failed to stay up" loop-giveup.log || { - cat loop-giveup.log; echo "FAIL: giving up was not reported"; exit 1; } - -# ── `loop` on a self-closing event is refused, not ignored ────────────── -write_manifest <<'EOF' -[package] -name = "hookprobe" -version = "0.1.0" - -[hooks] -build_start = { cmd = "echo start", loop = true } -EOF -rc=0 -"$MCPP" build > loop-misplaced.log 2>&1 || rc=$? -[[ $rc -ne 0 ]] || { - cat loop-misplaced.log; echo "FAIL: loop on build_start was accepted"; exit 1; } -grep -q "during_build" loop-misplaced.log || { - cat loop-misplaced.log - echo "FAIL: the diagnostic does not name the event that supports loop"; exit 1; } - -# ── An interrupted build leaves nothing running ───────────────────────── -# -# POSIX only, and the skip is printed rather than silent: sending Ctrl-C to -# another process on Windows needs a helper this suite does not have, and a -# test that cannot fail there would read as coverage. -if [[ $HOST_WINDOWS -eq 1 ]]; then - echo "SKIP(within test): Ctrl-C cleanup — no way to signal another process here" -else - rm -f beat.log - write_manifest < interrupted.log 2>&1 & - build_pid=$! - sleep 3 - kill -INT "$build_pid" 2>/dev/null || true - wait "$build_pid" 2>/dev/null || true - beat_is_frozen || { - echo "FAIL: Ctrl-C left during_build running"; exit 1; } -fi - -# ── A DEPENDENCY's hooks never run ────────────────────────────────────── -# -# Every manifest mcpp parses carries a `[hooks]` field, a dependency's -# included. Only the root project's is ever invoked, and that is the whole -# reason `mcpp add` of a third-party package is not "run their shell command -# on my next build". A comment cannot hold that; this can. -DEP="$TMP/dep" -DEP_HOST="$(host_path "$DEP")" -mkdir -p "$DEP/src" -cat > "$DEP/src/dep.cppm" <<'EOF' -export module dep; -export int dep_answer() { return 0; } -EOF -cat > "$DEP/mcpp.toml" <<'EOF' -[package] -name = "dep" -version = "0.1.0" - -[targets.dep] -kind = "lib" - -[hooks] -build_start = "echo dependency>>dependency-hook.log" -build_finished = "echo dependency>>dependency-hook.log" -EOF -: > hooks.log -rm -f "$DEP/dependency-hook.log" dependency-hook.log -write_manifest < src/main.cpp <<'EOF' -import dep; -int main() { return dep_answer(); } -EOF -"$MCPP" build > dependency-hooks.log 2>&1 || { - cat dependency-hooks.log; echo "FAIL: build with a hooked dependency failed"; exit 1; } -expect_log hooks.log start finished -[[ ! -e "$DEP/dependency-hook.log" && ! -e dependency-hook.log ]] || { - echo "FAIL: a dependency's hooks ran"; exit 1; } - -# ── A workspace runs each MEMBER's hooks, in that member's root ───────── -# -# Members are separate packages, so their hooks are separate too — and the -# workspace root's own `[hooks]` belongs to a node that builds nothing. -WS="$TMP/ws" -mkdir -p "$WS/alpha/src" "$WS/beta/src" -cat > "$WS/mcpp.toml" <<'EOF' -[workspace] -members = ["alpha", "beta"] - -[hooks] -build_start = "echo root>>root.log" -build_finished = "echo root>>root.log" -EOF -for member in alpha beta; do - cat > "$WS/$member/src/main.cpp" <<'EOF' -int main() { return 0; } -EOF - cat > "$WS/$member/mcpp.toml" < ws-build.log 2>&1 || { - cat ws-build.log; echo "FAIL: workspace build with hooks failed"; exit 1; } -expect_log alpha/hooks.log alpha-start alpha-finished -expect_log beta/hooks.log beta-start beta-finished -[[ ! -e root.log ]] || { - cat root.log; echo "FAIL: the workspace root's own hooks fired"; exit 1; } -[[ ! -e hooks.log ]] || { - cat hooks.log; echo "FAIL: a member hook ran in the workspace root"; exit 1; } - -echo "OK" diff --git a/tests/e2e/51_package_generated_files.sh b/tests/e2e/51_package_generated_files.sh deleted file mode 100755 index 385069942..000000000 --- a/tests/e2e/51_package_generated_files.sh +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc fresh-sandbox -# Form B package descriptors can materialize small package-owned support -# files before scanning and compiling the package. -set -e -source "$(dirname "$0")/_host_path.sh" - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT - -export MCPP_HOME="$TMP/mcpp-home" -source "$(dirname "$0")/_inherit_toolchain.sh" - -INDEX_DIR="$TMP/local-index" -INDEX_DIR_HOST="$(host_path "$INDEX_DIR")" -mkdir -p "$INDEX_DIR/pkgs/t" -cat > "$INDEX_DIR/pkgs/t/tinycfg.lua" <<'EOF' -package = { - spec = "1", - name = "tinycfg", - description = "Generated config header package", - licenses = {"MIT"}, - type = "package", - xpm = { - linux = { - ["1.0.0"] = { - url = "https://example.invalid/tinycfg-1.0.0.tar.gz", - sha256 = "0000000000000000000000000000000000000000000000000000000000000000", - }, - }, - }, - mcpp = { - language = "c++23", - import_std = false, - sources = { "src/*.c" }, - include_dirs = { "mcpp_generated/include" }, - generated_files = { - ["mcpp_generated/include/generated_config.h"] = "#pragma once\n#define TINYCFG_VALUE 42\n", - }, - targets = { ["tinycfg"] = { kind = "lib" } }, - deps = {}, - }, -} -EOF - -mkdir -p "$TMP/project/app/src" \ - "$TMP/project/app/.mcpp/.xlings/data/xpkgs/local-dev.tinycfg/1.0.0/src" -cd "$TMP/project/app" - -cat > .mcpp/.xlings/data/xpkgs/local-dev.tinycfg/1.0.0/src/tinycfg.c <<'EOF' -#include "generated_config.h" -int tinycfg_value(void) { - return TINYCFG_VALUE; -} -EOF - -cat > src/main.cpp <<'EOF' -extern "C" int tinycfg_value(void); -int main() { - return tinycfg_value() == 42 ? 0 : 1; -} -EOF - -cat > mcpp.toml < build.log 2>&1 || { - cat build.log - exit 1 -} - -generated=".mcpp/.xlings/data/xpkgs/local-dev.tinycfg/1.0.0/mcpp_generated/include/generated_config.h" -[[ -f "$generated" ]] || { - echo "FAIL: generated file was not materialized" - find .mcpp/.xlings/data/xpkgs/local-dev.tinycfg/1.0.0 -maxdepth 4 -type f | sort - exit 1 -} - -grep -q "TINYCFG_VALUE 42" "$generated" || { - echo "FAIL: generated file content mismatch" - cat "$generated" - exit 1 -} - -"$MCPP" run > run.log 2>&1 || { - cat run.log - exit 1 -} - -echo "OK" diff --git a/tests/e2e/606_constrained_source_globs_narrow_to_the_build.sh b/tests/e2e/606_constrained_source_globs_narrow_to_the_build.sh deleted file mode 100755 index b689ee842..000000000 --- a/tests/e2e/606_constrained_source_globs_narrow_to_the_build.sh +++ /dev/null @@ -1,123 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# A `[build] sources` entry may carry the accel it is for, and the build -# narrows to it. -# -# sources = ["src/**/*.cpp", { glob = "src/dev/**/*", accel = "widget9+{w1}" }] -# -# Three outcomes, each measured here with its control: -# - the build covers the constraint: the glob's C++ files compile, and its -# device-kind files reach the build program as MCPP_DEVICE_SOURCES; -# - `--no-accel`: the glob is left out -- its C++ file is not compiled and -# the device list is empty. This is the CPU-only variant of one project; -# - the build targets something else: refused, naming the glob and both -# sides (`accel-mismatch`); -# - a constrained glob matching no file: refused, naming the glob. -# -# `widget` is not a backend anything knows. The engine reads a shape. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -"$MCPP" new narrow > /dev/null; cd narrow -rm -f src/*.cppm; mkdir -p src/dev -cat > src/main.cpp <<'EOF' -int main() { return 0; } -EOF -# A C++ file under the constrained glob: whether it is compiled is visible in -# the verbose command lines. A device-kind file beside it: the engine has no -# rule for it, and hands it to the build program. -printf 'int widget_helper() { return 42; }\n' > src/dev/helper.cpp -printf '__global__ void k() {}\n' > src/dev/kernel.cu - -write_manifest() { # $1 = accel of the build, $2 = accel of the glob, $3 = glob - cat > mcpp.toml < build.mcpp <<'EOF' -import std; -import mcpp; - -// A `check` action per device source, and the reason it is here rather than a -// bare read of the variable: mcpp refuses a device source that reached no -// action (2026.9.6.5). A build program that only LOOKS at -// `mcpp::device_sources()` models a project whose device files compile to -// nothing, which is the defect that refusal exists to catch -- so this fixture -// declares the edge a real rule package would declare, and asserts on the -// variable as before. -int main() { - const char* d = mcpp::device_sources(); - std::string flat(d); - std::size_t n = 0, start = 0; - while (start <= flat.size()) { - auto nl = flat.find('\n', start); - auto one = flat.substr(start, nl == std::string::npos ? flat.size() - start : nl - start); - start = nl == std::string::npos ? flat.size() + 1 : nl + 1; - if (one.empty()) continue; - auto stamp = std::string(mcpp::out_dir()) + "/dev-" + std::to_string(n++) + ".stamp"; - mcpp::action a; - a.id = "seen"; - a.role = "check"; - a.description = "account for a device source"; - auto abs = std::string(mcpp::manifest_dir()) + "/" + one; - a.arg("cp").arg(abs.c_str()).arg(stamp.c_str()); - a.input(abs.c_str()); - a.output(stamp.c_str()); - a.submit(); - } - mcpp::warning((*d ? d : "(no device sources)")); - return 0; -} -EOF -} - -# ── One: the build covers the constraint ───────────────────────────────── -write_manifest "widget9+{w1,w2}" "widget9+{w1}" "src/dev/**/*" -"$MCPP" build -v > covered.log 2>&1 || { cat covered.log; echo "FAIL: a covered constraint failed the build"; exit 1; } -grep -q "helper.cpp" covered.log || { cat covered.log; echo "FAIL: the constrained glob's C++ file was not compiled"; exit 1; } -grep -q "src/dev/kernel.cu" covered.log || { cat covered.log; echo "FAIL: the device source did not reach the build program"; exit 1; } -echo "PASS: a covered constraint compiles its C++ and hands its device sources to the build program" - -# ── Two: --no-accel leaves the glob out ─────────────────────────────────── -"$MCPP" build -v --no-accel > none.log 2>&1 || { cat none.log; echo "FAIL: --no-accel failed the build"; exit 1; } -if grep -q "helper.cpp" none.log; then - cat none.log; echo "FAIL: the constrained glob was compiled under --no-accel"; exit 1 -fi -grep -q "(no device sources)" none.log || { cat none.log; echo "FAIL: device sources were listed under --no-accel"; exit 1; } -echo "PASS: --no-accel leaves the constrained glob out, C++ and device files alike" - -# ── Three: the build targets something the constraint is not within ────── -write_manifest "widget9+{w2}" "widget9+{w1}" "src/dev/**/*" -if "$MCPP" build > mismatch.log 2>&1; then - cat mismatch.log; echo "FAIL: a constraint outside the build's accel was accepted"; exit 1 -fi -grep -q "src/dev/\*\*/\*" mismatch.log || { cat mismatch.log; echo "FAIL: refusal does not name the glob"; exit 1; } -grep -q "widget9+{w1}" mismatch.log || { cat mismatch.log; echo "FAIL: refusal does not name the constraint"; exit 1; } -grep -q "widget9+{w2}" mismatch.log || { cat mismatch.log; echo "FAIL: refusal does not name what the build targets"; exit 1; } -reason="$("$MCPP" why toolchain --format json 2>/dev/null | jq -r '.data.reason // "-"' | tr -d '\r')" -[[ "$reason" == "accel-mismatch" ]] || { echo "FAIL: reason is '$reason', expected accel-mismatch"; exit 1; } -echo "PASS: a constraint outside the build is refused naming the glob and both sides" - -# ── Four: a constrained glob that matches nothing ───────────────────────── -write_manifest "widget9+{w1,w2}" "widget9+{w1}" "src/nowhere/**/*" -if "$MCPP" build > empty.log 2>&1; then - cat empty.log; echo "FAIL: a constrained glob matching nothing was accepted"; exit 1 -fi -grep -q "src/nowhere" empty.log || { cat empty.log; echo "FAIL: refusal does not name the empty glob"; exit 1; } -echo "PASS: a constrained glob matching nothing is refused" - -echo "PASS: constrained source globs narrow to the build" diff --git a/tests/e2e/607_chained_actions_form_the_device_link.sh b/tests/e2e/607_chained_actions_form_the_device_link.sh deleted file mode 100755 index cb304f7db..000000000 --- a/tests/e2e/607_chained_actions_form_the_device_link.sh +++ /dev/null @@ -1,85 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# One action may consume what another produced, and the engine only orders and -# fingerprints them. That is the whole engine-side content of a device link -# (multi-device design C-1/C-2): a rule package emits N `artifact` actions -# whose outputs stay out of the link, and one `object` action that reads them -# and produces the object that does join it. Nothing here names a device: -# the "device link" is `cat`, the "device compiler" is the toolchain's own C -# compiler, and the property under test is the graph, not the vendor. -# -# step 1 (artifact): src/parts/a.inc + src/parts/b.inc -> out/joined.c -# step 2 (object): out/joined.c -> out/joined.o (linked) -# -# Also measured: editing an input of step 1 rebuilds step 2 and relinks, which -# is the "changing the device link re-prepares" half of criterion C9. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -"$MCPP" new chain > /dev/null; cd chain -rm -f src/*.cppm; mkdir -p src/parts -cat > src/main.cpp <<'EOF2' -extern "C" int joined_value(); -int main() { return joined_value() == 42 ? 0 : 1; } -EOF2 -printf 'extern "C" int joined_value() { return\n' > src/parts/a.inc -printf '42; }\n' > src/parts/b.inc -cat > mcpp.toml <<'EOF2' -[package] -name = "chain" -version = "0.1.0" -[language] -standard = "c++23" -[targets.chain] -kind = "bin" -main = "src/main.cpp" -EOF2 -cat > build.mcpp <<'EOF2' -import std; -import mcpp; -int main() { - mcpp::rerun_if_changed("src/parts/a.inc"); - mcpp::rerun_if_changed("src/parts/b.inc"); - const std::string root = mcpp::manifest_dir(), out = mcpp::out_dir(); - const std::string joined = out + "/joined.cpp", obj = out + "/joined.o"; - { // Step 1: an ARTIFACT. Its output is data as far as the link is concerned. - mcpp::action a; - a.id = "join"; a.role = "artifact"; a.description = "join parts"; - a.arg("sh"); a.arg("-c"); - a.arg(("cat '" + root + "/src/parts/a.inc' '" + root + "/src/parts/b.inc' > '" + joined + "'").c_str()); - a.input((root + "/src/parts/a.inc").c_str()); - a.input((root + "/src/parts/b.inc").c_str()); - a.output(joined.c_str()); - a.submit(); - } - { // Step 2: an OBJECT that consumes step 1's output. The engine sees a - // path; ninja orders the two by it. - mcpp::action a; - a.id = "compile-joined"; a.role = "object"; a.description = "compile joined"; - a.arg((std::string(mcpp::toolchain_dir()) + "/bin/g++").c_str()); - a.arg("-c"); a.arg(joined.c_str()); a.arg("-o"); a.arg(obj.c_str()); - a.input(joined.c_str()); - a.output(obj.c_str()); - a.submit(); - } - return 0; -} -EOF2 - -"$MCPP" build > build1.log 2>&1 || { cat build1.log; echo "FAIL: the chained build failed"; exit 1; } -"$MCPP" run > run1.log 2>&1 || { cat run1.log; echo "FAIL: the linked object did not carry the joined value"; exit 1; } -echo "PASS: an object action consumed an artifact action's output and joined the link" - -# The second half of C9: a change at the head of the chain propagates. The -# joined value becomes 43, main returns 1, and only a relink would notice. -printf '43; }\n' > src/parts/b.inc -"$MCPP" build > build2.log 2>&1 || { cat build2.log; echo "FAIL: rebuild after editing an input failed"; exit 1; } -if "$MCPP" run > run2.log 2>&1; then - cat build2.log run2.log; echo "FAIL: the edit at the head of the chain did not reach the link"; exit 1 -fi -echo "PASS: editing an input of the first action rebuilt the second and relinked" - -echo "PASS: chained actions form the device link" diff --git a/tests/e2e/608_object_actions_reach_a_static_library.sh b/tests/e2e/608_object_actions_reach_a_static_library.sh deleted file mode 100755 index bb9d383b1..000000000 --- a/tests/e2e/608_object_actions_reach_a_static_library.sh +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# An `object`-role action's outputs join a STATIC LIBRARY, not only an -# executable. That is the engine half of the multi-device design's C-6: a -# package whose device code is its point declares `kind = "lib"`, and until -# this the actions it emitted were dropped with a warning and the archive came -# out with none of them in it. -# -# Nothing here names a device. The "device compiler" is the toolchain's own C -# compiler and the property under test is which link units an action attaches -# to, which is not a vendor question. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -"$MCPP" new archived > /dev/null; cd archived -rm -f src/*.cppm src/main.cpp -mkdir -p src -cat > src/host.c <<'EOF2' -extern int from_action(void); -int host_value(void) { return from_action(); } -EOF2 -cat > mcpp.toml <<'EOF2' -[package] -name = "archived" -version = "0.1.0" -[language] -standard = "c++23" -[build] -sources = ["src/*.c"] -[targets.archived] -kind = "lib" -EOF2 -cat > build.mcpp <<'EOF2' -import std; -import mcpp; -int main() { - const std::string out = std::string(mcpp::out_dir()); - const std::string src = out + "/piece.c"; - { std::ofstream f(src, std::ios::trunc); - f << "int from_action(void) { return 7; }\n"; } - const std::string obj = out + "/piece.o"; - mcpp::action a; - a.id = "piece"; a.role = "object"; a.description = "compile the piece"; - a.arg((std::string(mcpp::toolchain_dir()) + "/bin/gcc").c_str()); - a.arg("-c"); a.arg(src.c_str()); a.arg("-o"); a.arg(obj.c_str()); - a.input(src.c_str()); - a.output(obj.c_str()); - a.submit(); - return 0; -} -EOF2 - -# ── One: the action runs and its object is IN the archive ──────────────── -"$MCPP" build > build.log 2>&1 || { cat build.log; echo "FAIL: the build failed"; exit 1; } - -grep -q "produces no target" build.log && { - cat build.log - echo "FAIL: the action was dropped for want of a linked image" - exit 1 -} - -lib=$(find target -name 'libarchived.a' | head -1) -[ -n "$lib" ] || { echo "FAIL: no static library was produced"; exit 1; } - -# THE MEMBER LIST IS THE ASSERTION, not the exit status: an `ar` that was -# handed nothing still writes a well-formed archive and reports success. -members=$(ar t "$lib" 2>/dev/null | tr '\n' ' ') -case "$members" in - *piece.o*) ;; - *) echo "FAIL: piece.o is not a member of the archive (members: $members)"; exit 1 ;; -esac -echo "PASS: an object action's output is archived into a static library" - -# ── Two: the symbol is really there ───────────────────────────────────── -if command -v nm > /dev/null 2>&1; then - nm "$lib" 2>/dev/null | grep -q "from_action" || { - echo "FAIL: the archive has the member but not its symbol"; exit 1; } - echo "PASS: the archived member carries its symbol" -fi - -echo "PASS: object actions reach a static library" diff --git a/tests/e2e/609_shader_sources_are_device_sources.sh b/tests/e2e/609_shader_sources_are_device_sources.sh deleted file mode 100755 index a3bf22d71..000000000 --- a/tests/e2e/609_shader_sources_are_device_sources.sh +++ /dev/null @@ -1,162 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# A shader is a device source, and the classification table says so for every -# language a separate compiler consumes -- not only for NVIDIA's two. -# -# THE DEFECT THIS MEASURES. `SourceKind::Device` is documented as a graph role, -# "compiled by a device compiler mcpp does not drive", explicitly so that the -# table does not grow a row per vendor. The extension list nonetheless held -# `.cu` and `.hip` alone, so a shader in a constrained glob was refused: -# -# 'scale.comp' is listed in [build] sources, and mcpp has no role for the -# extension '.comp'. -# -# and the rule package that exists to compile it was told, in the same run, -# that there were no device sources -- a warning and an error contradicting -# each other about the same file. -# -# Four sections, and the last two are what make the first two a measurement -# rather than a demonstration. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -"$MCPP" new shaders > /dev/null; cd shaders -rm -f src/*.cppm -cat > src/main.cpp <<'EOF' -int main() { return 0; } -EOF -mkdir -p shaders - -# The build program prints what it was handed, delimited, so a section can -# assert on a whole list rather than on a substring of one name. -# MCPP_DEVICE_SOURCES is newline-separated and the advisory channel is one -# line per message, so the list is flattened before it is printed. Delimited, -# so an empty list is `device=[]` and not the absence of a line -- which is -# what section four asserts on. -cat > build.mcpp <<'EOF' -import std; -import mcpp; - -// A `check` action per device source, and the reason it is here rather than a -// bare read of the variable: mcpp refuses a device source that reached no -// action (2026.9.6.5). A build program that only LOOKS at -// `mcpp::device_sources()` models a project whose device files compile to -// nothing, which is the defect that refusal exists to catch -- so this fixture -// declares the edge a real rule package would declare, and asserts on the -// variable as before. -int main() { - std::string flat(mcpp::device_sources()); - std::size_t n = 0, start = 0; - while (start <= flat.size()) { - auto nl = flat.find('\n', start); - auto one = flat.substr(start, nl == std::string::npos ? flat.size() - start : nl - start); - start = nl == std::string::npos ? flat.size() + 1 : nl + 1; - if (one.empty()) continue; - auto stamp = std::string(mcpp::out_dir()) + "/dev-" + std::to_string(n++) + ".stamp"; - mcpp::action a; - a.id = "seen"; - a.role = "check"; - a.description = "account for a device source"; - auto abs = std::string(mcpp::manifest_dir()) + "/" + one; - a.arg("cp").arg(abs.c_str()).arg(stamp.c_str()); - a.input(abs.c_str()); - a.output(stamp.c_str()); - a.submit(); - } - for (auto& c : flat) if (c == '\n') c = ' '; - mcpp::warning(("device=[" + flat + "]").c_str()); - return 0; -} -EOF - -write_manifest() { # $1 = accel line, $2 = sources line - cat > mcpp.toml < "shaders/s.$e"; done - -write_manifest 'accel = "widget9+{w1}"' '"src/*.cpp", { glob = "shaders/*", accel = "widget9+{w1}" }' -"$MCPP" build > all.log 2>&1 || { cat all.log; echo "FAIL: a project with shader sources failed to build"; exit 1; } -missing="" -for e in $exts; do - grep -q "shaders/s.$e" all.log || missing="$missing $e" -done -[ -z "$missing" ] || { cat all.log; echo "FAIL: not handed to the build program:$missing"; exit 1; } -echo "PASS: all 18 shader and kernel extensions reach the build program as device sources" - -# ── Two: and none of them is compiled ───────────────────────────────────── -# -# The complement of section one. A device source that ALSO went to the C++ -# compiler would satisfy section one and produce an object nothing can link. -"$MCPP" build -v > verbose.log 2>&1 -for e in $exts; do - if grep -q "s\.$e -o\|s\.$e\.o" verbose.log; then - cat verbose.log; echo "FAIL: s.$e was compiled"; exit 1 - fi -done -echo "PASS: no shader is offered to the C++ compiler" - -# ── Three: an extension that is NOT in the table is still refused ───────── -# -# Without this the first section would also pass on a table that classified -# every unknown extension as a device source, which is the opposite of what -# the table is for. -printf '// wgsl\n' > shaders/s.wgsl -write_manifest 'accel = "widget9+{w1}"' '"src/*.cpp", { glob = "shaders/s.wgsl", accel = "widget9+{w1}" }' -if "$MCPP" build > unknown.log 2>&1; then - cat unknown.log; echo "FAIL: an extension with no role was accepted"; exit 1 -fi -grep -q "no role for the extension" unknown.log || { - cat unknown.log; echo "FAIL: refusal does not say the extension has no role"; exit 1; } -grep -q "\.wgsl" unknown.log || { cat unknown.log; echo "FAIL: refusal does not name the extension"; exit 1; } -rm -f shaders/s.wgsl -echo "PASS: an extension the table does not name is still refused, by name" - -# ── Four: the default source globs did not widen ────────────────────────── -# -# The compatibility promise the whole change rests on. A package that vendors -# shaders it builds elsewhere must not start handing them to a build program -# on upgrade, so a project with NO `sources` entry must see none of them. -write_manifest '' '"src/*.cpp"' -mkdir -p src/shaders && cp shaders/s.comp src/shaders/ -rm -f mcpp.toml -cat > mcpp.toml <<'EOF' -[package] -name = "shaders" -version = "0.1.0" -[language] -standard = "c++23" - -[targets.shaders] -kind = "bin" -main = "src/main.cpp" -EOF -"$MCPP" build > default.log 2>&1 || { cat default.log; echo "FAIL: the default-glob build failed"; exit 1; } -grep -q "device=\[\]" default.log || { - cat default.log; echo "FAIL: a shader under src/ reached a build program with no sources entry"; exit 1; } -echo "PASS: the default source globs still exclude every device extension" - -echo "PASS: shader sources are device sources" diff --git a/tests/e2e/610_feature_controlled_rule_collection.sh b/tests/e2e/610_feature_controlled_rule_collection.sh deleted file mode 100755 index 1e8b143ff..000000000 --- a/tests/e2e/610_feature_controlled_rule_collection.sh +++ /dev/null @@ -1,202 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 610_feature_controlled_rule_collection.sh — one package, several rules, the -# set chosen by the consumer's feature request (mcpp 2026.9.5.3+). -# -# Before this, a host-module package contributed exactly one module: the unit -# at its lib root. A collection such as `mcpp:plugins` -- `mcpp.rules.cuda`, -# `mcpp.rules.spirv`, later `mcpp.tools.*` -- therefore needed one package per -# member, and a consumer could not say "these two" in the one place it says -# everything else about a dependency. Now every module INTERFACE unit among the -# package's resolved sources is a host module of its own, and `[features.] -# sources` is what puts a unit into that set. -# -# Pinned here: -# 1. a feature the consumer activates makes its unit importable, under the -# name the unit declares, and that unit may import the lib root; -# 2. a unit whose feature is NOT activated is not compiled and cannot be -# imported -- the module set is the feature set, not the file set; -# 3. two activated features give two modules; -# 4. a collection in the `mcpp` namespace draws no reserved-prefix warning, -# and the same collection under another namespace draws one per unit; -# 5. a package with neither a lib root nor a listed interface unit keeps the -# diagnostic it had. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -write_collection() { # $1 = namespace - rm -rf plugins - mkdir -p plugins/src plugins/rules - cat > plugins/mcpp.toml < plugins/src/plugins.cppm <<'EOT' -export module mcpp.plugins; -import std; -export namespace mcpp::plugins { -inline constexpr std::string_view version = "0.1.0"; -} -EOT - cat > plugins/rules/a.cppm <<'EOT' -export module mcpp.rules.a; -import std; -import mcpp; -import mcpp.plugins; // the lib root precedes every feature unit -export namespace mcpp::rules::a { -inline void apply() { - mcpp::cxxflag("-DRULE_A=1"); - // Unquoted on purpose: the token is stringized by the consumer, and a - // quoted value would have to survive two layers of shell quoting. - mcpp::cxxflag(std::format("-DPLUGINS_VERSION={}", mcpp::plugins::version).c_str()); -} -} -EOT - cat > plugins/rules/b.cppm <<'EOT' -export module mcpp.rules.b; -import std; -import mcpp; -export namespace mcpp::rules::b { -inline void apply() { mcpp::cxxflag("-DRULE_B=1"); } -} -EOT -} - -write_app() { # $1 = namespace, $2 = features (TOML list body), $3 = imports (space-separated module names) - rm -rf app - mkdir -p app/src - cat > app/mcpp.toml < app/build.mcpp - cat > app/src/main.cpp <<'EOT' -#include -#ifndef RULE_A -#define RULE_A 0 -#endif -#ifndef RULE_B -#define RULE_B 0 -#endif -#ifndef PLUGINS_VERSION -#define PLUGINS_VERSION none -#endif -#define STR_(x) #x -#define STR(x) STR_(x) -int main() { std::printf("A=%d B=%d V=%s\n", RULE_A, RULE_B, STR(PLUGINS_VERSION)); } -EOT -} - -# ── 1. one feature, one module, and the unit imports the lib root ─────────── -write_collection mcpp -write_app mcpp '"rules-a"' 'mcpp.rules.a' -cd app -"$MCPP" build > b1.log 2>&1 || { cat b1.log; echo "FAIL: a feature-selected rule did not build"; exit 1; } -out="$("$MCPP" run 2>&1 | grep '^A=' | tail -1)" -[[ "$out" == 'A=1 B=0 V=0.1.0' ]] || { echo "FAIL: expected A=1 B=0 V=0.1.0, got: $out"; exit 1; } -echo "PASS: a feature the consumer activates makes its unit importable under its declared name" -if grep -q 'belongs to the modules maintained by the mcpp project' b1.log; then - echo "FAIL: the mcpp namespace drew a reserved-prefix warning"; cat b1.log; exit 1; fi -echo "PASS: a collection in the mcpp namespace draws no reserved-prefix warning" -cd .. - -# ── 2. a unit whose feature is not activated is not importable ─────────────── -write_app mcpp '"rules-a"' 'mcpp.rules.b' -cd app -if "$MCPP" build > b2.log 2>&1; then - echo "FAIL: a rule whose feature is not active was importable"; cat b2.log; exit 1; fi -grep -q 'mcpp.rules.b' b2.log || { echo "FAIL: the refusal does not name the module"; cat b2.log; exit 1; } -echo "PASS: a unit whose feature is not activated is not compiled and cannot be imported" -cd .. - -# ── 3. two features, two modules ───────────────────────────────────────────── -write_app mcpp '"rules-a", "rules-b"' 'mcpp.rules.a mcpp.rules.b' -cd app -"$MCPP" build > b3.log 2>&1 || { cat b3.log; echo "FAIL: two feature-selected rules did not build"; exit 1; } -out="$("$MCPP" run 2>&1 | grep '^A=' | tail -1)" -[[ "$out" == 'A=1 B=1 V=0.1.0' ]] || { echo "FAIL: expected A=1 B=1 V=0.1.0, got: $out"; exit 1; } -echo "PASS: two activated features give two modules" -cd .. - -# ── 4. the control: another namespace is warned about, once per unit ───────── -write_collection acme -write_app acme '"rules-a", "rules-b"' 'mcpp.rules.a mcpp.rules.b' -cd app -"$MCPP" build > b4.log 2>&1 || { cat b4.log; echo "FAIL: the acme collection did not build"; exit 1; } -n=$(grep -c 'belongs to the modules maintained by the mcpp project' b4.log || true) -# three units -- the lib root `mcpp.plugins` and the two rules -- each claims the prefix -[[ "$n" -eq 3 ]] || { echo "FAIL: expected 3 reserved-prefix warnings (one per unit), got $n"; cat b4.log; exit 1; } -echo "PASS: the same collection under another namespace draws one warning per unit" -cd .. - -# ── 5. neither a lib root nor a listed unit: the diagnostic is unchanged ───── -rm -rf plugins app -mkdir -p plugins/rules app/src -cat > plugins/mcpp.toml <<'EOT' -[package] -name = "plugins" -version = "0.1.0" - -[build] -sources = ["rules/*.cppm"] - -[targets.plugins] -kind = "lib" -EOT -cat > plugins/rules/impl.cppm <<'EOT' -module nothing.exported; // an implementation unit is not an interface -EOT -cat > app/mcpp.toml <<'EOT' -[package] -name = "app" -version = "0.1.0" - -[dependencies] -plugins = { path = "../plugins", host-module = true } -EOT -printf 'import mcpp;\nint main() {}\n' > app/build.mcpp -echo 'int main() {}' > app/src/main.cpp -cd app -if "$MCPP" build > b5.log 2>&1; then - echo "FAIL: a library whose only unit implements a module no unit declares built"; cat b5.log; exit 1; fi -# The library is compiled as the package it is (mcpp#797), so its own defect is -# reported as it would be anywhere: the unit, the module, and where the build -# programs' workspace is to reproduce it. -grep -q 'nothing.exported' b5.log || { echo "FAIL: the diagnostic does not name the module"; cat b5.log; exit 1; } -grep -q 'rules/impl.cppm' b5.log || { echo "FAIL: the diagnostic does not name the unit"; cat b5.log; exit 1; } -grep -q "the build programs' workspace" b5.log || { echo "FAIL: the diagnostic does not say where to reproduce it"; cat b5.log; exit 1; } -echo "PASS: a package with neither a lib root nor a listed interface unit keeps its diagnostic" - -echo "PASS: feature-controlled rule collection" diff --git a/tests/e2e/611_variant_switch_is_not_replayed.sh b/tests/e2e/611_variant_switch_is_not_replayed.sh deleted file mode 100755 index 4494109af..000000000 --- a/tests/e2e/611_variant_switch_is_not_replayed.sh +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 611_variant_switch_is_not_replayed.sh — a plain `mcpp build` after -# `mcpp build --no-accel` builds the device variant again (mcpp 2026.9.5.3+). -# -# The device variant is part of the fingerprint, so the two builds land in -# different directories. The fast path, which runs before any plan exists, -# replayed whichever directory was built LAST: after `--no-accel`, a plain -# build reported "Finished in 0.00s" and `mcpp run` executed the CPU variant, -# while the manifest said the project is a device build. The graph now records -# whether an override chose it, and the fast paths decline such a graph. -# -# The fixture is the one e2e 605 uses: a cfg section keyed on `accelerator` -# supplies a define, so the running program reports which variant it is. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -"$MCPP" new probe > /dev/null; cd probe -rm -f src/*.cppm -cat > mcpp.toml <<'EOT' -[package] -name = "probe" -version = "0.1.0" -[language] -standard = "c++23" - -[build] -accel = "widget9+{w1}" - -[target.'cfg(accelerator = "widget")'.build] -defines = ["WIDGET_ON=1"] - -[targets.probe] -kind = "bin" -main = "src/main.cpp" -EOT -cat > src/main.cpp <<'EOT' -#include -#ifndef WIDGET_ON -#define WIDGET_ON 0 -#endif -int main() { std::printf("WIDGET=%d\n", WIDGET_ON); } -EOT - -variant() { "$MCPP" run "$@" 2>&1 | grep '^WIDGET=' | tail -1; } - -"$MCPP" build > b1.log 2>&1 || { cat b1.log; echo "FAIL: the device build failed"; exit 1; } -[[ "$(variant)" == "WIDGET=1" ]] || { echo "FAIL: the first build is not the device variant"; exit 1; } - -"$MCPP" build --no-accel > b2.log 2>&1 || { cat b2.log; echo "FAIL: --no-accel failed"; exit 1; } -[[ "$(variant --no-accel)" == "WIDGET=0" ]] || { echo "FAIL: --no-accel did not produce the CPU variant"; exit 1; } - -# The measurement. Before the fix this printed "Finished dev in 0.00s" and the -# run below answered WIDGET=0. -"$MCPP" build > b3.log 2>&1 || { cat b3.log; echo "FAIL: the plain build after --no-accel failed"; exit 1; } -out="$(variant)" -[[ "$out" == "WIDGET=1" ]] || { - echo "FAIL: a plain build after --no-accel handed back the CPU variant: $out" - cat b3.log; exit 1; } -echo "PASS: a plain build after --no-accel builds and runs the device variant" - -# And the other direction still holds (it did before: the flag bypasses the -# fast path), so the two are symmetric. -"$MCPP" build --no-accel > b4.log 2>&1 -[[ "$(variant --no-accel)" == "WIDGET=0" ]] || { echo "FAIL: --no-accel after a plain build is not the CPU variant"; exit 1; } -echo "PASS: --no-accel after a plain build still selects the CPU variant" - -# The graph says which selection wrote it, in the file itself. -n_default=$(grep -l '^# mcpp:graph=normal;schedule=[a-z-]*;accel=default' target/*/*/build.ninja | wc -l) -n_override=$(grep -l '^# mcpp:graph=normal;schedule=[a-z-]*;accel=override' target/*/*/build.ninja | wc -l) -[[ "$n_default" -eq 1 && "$n_override" -eq 1 ]] || { - echo "FAIL: expected one default and one override graph, got default=$n_default override=$n_override" - head -3 target/*/*/build.ninja; exit 1; } -echo "PASS: each graph records the selection that wrote it" - -echo "PASS: a variant switch is not replayed" diff --git a/tests/e2e/613_sycl_sources_are_device_sources.sh b/tests/e2e/613_sycl_sources_are_device_sources.sh deleted file mode 100755 index 0e65f9650..000000000 --- a/tests/e2e/613_sycl_sources_are_device_sources.sh +++ /dev/null @@ -1,171 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# A SYCL translation unit is a device source, and the criterion for that is the -# COMPILER rather than the dialect. -# -# WHAT THIS MEASURES. `SourceKind::Device` is documented as a graph role -- "not -# scanned, no BMI, compiled by a device compiler mcpp does not drive" -- and -# every extension in the table until now was a dialect a general C++ compiler -# would refuse. `.sycl` is not: its content is ordinary C++. What makes it a -# device unit is that it goes to a second compiler with a device back end -# (icpx, or a clang built with the SYCL front end), which mcpp does not drive -# and which does not accept C++20 modules. -# -# So this file asserts the row exists, that adding it changed nothing else, and -# -- the part a test naming `.sycl` alone would miss -- that the C++ spelling of -# the same content is still C++. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -"$MCPP" new sycltest > /dev/null; cd sycltest -rm -f src/*.cppm -cat > src/main.cpp <<'EOF2' -int main() { return 0; } -EOF2 -mkdir -p src/kernels - -# The build program prints what it was handed, delimited, so a section can -# assert on a whole list rather than on a substring of one name. Delimited, so -# an empty list is `device=[]` and not the absence of a line -- which is what -# section three asserts on. -cat > build.mcpp <<'EOF2' -import std; -import mcpp; - -// A `check` action per device source, and the reason it is here rather than a -// bare read of the variable: mcpp refuses a device source that reached no -// action (2026.9.6.5). A build program that only LOOKS at -// `mcpp::device_sources()` models a project whose device files compile to -// nothing, which is the defect that refusal exists to catch -- so this fixture -// declares the edge a real rule package would declare, and asserts on the -// variable as before. -int main() { - std::string flat(mcpp::device_sources()); - std::size_t n = 0, start = 0; - while (start <= flat.size()) { - auto nl = flat.find('\n', start); - auto one = flat.substr(start, nl == std::string::npos ? flat.size() - start : nl - start); - start = nl == std::string::npos ? flat.size() + 1 : nl + 1; - if (one.empty()) continue; - auto abs = std::string(mcpp::manifest_dir()) + "/" + one; - auto stamp = std::string(mcpp::out_dir()) + "/dev-" + std::to_string(n++) + ".stamp"; - mcpp::action a; - a.id = "seen"; - a.role = "check"; - a.description = "account for a device source"; - a.arg("cp").arg(abs.c_str()).arg(stamp.c_str()); - a.input(abs.c_str()); - a.output(stamp.c_str()); - a.submit(); - } - for (auto& c : flat) if (c == '\n') c = ' '; - mcpp::warning(("device=[" + flat + "]").c_str()); - return 0; -} -EOF2 - -write_manifest() { # $1 = accel line, $2 = sources line - cat > mcpp.toml < src/kernels/saxpy.sycl <<'EOF2' -// Ordinary C++. What makes this a device unit is which compiler consumes it. -extern "C" int saxpy_device(int n) { return n; } -EOF2 - -write_manifest 'accel = "sycl"' '"src/*.cpp", { glob = "src/kernels/*.sycl", accel = "sycl" }' -"$MCPP" build > one.log 2>&1 || { cat one.log; echo "FAIL: a project with a .sycl source failed to build"; exit 1; } -grep -q "device=\[.*src/kernels/saxpy.sycl.*\]" one.log || { - cat one.log; echo "FAIL: the .sycl unit did not reach MCPP_DEVICE_SOURCES"; exit 1; } -echo "PASS: a .sycl unit in a constrained glob reaches the build program" - -# ── Two: and it is not offered to the C++ compiler ──────────────────────── -# -# The complement of section one, and the section that would fail if `.sycl` -# had been added as an ordinary C++ extension instead of a device one: the -# unit would reach the build program AND be compiled, producing an object -# built by the wrong compiler that links and silently contains no device code. -"$MCPP" build -v > two.log 2>&1 -if grep -qE "saxpy\.sycl -o|saxpy\.sycl\.o" two.log; then - cat two.log; echo "FAIL: the .sycl unit was offered to the C++ compiler"; exit 1 -fi -echo "PASS: the .sycl unit is not compiled by mcpp's own compiler" - -# ── Three: the SAME CONTENT named .cpp is still C++ ─────────────────────── -# -# The control, and the one section that separates "the table gained a row" from -# "the table now classifies by content". A project with a `.cpp` under the same -# directory must compile it and must NOT hand it to the build program. -rm -f src/kernels/saxpy.sycl -cp /dev/null src/kernels/saxpy.cpp -cat > src/kernels/saxpy.cpp <<'EOF2' -extern "C" int saxpy_device(int n) { return n; } -EOF2 -write_manifest 'accel = "sycl"' '"src/*.cpp", "src/kernels/*.cpp"' -"$MCPP" build -v > three.log 2>&1 || { cat three.log; echo "FAIL: the .cpp spelling failed to build"; exit 1; } -grep -q "device=\[\]" three.log || { - cat three.log; echo "FAIL: a .cpp was handed to the build program as a device source"; exit 1; } -grep -qE "saxpy\.cpp" three.log || { - cat three.log; echo "FAIL: the .cpp was not compiled"; exit 1; } -echo "PASS: the same content named .cpp is compiled as C++ and is not a device source" - -# ── Four: --no-accel takes the constrained glob out ─────────────────────── -# -# The variant switch, applied to the new row. A device source carries the accel -# it is for, so a build that asks for none must not see it -- and this is what -# makes the CPU half of a seam reachable without editing the manifest. Asserted -# as an empty list rather than as the absence of a name, so a build program that -# printed nothing at all would fail here. -# Section three left a `.cpp` in that directory and no `.sycl`; put the device -# unit back so the glob has something to exclude. -rm -f src/kernels/saxpy.cpp -cat > src/kernels/saxpy.sycl <<'EOF2' -extern "C" int saxpy_device(int n) { return n; } -EOF2 -write_manifest 'accel = "sycl"' '"src/*.cpp", { glob = "src/kernels/*.sycl", accel = "sycl" }' -"$MCPP" build --no-accel > four.log 2>&1 || { cat four.log; echo "FAIL: --no-accel failed to build"; exit 1; } -grep -q "device=\[\]" four.log || { - cat four.log; echo "FAIL: --no-accel still handed the .sycl unit to the build program"; exit 1; } -echo "PASS: --no-accel removes the constrained glob that carries the .sycl unit" - -# ── Five: the default source globs did not widen ────────────────────────── -# -# The other half of the compatibility promise. A package that vendors SYCL -# sources it builds elsewhere must not start handing them to a build program on -# upgrade, so a project with NO `sources` entry must see none of them. -rm -f mcpp.toml -cat > mcpp.toml <<'EOF2' -[package] -name = "sycltest" -version = "0.1.0" -[language] -standard = "c++23" - -[targets.sycltest] -kind = "bin" -main = "src/main.cpp" -EOF2 -"$MCPP" build > five.log 2>&1 || { cat five.log; echo "FAIL: the default-glob project failed to build"; exit 1; } -grep -q "device=\[\]" five.log || { - cat five.log; echo "FAIL: a default glob picked up the .sycl source"; exit 1; } -echo "PASS: the default source globs do not include .sycl" - -echo "ALL PASS: 613" diff --git a/tests/e2e/619_a_tool_may_not_be_conditioned_on_a_layer.sh b/tests/e2e/619_a_tool_may_not_be_conditioned_on_a_layer.sh deleted file mode 100755 index ad8da2b33..000000000 --- a/tests/e2e/619_a_tool_may_not_be_conditioned_on_a_layer.sh +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# `[target..xlings…]` accepts a target predicate. It must REFUSE one -# that names a target-side layer. -# -# The reason is schedule, not style. The five RESOLVED layer keys (`c-abi`, -# `compiler`, ...) are answered by dependency RESOLUTION, so a -# predicate naming one is held back to the second merge pass -- which runs after -# tools are provisioned and after every build.mcpp. An entry admitted there is -# declared and never installed, and the build that results is the worst-shaped -# failure there is: it succeeds, and the tool is simply absent. -# -# THE CRITERION IS TWO-SIDED, WHICH IS WHY THE TOOL NAMES NOTHING REAL. Without -# the refusal the entry is dropped in silence and this project BUILDS; with it -# the build fails naming both halves. A tool that exists would make both legs -# look the same on a machine that already has it. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -mkdir -p src -cat > mcpp.toml <<'TOML' -[package] -name = "layer-gated-tool" -version = "0.1.0" - -[target.'cfg(c-abi = "musl")'.xlings.workspace] -"xim:mcpp-e2e-no-such-tool" = "1.0" -TOML -cat > src/main.cpp <<'CPP' -int main() { return 0; } -CPP - -if out=$("$MCPP" build 2>&1); then status=0; else status=$?; fi -echo "$out" - -if [ "$status" -eq 0 ]; then - echo "FAIL: the build succeeded; a tool conditioned on a layer was accepted and then never installed" - exit 1 -fi - -# The message must name the TOOL -- the author has to know which line to move -- -# and the PREDICATE, which is the half they are not looking at. -case "$out" in - *"xim:mcpp-e2e-no-such-tool"*) ;; - *) echo "FAIL: the refusal does not name the tool"; exit 1 ;; -esac -case "$out" in - *'c-abi = "musl"'*) ;; - *) echo "FAIL: the refusal does not name the predicate"; exit 1 ;; -esac -# And it must offer both ways out, or it is a refusal without a repair. -case "$out" in - *"feature-xlings"*) ;; - *) echo "FAIL: the refusal does not name the gate form as the way out"; exit 1 ;; -esac - -echo "PASS: a tool conditioned on a target-side layer is refused, naming the tool and the predicate" diff --git a/tests/e2e/623_device_sources_must_reach_an_action.sh b/tests/e2e/623_device_sources_must_reach_an_action.sh deleted file mode 100755 index 95d612316..000000000 --- a/tests/e2e/623_device_sources_must_reach_an_action.sh +++ /dev/null @@ -1,83 +0,0 @@ -#!/usr/bin/env bash -# requires: elf gcc -# A device-kind source that no action compiles is refused, and the refusal -# names the file. -# -# The engine has no compile rule for `.cu` and never will: a device source is -# handed to the package's build program and comes back as an action, or it is -# not compiled at all. Nothing checked that it came back, so a project whose -# build program does not claim the extension -- or that has no build program -- -# got an undefined reference at the link naming a SYMBOL and never the file -# that would have defined it. For a `kind = "lib"` target not even that, -# because an archive is not resolved. -# -# TWO LEGS, because the two situations have different fixes and the message -# has to distinguish them. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -mk() { - rm -rf p; mkdir -p p/src/kernels - cat > p/src/main.cpp <<'EOF' -extern "C" void k(); -int main() { return 0; } -EOF - cat > p/src/kernels/k.cu <<'EOF' -extern "C" __global__ void k() {} -EOF - cat > p/mcpp.toml <<'EOF' -[package] -name = "orphan" -version = "0.1.0" -accelerators = ["cuda"] -[build] -accel = "cuda12.9+{sm_89}" -sources = [ - "src/*.cpp", - { glob = "src/kernels/*.cu", accel = "cuda12.9+{sm_89}" }, -] -[targets.orphan] -kind = "bin" -main = "src/main.cpp" -EOF -} - -# ── leg 1: no build program at all ────────────────────────────────────────── -mk -cd p -out=$("$MCPP" build 2>&1) && { echo "FAIL: a package with an uncompilable device source built"; exit 1; } -echo "$out" | grep -q 'src/kernels/k.cu' || { echo "FAIL: the refusal does not name the file"; echo "$out" | tail -5; exit 1; } -echo "$out" | grep -q 'no `build.mcpp`' || { echo "FAIL: the refusal does not say the build program is missing"; echo "$out" | tail -5; exit 1; } -echo "ok: no build program -- named the file and the missing program" -cd .. - -# ── leg 2: a build program that claims nothing ────────────────────────────── -# -# The ordinary shape for a project with two backends: a rule takes the -# extensions it knows and leaves the rest, so a file no imported rule claims is -# left over. Distinguished from leg 1 because the fix is different. -mk -cat > p/build.mcpp <<'EOF' -import std; -import mcpp; -int main() { return 0; } -EOF -cd p -out=$("$MCPP" build 2>&1) && { echo "FAIL: an unclaimed device source built"; exit 1; } -echo "$out" | grep -q 'src/kernels/k.cu' || { echo "FAIL: the refusal does not name the file"; echo "$out" | tail -5; exit 1; } -echo "$out" | grep -q 'ran but declared no action' || { echo "FAIL: the refusal does not distinguish a program that claimed nothing"; echo "$out" | tail -5; exit 1; } -echo "ok: build program present -- named the file and said no action took it" -cd .. - -# ── the negative leg: --no-accel leaves the glob out, so there is nothing to -# refuse. Without this the two legs above would pass on an engine that refused -# EVERY device source, which is a different and wrong behaviour. -mk -cd p -"$MCPP" build --no-accel >/dev/null 2>&1 || { echo "FAIL: --no-accel must not be refused; the constrained glob is left out"; exit 1; } -echo "ok: --no-accel builds -- no device source, nothing to account for" - -echo "PASS: a device source that reaches no action is refused, and only then" diff --git a/tests/e2e/630_a_rule_brings_its_own_environment.sh b/tests/e2e/630_a_rule_brings_its_own_environment.sh deleted file mode 100755 index 727d363d0..000000000 --- a/tests/e2e/630_a_rule_brings_its_own_environment.sh +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# A RULE PACKAGE DECLARES THE PAYLOADS ITS RULE NEEDS, AND THE PROJECT WRITES -# ONE EDGE. -# -# This is the arrangement the two preceding tests exist to make safe. A project -# that wants a device island used to write the rule edge AND repeat the rule's -# own package list with exact pins -- a copy that goes stale silently, because -# the rule moves and the projects do not. -# -# TWO GATES, AND BOTH MUST OPEN. The feature says whether the rule is wanted; -# the `cfg(accelerator = ...)` selector says which builds actually need the -# device toolkit. Both legs are here: without the second, an implementation -# that ignores the selector and downloads the toolkit for every build passes -- -# and that build is the cheapest one, the one CI runs, and the one a vendor -# toolkit is gigabytes too expensive for. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -TOOL=zoxide - -mkdir -p dep/src -cat > dep/src/lib.cpp <<'EOF' -int dep_touch() { return 1; } -EOF -cat > dep/build.mcpp < -#include -import mcpp; -int main() { - const char* d = mcpp::xpkg_dir("xim", "$TOOL"); - std::string out = std::string(mcpp::manifest_dir()) + "/answered.txt"; - std::FILE* f = std::fopen(out.c_str(), "w"); - if (f == nullptr) return 3; - std::fprintf(f, "%s\n", d == nullptr ? "" : d); - std::fclose(f); - return 0; -} -EOF -# THE SHAPE mcpp:plugins USES: the payload is named by the rule, under the -# feature that selects the rule and the accelerator the rule serves. -cat > dep/mcpp.toml < app/src/main.cpp <<'EOF' -int main() { return 0; } -EOF -# ONE EDGE, AND NOTHING ELSE. No [xlings.workspace] anywhere in the project. -cat > app/mcpp.toml <<'EOF' -[package] -name = "consumer" -version = "0.1.0" -accelerators = ["cuda"] -[dependencies] -ruleowner = { path = "../dep", features = ["rules-toy"] } -[targets.consumer] -kind = "bin" -main = "src/main.cpp" -EOF - -export MCPP_HOME="$TMP/home" -mkdir -p "$MCPP_HOME" -STORE="$MCPP_HOME/registry/data/xpkgs/xim-x-$TOOL" - -cd app - -# ── leg 1: no accelerator, so neither gate opens ──────────────────────────── -"$MCPP" build >cpu.log 2>&1 || { echo "FAIL: the CPU-only build did not build"; tail -15 cpu.log; exit 1; } -[ -d "$STORE" ] && { - echo "FAIL: a build that named no accelerator downloaded the device payload" - ls "$STORE" - exit 1 -} -echo "no accelerator: nothing installed" - -# ── leg 2: the accelerator is named, so both gates open ───────────────────── -"$MCPP" build --accel "cuda12.9+{sm_89}" >dev.log 2>&1 || { - echo "FAIL: the device build did not build" - tail -15 dev.log - exit 1 -} -[ -d "$STORE" ] || { - echo "FAIL: the rule's payload was never installed" - tail -15 dev.log - exit 1 -} -answered="$TMP/dep/answered.txt" -[ -s "$answered" ] || { echo "FAIL: the rule's build program recorded nothing"; exit 1; } -echo "the rule's build program saw: $(cat "$answered")" -grep -q "xim-x-$TOOL" "$answered" || { - echo "FAIL: the rule cannot find the payload it declared: $(cat "$answered")" - exit 1 -} - -echo "PASS: the rule brings its own environment, and only when both gates open" diff --git a/tests/e2e/631_action_depfile_tracks_a_runtime_discovered_include.sh b/tests/e2e/631_action_depfile_tracks_a_runtime_discovered_include.sh deleted file mode 100755 index f26d66ca9..000000000 --- a/tests/e2e/631_action_depfile_tracks_a_runtime_discovered_include.sh +++ /dev/null @@ -1,147 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 631_action_depfile_tracks_a_runtime_discovered_include.sh — `mcpp::action`'s -# `depfile` field: a rule that discovers its own dependency graph by RUNNING -# (glslangValidator `--depfile`, glslc `-MD -MF`, slangc `-depfile`, nvcc/clang -# `-MD -MF`) has a way to tell ninja about it. -# -# THE DEFECT THIS DEFENDS AGAINST. `a.inputs` is fixed when build.mcpp runs, -# before the action's own command has executed. A shader/device compiler does -# not know its `#include` graph at that point — it learns it by PARSING the -# source, and reports the result afterward as a Make-style depfile. Without -# this field there is no channel for that report to reach ninja: the action's -# edge tracks exactly the files build.mcpp named, so editing a file the -# command merely READ (never a declared `.input()`) reruns nothing, and -# `mcpp build` stays green over a stale generated artifact. -# -# THE SCENARIO. The action's command is a stand-in generator: it writes its -# declared output AND a depfile naming a second file — `included.glsl` — that -# the action never declares as an input. Touching that second file (a bare -# `touch`, no content edit — exactly what a real `#include`'s mtime changing -# looks like) must make ninja rerun the action on the next build. -# -# ASSERTED ON A RUN COUNTER gen.sh writes as a side effect, NOT on grepping -# `mcpp build`'s own log for the ninja description text ("GENERATE ..."). That -# text cannot appear there: on a non-verbose SUCCESSFUL build mcpp passes -# ninja `--quiet` and only surfaces its captured stdout when the build FAILS -# or `--verbose` is given (src/build/ninja_backend.cppm, src/build/execute.cppm) -# — so a log grep for it on success is vacuous, true whether or not the edge -# ran. Verified directly against this fixture: even the very first, from- -# scratch build (which unquestionably runs the action) prints no such line. -# The run counter is a real side effect of the COMMAND executing, so reading -# it back is a direct measurement instead of a guess about log formatting. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -mkdir -p "$TMP/shaderdep/src" -cd "$TMP/shaderdep" - -cat > mcpp.toml <<'EOF' -[package] -name = "shaderdep" -version = "0.1.0" -EOF - -cat > src/main.cpp <<'EOF' -#include -int generated_value(); -int main() { std::printf("VALUE=%d\n", generated_value()); } -EOF - -# The file the ACTION discovers only by "compiling" — analogous to a shader's -# `#include`. Its CONTENT is never read below; only its mtime matters, which -# is exactly what a depfile-tracked dependency promises to react to. -echo "float unused = 1.0;" > included.glsl - -# The stand-in device compiler. $1 = the discovered file, $2 = the source it -# generates, $3 = the depfile to write, $4 = a run counter so the test can -# tell the command executed again versus ninja deciding it was up to date. -# Deliberately NOT `set -e`: the `[ -f ] &&` idiom below is a normal false -# branch, not an error. -cat > gen.sh <<'EOF' -#!/usr/bin/env bash -n=0 -[ -f "$4" ] && n="$(cat "$4")" -n=$((n + 1)) -echo "$n" > "$4" -printf 'int generated_value() { return %s; }\n' "$n" > "$2" -# Make syntax, the shape `-MD -MF`/`--depfile` produce: OUTPUT: PREREQUISITES. -printf '%s: %s\n' "$2" "$1" > "$3" -EOF -chmod +x gen.sh - -cat > build.mcpp <<'EOF' -#include -#include -import mcpp; -int main() { - const std::string root = mcpp::manifest_dir(); - const std::string out = std::string(mcpp::out_dir()) + "/generated.cpp"; - const std::string dep = std::string(mcpp::out_dir()) + "/generated.cpp.d"; - const std::string count = root + "/run_count.txt"; - - mcpp::action a; - a.id = "genshader"; - a.role = "source"; - a.depfile = dep.c_str(); - // Deliberately NOT declared as `.input(...)`: the whole point is that - // ninja learns about it from the depfile the command writes, not from - // anything build.mcpp told the engine in advance. - a.arg((root + "/gen.sh").c_str()) - .arg((root + "/included.glsl").c_str()) - .arg(out.c_str()) - .arg(dep.c_str()) - .arg(count.c_str()) - .output(out.c_str()) - .submit(); -} -EOF - -run_count() { cat run_count.txt 2>/dev/null || echo ""; } - -# ── 1. first build: the action runs once, VALUE reflects run #1 ──────────── -"$MCPP" build > b1.log 2>&1 || { cat b1.log; echo "FAIL: initial build failed"; exit 1; } -[[ "$(run_count)" == "1" ]] || { - cat b1.log; echo "FAIL: the action did not run on a fresh build (count=$(run_count))"; exit 1; } -out="$("$MCPP" run 2>&1 | grep '^VALUE=' | tail -1)" -[[ "$out" == "VALUE=1" ]] || { echo "FAIL: unexpected initial output: $out"; exit 1; } - -# ── 2. THE CONTROL: rebuild with nothing changed must not rerun the action ── -# Without this half, the assertion in part 3 would pass against a backend -# that reruns the action on every build regardless of the depfile. -"$MCPP" build > b2.log 2>&1 || { cat b2.log; echo "FAIL: no-op rebuild failed"; exit 1; } -[[ "$(run_count)" == "1" ]] || { - cat b2.log - echo "FAIL: the action reran although nothing it tracks changed (count=$(run_count))" - exit 1; } - -# ── 3. touch the file named ONLY in the depfile — never a declared input ─── -touch included.glsl -"$MCPP" build > b3.log 2>&1 || { cat b3.log; echo "FAIL: rebuild after touch failed"; exit 1; } -[[ "$(run_count)" == "2" ]] || { - cat b3.log - echo "FAIL: touching a file the depfile named did not rerun the action" - echo " (count=$(run_count), expected 2)" - echo " this is the defect: a.inputs alone cannot express this" - echo " dependency, and without depfile support ninja never learns it" - exit 1; } -out="$("$MCPP" run 2>&1 | grep '^VALUE=' | tail -1)" -[[ "$out" == "VALUE=2" ]] || { - echo "FAIL: the action reran but the artifact was not regenerated: $out" - echo " (expected VALUE=2)" - exit 1; } - -# ── 4. and it behaves once more, so #3 was not a first-build artifact ────── -touch included.glsl -"$MCPP" build > b4.log 2>&1 || { cat b4.log; echo "FAIL: second touch rebuild failed"; exit 1; } -[[ "$(run_count)" == "3" ]] || { - cat b4.log - echo "FAIL: the second touch did not rerun the action (count=$(run_count))" - exit 1; } -out="$("$MCPP" run 2>&1 | grep '^VALUE=' | tail -1)" -[[ "$out" == "VALUE=3" ]] || { - echo "FAIL: unexpected output after the second touch: $out (expected VALUE=3)" - exit 1; } - -echo "OK" diff --git a/tests/e2e/648_an_install_hook_sees_the_build_target.sh b/tests/e2e/648_an_install_hook_sees_the_build_target.sh deleted file mode 100755 index 6b015f90d..000000000 --- a/tests/e2e/648_an_install_hook_sees_the_build_target.sh +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/env bash -# requires: elf -# 648_an_install_hook_sees_the_build_target.sh -- #613: a dependency's install -# hook receives the build's target under the names a build program uses, and the -# two toolchain names present and empty. -# -# The toolchain is resolved after the dependency graph, so no compiler or -# standard library exists when a dependency installs, and the values are emitted -# empty rather than guessed. The run exports both toolchain names first: a hook -# that reads a non-empty value read one inherited from its parent, which the -# always-emitted rule exists to prevent. -# -# The package below has no download. Its `install()` writes the module mcpp -# compiles next, with the values it read compiled in, and the consumer prints -# them. What is compared is therefore what the hook saw, carried in the artefact. -# The composition itself is unit-tested on every platform (test_install_hook_env). -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT - -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } - -export MCPP_HOME="$TMP/mcpp-home" -source "$(dirname "$0")/_inherit_toolchain.sh" - -mkdir -p "$TMP/proj/local-index/pkgs/h" "$TMP/proj/src" -cd "$TMP/proj" - -cat > local-index/pkgs/h/acme.hookprobe.lua <<'LUA' -package = { - spec = "1", - namespace = "acme", - name = "hookprobe", - description = "Compiles in what its install hook saw", - licenses = {"MIT"}, - type = "package", - xpm = { - linux = { ["latest"] = { ref = "1.0.0" }, ["1.0.0"] = { } }, - macosx = { ["latest"] = { ref = "1.0.0" }, ["1.0.0"] = { } }, - windows = { ["latest"] = { ref = "1.0.0" }, ["1.0.0"] = { } }, - }, - mcpp = { - language = "c++23", - import_std = false, - sources = { "src/hookprobe.cppm" }, - targets = { ["hookprobe"] = { kind = "lib" } }, - deps = {}, - }, -} - -import("xim.libxpkg.pkginfo") - -function install() - local dir = pkginfo.install_dir() - os.mkdir(path.join(dir, "src")) - local function value(name) return os.getenv(name) or "" end - io.writefile(path.join(dir, "src", "hookprobe.cppm"), string.format([[ -export module hookprobe; -export const char* hook_compiler() { return "%s"; } -export const char* hook_stdlib() { return "%s"; } -export const char* hook_target_os() { return "%s"; } -export const char* hook_target() { return "%s"; } -]], value("MCPP_COMPILER"), value("MCPP_CXX_STDLIB"), value("MCPP_TARGET_OS"), - value("MCPP_TARGET"))) - return true -end -LUA - -cat > src/main.cpp <<'CPP' -#include -import hookprobe; -int main() { - std::printf("compiler=%s stdlib=%s os=%s target=%s\n", - hook_compiler(), hook_stdlib(), hook_target_os(), hook_target()); - return 0; -} -CPP - -cat > mcpp.toml <<'TOML' -[package] -name = "consumer" -version = "0.1.0" - -[dependencies.acme] -hookprobe = "1.0.0" - -[indices] -acme = { path = "local-index" } -TOML - -MCPP_COMPILER=inherited MCPP_CXX_STDLIB=inherited "$MCPP" run > run.log 2>&1 \ - || fail "the build or the run failed" run.log -grep -Eq '^compiler= stdlib= os=linux target=[a-z0-9_]+-linux' run.log \ - || fail "the install hook did not see the build's target with empty toolchain names" run.log -echo "install hook environment OK" diff --git a/tests/e2e/659_an_action_declaration_has_no_size_limit.sh b/tests/e2e/659_an_action_declaration_has_no_size_limit.sh deleted file mode 100755 index 5770d7425..000000000 --- a/tests/e2e/659_an_action_declaration_has_no_size_limit.sh +++ /dev/null @@ -1,209 +0,0 @@ -#!/usr/bin/env bash -# 659_an_action_declaration_has_no_size_limit.sh -- `mcpp::action`'s list -# fields have no declared size limit (2026.9.13.1). -# -# Until this release the bundled `mcpp` module collected an action's inputs, -# outputs and command into fixed arrays (8192 bytes of serialised JSON for -# `inputs` and `outputs`, 16384 for `command`) and refused a declaration that -# did not fit with "arguments did not fit". The bound was in bytes, so a -# consumer's checkout depth decided whether a list of 44 resource files fit -# (HuxerUI#130 measured the margin at 45 bytes), and `outputs` is the one list -# an author cannot shorten: an output the program does not name cannot be -# built, and there is no depfile for outputs. -# -# This fixture declares one action whose serialised `inputs` and `outputs` each -# exceed the old bound by construction, and asserts that the whole declaration -# reaches the graph: `build.ninja` carries an edge with exactly N outputs and N -# inputs, N being this file's constant and not a number read back from the -# engine. The reverse leg -- the same fixture on 2026.9.12.4 is refused with -# the overflow diagnostic -- was run once before merge and is recorded in the -# pull request; CI cannot run two engines on one fixture. -# -# The command is the engine itself, which is present on every shard, exits 0 -# and writes nothing, so the fixture runs everywhere the suite runs (no -# `# requires:` line, as 313 does). The outputs are therefore not produced; -# ninja does not fail on an unproduced output (313 states the measurement), and -# producing files is the command's business, covered by 188. What is under -# test here is the declaration channel. -# -# N is chosen so that the action's EDGE LINE in build.ninja (outputs, rule, -# inputs) is longer than the POSIX per-argument limit of 128 KiB as well as -# Windows's 32767-character CreateProcess limit. That is the second thing this -# fixture measures: the engine's command-length guard used to read the edge -# line as a proxy for the command, which is right for a rule that expands -# `$in` and `$out` and wrong for an action rule, whose command is a literal -# argv and whose inputs and outputs exist only so that ninja can order and -# re-run it. Its first run on the Windows shard refused this fixture with the -# whole list counted as argv; the guard now measures a literal command's own -# text, and the same fixture is what holds that on every shard. -# -# A second action carries a command past 16384 bytes, the old bound of the -# command list, on the POSIX hosts: its command is `true`, a program that -# accepts any argv and exits 0, which Windows does not have. 19200 bytes is -# under every operating system's limit on a process's arguments; what remains -# bounded at run time is the tool's own argv, by the OS (the design record's -# section 2.4), and that is not the engine's bound on the declaration. -set -e - -source "$(dirname "$0")/_host_path.sh" - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -readonly N=600 - -mkdir -p app/src app/in -cd app - -cat > mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" - -[targets.app] -kind = "bin" -main = "src/main.cpp" -EOF - -cat > src/main.cpp <<'EOF' -#include -int main() { std::puts("LARGE_DECLARATION_OK"); } -EOF - -# N input files, so that ninja finds every declared input on disk. Each path -# is padded to a fixed width with a long directory name, so the serialised -# lists cross the old 8192-byte bound and the edge line crosses 128 KiB -# regardless of where this fixture runs: with a 100-byte directory name each -# path is at least 115 bytes, and 600 inputs plus 600 outputs put more than -# 138000 bytes on the one edge line. -PAD="a-directory-name-long-enough-to-make-the-edge-line-cross-the-per-argument-limit-of-the-host-os-xxxx" -mkdir -p "in/$PAD" -for i in $(seq 1 $N); do : > "in/$PAD/input-$i.txt"; done - -ROOT_HOST=$(host_path "$PWD") -MCPP_HOST=$(host_path "$(cd "$(dirname "$MCPP")" && pwd)/$(basename "$MCPP")") - -# The long-command leg: a program that accepts any argv and exits 0. POSIX -# hosts only (see the header). -TRUE_HOST="" -case "$(uname -s)" in - MINGW* | MSYS* | CYGWIN*) ;; - *) TRUE_HOST=$(host_path "$(type -P true)") ;; -esac - -cat > build.mcpp < b1.log 2>&1 || { cat b1.log; echo "FAIL: a build with a wide action was refused"; exit 1; } -out="$("$MCPP" run 2>&1 | tail -1)" -[[ "$out" == *LARGE_DECLARATION_OK* ]] || { echo "FAIL: the program did not run: '$out'"; exit 1; } - -NINJA=$(find target -name build.ninja -not -path '*/.build-mcpp/*' -print -quit) -[ -n "$NINJA" ] || { echo "FAIL: no build.ninja"; exit 1; } - -# THE criterion: the edge in the graph names every declared output and every -# declared input. Counted against N, not against anything the engine reports. -# Only the action's own edge is read (`build : mcpp_action_ -# `): the engine also lists a source action's outputs a second time, -# as the inputs of the package's ordering phony, and counting the whole file -# would report 2N. -count_in_edge() { - # $1 = the substring that identifies the file kind - grep ': mcpp_action_' "$NINJA" | tr -s ' ' '\n' | grep -c "$1" || true -} -outs=$(count_in_edge "output-[0-9]*\.txt") -ins=$(count_in_edge "input-[0-9]*\.txt") -[ "$outs" = "$N" ] || { echo "FAIL: build.ninja names $outs of $N declared outputs"; exit 1; } -[ "$ins" = "$N" ] || { echo "FAIL: build.ninja names $ins of $N declared inputs"; exit 1; } -echo "ok: the edge carries $N outputs and $N inputs" - -if [ -n "$TRUE_HOST" ]; then - stamp=$(find target -name 'long-command.stamp' | wc -l | tr -d '[:space:]') - [ "$stamp" = "1" ] || { cat b1.log; echo "FAIL: the long-command check did not run (stamps: $stamp)"; exit 1; } - echo "ok: a command of 19200 bytes ran" -fi - -# The overflow marker must be absent: the refusal path of the old bound is the -# only thing that ever wrote it, and the message it now carries is about -# allocation failure, which this build did not have. -if grep -q '"overflow":true' b1.log; then cat b1.log; echo "FAIL: the overflow marker appeared"; exit 1; fi - -# Replay: an unrelated source is added, so the project fast path is off and -# the build program's cache record is replayed rather than re-run. The edge -# must come back complete from the record, not only from a live run. -cat > src/extra.cpp <<'EOF' -int extra() { return 1; } -EOF -"$MCPP" build > b2.log 2>&1 || { cat b2.log; echo "FAIL: rebuild after an unrelated source failed"; exit 1; } -NINJA=$(find target -name build.ninja -not -path '*/.build-mcpp/*' -print -quit) -outs=$(count_in_edge "output-[0-9]*\.txt") -[ "$outs" = "$N" ] || { echo "FAIL: after a replay build.ninja names $outs of $N declared outputs"; exit 1; } -echo "ok: the replayed record carries all $N outputs" - -# The negative direction of the guard: a LITERAL command that really is too -# long for the host is still refused, before anything is compiled, and the -# refusal names the edge by its first output rather than by every output. -# 2200 arguments of 64 bytes is 140800 bytes, over the POSIX 128 KiB -# per-argument limit and over Windows's 32767 alike, so this leg runs on -# every shard. The program named does not have to exist: the guard fires -# while build.ninja is being written. -mkdir -p ../too-long/src -cp mcpp.toml ../too-long/ && cp src/main.cpp ../too-long/src/ -cd ../too-long -cat > build.mcpp < b4.log 2>&1; then - cat b4.log; echo "FAIL: a 140800-byte literal command was accepted"; exit 1 -fi -grep -q "over the .* byte limit" b4.log || { cat b4.log; echo "FAIL: the refusal is not the command-length guard's"; exit 1; } -grep -q "too-long.stamp" b4.log || { cat b4.log; echo "FAIL: the refusal does not name the edge"; exit 1; } -# One output, so no "(and N more outputs)" suffix; the whole message stays -# short enough to read. -[ "$(wc -c < b4.log)" -lt 4000 ] || { wc -c b4.log; echo "FAIL: the refusal printed the whole edge line"; exit 1; } -echo "ok: a command over the host limit is refused, naming the edge" - -echo "OK" diff --git a/tests/e2e/660_an_action_names_the_engine_through_mcpp_self.sh b/tests/e2e/660_an_action_names_the_engine_through_mcpp_self.sh deleted file mode 100755 index 555f0c3aa..000000000 --- a/tests/e2e/660_an_action_names_the_engine_through_mcpp_self.sh +++ /dev/null @@ -1,124 +0,0 @@ -#!/usr/bin/env bash -# 660_an_action_names_the_engine_through_mcpp_self.sh -- `${mcpp.self}` in an -# action's argv is the engine's own executable, and `mcpp stage` through it is -# a portable copy (2026.9.13.1). -# -# An action's command is an argv with no shell assumed, so a build program had -# no portable way to copy a file: `cp` is absent on Windows, `cmd /c copy` is a -# shell and an 8191-character limit, and a copier carried by a package is a -# host-tool sub-build for one copy. The engine is the one program present -# wherever a build runs, and `mcpp stage --verify content --output ` -# is the copy every `stage_file` edge in build.ninja already performs: it -# creates the destination's parent and writes only when the bytes differ. -# `${mcpp.self}` is how an action names it, in the same substitution family as -# `${mcpp.out_dir}` and `${mcpp.target_file:}`. -# -# No `# requires:` line: the point is that this works on every shard, and the -# Windows one is the shard it exists for. The reverse leg -- under 2026.9.12.4 -# the token stays literal in build.ninja and the edge fails -- was run once -# before merge and is recorded in the pull request. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -mkdir -p app/src -cd app - -cat > mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" - -[targets.app] -kind = "bin" -main = "src/main.cpp" -EOF - -cat > src/main.cpp <<'EOF' -#include -int main() { std::puts("SELF_COPY_OK"); } -EOF - -cat > build.mcpp <<'EOF' -import std; -import mcpp; -int main() { - mcpp::action a; - a.id = "copy"; - a.role = "artifact"; - a.description = "the linked program, copied by the engine"; - a.arg("${mcpp.self}").arg("stage").arg("--verify").arg("content") - .arg("--output").arg("${mcpp.out_dir}/copied/deeper/app-copy") - .arg("${mcpp.target_file:app}") - .input("${mcpp.target_file:app}") - .output("${mcpp.out_dir}/copied/deeper/app-copy") - .submit(); -} -EOF - -"$MCPP" build > b1.log 2>&1 || { cat b1.log; echo "FAIL: build failed"; exit 1; } -out="$("$MCPP" run 2>&1 | tail -1)" -[[ "$out" == *SELF_COPY_OK* ]] || { echo "FAIL: the program did not run: '$out'"; exit 1; } - -NINJA=$(find target -name build.ninja -not -path '*/.build-mcpp/*' -print -quit) -[ -n "$NINJA" ] || { echo "FAIL: no build.ninja"; exit 1; } - -# The token is substituted, never emitted literally. -if grep -q 'mcpp\.self' "$NINJA"; then - grep -n 'mcpp\.self' "$NINJA" | head -3 - echo "FAIL: \${mcpp.self} reached build.ninja unsubstituted"; exit 1 -fi - -# ── every `${mcpp.*}` path token expands to the GENERIC spelling ─────────── -# -# A token is spliced into text the build program wrote, and that text writes -# its own separators as `/` (`${mcpp.out_dir}/copied/deeper/app-copy`); a -# native `C:\…` prefix beside that `/` is a mixed spelling, and the command -# line reaches the tool as one string (mcpp#393). Every token this action names -# -- `${mcpp.self}`, `${mcpp.out_dir}` and `${mcpp.target_file:app}` -- is an -# engine path, so on Windows a backslash left in the argv IS the regression. -# On POSIX the two spellings are byte-identical, so this leg only bites on the -# shard the test exists for; the `# requires:`-free note at the top is why it -# is here and not in a unit test. -# -# The ARGV is read out of the rule's `command =` line, past the wrapper the -# engine adds when a check/stamp or an env/cwd is in play (`... -- `); a -# bare action has no wrapper, and then the whole line is the argv. The EDGE -# line cannot show the bug: `escape_ninja_path` generic-izes every node name -# whatever the token spelled. -CMD_LINES=$( - awk '/^rule mcpp_action_/{seen=1; next} - seen && /^ command = /{sub(/^ command = /, ""); print; seen=0}' "$NINJA" -) -[ -n "$CMD_LINES" ] || { echo "FAIL: no mcpp_action_ rule in build.ninja"; exit 1; } -while IFS= read -r line; do - argv="${line#* -- }" - case "$argv" in - *\\*) printf 'FAIL: a ${mcpp.*} path token expanded to a native spelling:\n %s\n' "$argv" - exit 1;; - esac -done <<< "$CMD_LINES" -echo "ok: \${mcpp.*} path tokens are spelled with forward slashes" - -# The copy exists, in a directory the action never created itself, and is the -# program byte for byte: the only way it got there is the engine's `stage`. -COPY=$(find target -path '*/copied/deeper/app-copy' -print -quit) -[ -n "$COPY" ] || { cat b1.log; echo "FAIL: the copy action produced nothing"; exit 1; } -BIN=$(find target -path '*/bin/app' -print -quit) -[ -n "$BIN" ] || BIN=$(find target -path '*/bin/app.exe' -print -quit) -[ -n "$BIN" ] || { echo "FAIL: no linked program under target/"; exit 1; } -cmp -s "$BIN" "$COPY" || { echo "FAIL: the copy differs from the program"; exit 1; } -echo "ok: the engine copied the program through \${mcpp.self}" - -# A no-op rebuild leaves the copy alone: the edge is satisfied by its output, -# and `stage --verify content` would write nothing even if it ran. -before=$(stat -c %Y "$COPY" 2>/dev/null || stat -f %m "$COPY") -sleep 1 -"$MCPP" build > b2.log 2>&1 || { cat b2.log; echo "FAIL: no-op rebuild failed"; exit 1; } -after=$(stat -c %Y "$COPY" 2>/dev/null || stat -f %m "$COPY") -[ "$before" = "$after" ] || { echo "FAIL: the copy was rewritten on a no-op rebuild ($before -> $after)"; exit 1; } -echo "ok: a no-op rebuild copied nothing" - -echo "OK" diff --git a/tests/e2e/780_a_passing_check_moves_its_stamp_past_a_changed_input.sh b/tests/e2e/780_a_passing_check_moves_its_stamp_past_a_changed_input.sh deleted file mode 100755 index 62ff71507..000000000 --- a/tests/e2e/780_a_passing_check_moves_its_stamp_past_a_changed_input.sh +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env bash -# 780_a_passing_check_moves_its_stamp_past_a_changed_input.sh — a `role = -# "check"` whose command writes no stamp, after one of its inputs changed. -# -# The engine writes such a check's stamp when the command succeeds (313). Until -# 2026.9.26.2 it only CREATED the stamp: an existing one was left alone. So once -# an input changed, the check ran and passed, and its stamp stayed older than -# that input -- and ninja ran it again on every build after, forever. Measured -# with mcpp-plugins' `deps-cmake`, whose check is a CMake build: every `mcpp -# build` after one edit re-ran configure, build and install. -# -# The criterion is the build AFTER the one that re-ran the check: it must not -# run the check again. Portable like 313: the command is the engine itself. -set -e - -source "$(dirname "$0")/_host_path.sh" - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -mkdir -p src -cat > mcpp.toml <<'EOF' -[package] -name = "stampmove" -version = "0.1.0" -EOF -cat > src/main.cpp <<'EOF' -#include -int main() { std::printf("STAMP_MOVE_OK\n"); } -EOF -echo one > input.txt - -MCPP_HOST="$(host_path "$MCPP")" -INPUT_HOST="$(host_path "$TMP/input.txt")" -cat > build.mcpp < b1.log 2>&1 || { cat b1.log; echo "FAIL: build failed"; exit 1; } -ran b1.log || { cat b1.log; echo "FAIL: the first build did not run the check"; exit 1; } - -"$MCPP" build -v > b2.log 2>&1 || { cat b2.log; echo "FAIL: no-op rebuild failed"; exit 1; } -if ran b2.log; then cat b2.log; echo "FAIL: the check re-ran with nothing changed"; exit 1; fi - -# A changed input: the check runs once... -sleep 1 -echo two > input.txt -"$MCPP" build -v > b3.log 2>&1 || { cat b3.log; echo "FAIL: rebuild after the edit failed"; exit 1; } -ran b3.log || { cat b3.log; echo "FAIL: a changed input did not re-run the check"; exit 1; } - -# ...and not again: its stamp is now newer than the input. -"$MCPP" build -v > b4.log 2>&1 || { cat b4.log; echo "FAIL: rebuild after the check failed"; exit 1; } -if ran b4.log; then - cat b4.log - echo "FAIL: the check re-ran on the build after it passed -- its stamp stayed older than the input" - exit 1 -fi - -out="$("$MCPP" run 2>&1 | tail -1)" -[[ "$out" == *STAMP_MOVE_OK* ]] || { echo "FAIL: the program did not run: '$out'"; exit 1; } -echo "OK" diff --git a/tests/e2e/790_a_prepare_action_populates_an_unknown_directory.sh b/tests/e2e/790_a_prepare_action_populates_an_unknown_directory.sh deleted file mode 100644 index 4a71ab3c1..000000000 --- a/tests/e2e/790_a_prepare_action_populates_an_unknown_directory.sh +++ /dev/null @@ -1,139 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 790_a_prepare_action_populates_an_unknown_directory.sh — the `prepare` -# role (design §5.4 P, mcpp#701/#702, SPEC-007 R3.3): construction whose file -# names are not known when the build program runs, because they are decided -# by a command it merely invokes (a vcpkg/CMake install, an SDK unpack) — -# `install.sh` here, standing in for one of those. The build program declares -# only the DIRECTORY (`a.output_dir`); the header and the shared library -# inside it are named by the script, never by build.mcpp. -# -# What this holds: -# -# 1. ON THE FIRST BUILD, a unit that `#include`s a header the action wrote -# compiles, and the program — linked against a library the SAME action -# wrote, found only through `runtime_search_dir` — runs and gets the -# right answer. Neither the header nor the library exists before this -# build starts; both exist only because the compile and the link waited -# for the action (design §5.4 P's ordering: every compile edge and every -# link edge of the declaring package). -# 2. A SECOND BUILD RUNS NOTHING: no compile, no link, no action — a plain -# "up to date" from ninja's own dependency tracking on unchanged inputs. -# 3. AFTER AN INPUT OF THE ACTION CHANGES, it runs ONCE — and the build -# after THAT does not run it again (R2, design §5.4: the engine touches -# the action's stamp on every success, whether or not the command wrote -# it, so the stamp is newer than the input from that point on). -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -mkdir -p "$TMP/prep/src" -cd "$TMP/prep" - -cat > mcpp.toml <<'EOF' -[package] -name = "prep" -version = "0.1.0" - -[toolchain] -linux = "gcc@16.1.0" - -[targets.prep] -kind = "bin" -main = "src/main.cpp" -EOF - -# The command a real rule package would run (vcpkg, a CMake `install` -# target). It decides the file NAMES; build.mcpp never sees them, only the -# directory they land in ($1). -cat > install.sh <<'EOF' -#!/usr/bin/env bash -set -e -PREFIX="$1" -mkdir -p "$PREFIX/include" "$PREFIX/lib" -cat > "$PREFIX/include/greet.h" < "$PREFIX/lib/greet.c" < recipe.txt - -cat > src/main.cpp <<'EOF' -#include -int main() { return prep_greet() == 99 ? 0 : 1; } -EOF - -cat > build.mcpp <<'EOF' -import mcpp; -#include -int main() { - const std::string root = mcpp::manifest_dir(); - const std::string prefix = std::string(mcpp::out_dir()) + "/prep-install"; - - mcpp::action a; - a.id = "prep:install"; - a.role = mcpp::roles::prepare; - a.arg((root + "/install.sh").c_str()).arg(prefix.c_str()) - .input((root + "/install.sh").c_str()) - .input((root + "/recipe.txt").c_str()) - .output((prefix + ".stamp").c_str()) - .output_dir(prefix.c_str()) - .submit(); - - mcpp::include_dir((prefix + "/include").c_str()); - mcpp::link_lib("prepgreet"); - mcpp::link_search((prefix + "/lib").c_str()); - mcpp::runtime_search_dir((prefix + "/lib").c_str()); - return 0; -} -EOF - -MCPP="${MCPP:-mcpp}" -ran_prepare() { grep -qE 'PREPARE prep:install|__action(-stamp)? .*install\.sh' "$1"; } - -# ── 1. first build: header compiles, program runs through runtime_search_dir -"$MCPP" build -v > b1.log 2>&1 || { cat b1.log; echo "FAIL: first build failed"; exit 1; } -ran_prepare b1.log || { cat b1.log; echo "FAIL: the prepare action did not run on the first build"; exit 1; } -nj=$(find target -name build.ninja -not -path '*/.build-mcpp/*' | head -1) -[[ -n "$nj" ]] || { echo "FAIL: no build.ninja"; exit 1; } -grep -qE '^build [^:]*(prep\.stamp|prepgreet)' "$nj" >/dev/null || true # sanity, not load-bearing -out1="$("$MCPP" run 2>&1)" || { echo "FAIL: run failed after the first build: $out1"; exit 1; } - -# ── 2. second build runs nothing at all ───────────────────────────────────── -"$MCPP" build -v > b2.log 2>&1 || { cat b2.log; echo "FAIL: second build failed"; exit 1; } -if ran_prepare b2.log; then - cat b2.log; echo "FAIL: the prepare action re-ran on an unchanged second build"; exit 1 -fi -if grep -qE '\bmain\.o\b' b2.log; then - cat b2.log; echo "FAIL: the compile re-ran on an unchanged second build"; exit 1 -fi - -# ── 3. an input changes: the action runs once, then not again ────────────── -sleep 1 -echo "recipe v2" > recipe.txt -"$MCPP" build -v > b3.log 2>&1 || { cat b3.log; echo "FAIL: third build (after the input changed) failed"; exit 1; } -ran_prepare b3.log || { cat b3.log; echo "FAIL: the action did not re-run after its input changed"; exit 1; } - -"$MCPP" build -v > b4.log 2>&1 || { cat b4.log; echo "FAIL: fourth build failed"; exit 1; } -if ran_prepare b4.log; then - cat b4.log - echo "FAIL: the action re-ran on the build after it passed -- R2's stamp rule did not hold" - exit 1 -fi -out2="$("$MCPP" run 2>&1)" || { echo "FAIL: run failed after the recipe changed: $out2"; exit 1; } - -echo "PASS: 790_a_prepare_action_populates_an_unknown_directory" diff --git a/tests/e2e/791_a_prepare_action_that_creates_nothing_fails.sh b/tests/e2e/791_a_prepare_action_that_creates_nothing_fails.sh deleted file mode 100644 index 08dbf9cc7..000000000 --- a/tests/e2e/791_a_prepare_action_that_creates_nothing_fails.sh +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 791_a_prepare_action_that_creates_nothing_fails.sh — design §5.4 P, SPEC-007 -# R3.3: "when the command succeeds and the directory does not exist or holds -# no file other than the action's stamps, the engine writes no stamp and fails -# the edge, naming the directory." A -# `prepare` action promises a directory the build reads BY DIRECTORY -# (`include_dir`/`link_search`/`runtime_search_dir`); a command that exits 0 -# without populating it is indistinguishable from one that silently did -# nothing, so this cannot be a warning the way an incomplete environment is -# (R1.2) -- nothing downstream can read a directory that was never created. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -mkdir -p "$TMP/app/src" -cd "$TMP/app" - -cat > mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" -EOF -printf '#include \nint main(){ std::printf("ok\\n"); }\n' > src/main.cpp - -write_program() { # $1 = where the stamp lies: beside | inside - cat > build.mcpp < -int main() { - const std::string prefix = std::string(mcpp::out_dir()) + "/never-created"; - const std::string stamp = std::string("$1") == "inside" - ? prefix + "/installed.stamp" : prefix + ".stamp"; - mcpp::action a; - a.id = "empty:prepare"; - a.role = mcpp::roles::prepare; - a.arg("true").output(stamp.c_str()).output_dir(prefix.c_str()).submit(); - return 0; -} -EOF -} - -MCPP="${MCPP:-mcpp}" - -# Two legs. BESIDE: the directory is never created. INSIDE: ninja creates the -# stamp's parent, which IS the directory, before the command runs, so the -# directory exists and holds nothing the command wrote; a post-condition that -# asked only whether it exists would pass there and defend nothing. -for where in beside inside; do - rm -rf target - write_program "$where" - if "$MCPP" build > b.log 2>&1; then - cat b.log - echo "FAIL ($where): a prepare action whose command created nothing let the build succeed" - exit 1 - fi - grep -qF "never-created" b.log \ - || { cat b.log; echo "FAIL ($where): the failure does not name the declared directory"; exit 1; } - grep -qi "does not exist or holds no file" b.log \ - || { cat b.log; echo "FAIL ($where): the failure does not state the post-condition"; exit 1; } - # No half-satisfied edge: the stamp must not exist either, or a later - # build would read a check that "passed" without its post-condition. - if find target -name '*.stamp' -path '*never-created*' | grep -q .; then - echo "FAIL ($where): a stamp was written although the directory holds nothing"; exit 1 - fi - echo " ok: the stamp $where the directory, and the empty directory fails the edge" -done - -echo "PASS: 791_a_prepare_action_that_creates_nothing_fails" diff --git a/tests/e2e/792_a_rerun_input_inside_a_prepare_directory_is_warned.sh b/tests/e2e/792_a_rerun_input_inside_a_prepare_directory_is_warned.sh deleted file mode 100644 index 632851b72..000000000 --- a/tests/e2e/792_a_rerun_input_inside_a_prepare_directory_is_warned.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 792_a_rerun_input_inside_a_prepare_directory_is_warned.sh — SPEC-007 R1.3 -# (design §5.4 P): a build program's re-run inputs are declared BEFORE -# anything is built, and a `prepare` action's directory is filled AFTER a -# build program has already run once for this build — it is a ninja edge, -# scheduled after `mcpp build`'s configure step ends. A program that also -# names a file or a glob INSIDE such a directory as its own -# `rerun_if_changed`/`rerun_if_changed_glob` reads a CONSTRUCTION RESULT -# while it configures: correct on the second build, once a previous build -# populated the directory, and wrong on the first. This is the pattern of -# watching an install prefix to place a first installation's libraries on the -# NEXT plan (the route mcpp-plugins' deps-vcpkg/deps-cmake used before this -# role existed). -# -# Warned, not refused: the program still configures correctly today. Two -# programs are declared, one for `rerun_if_changed` (a fixed path) and one -# for `rerun_if_changed_glob` (a pattern) — SPEC-007 R1.3 names both. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -mkdir -p "$TMP/prep/src" -cd "$TMP/prep" - -cat > mcpp.toml <<'EOF' -[package] -name = "prep" -version = "0.1.0" - -[toolchain] -linux = "gcc@16.1.0" - -[targets.prep] -kind = "bin" -main = "src/main.cpp" -EOF - -cat > install.sh <<'EOF' -#!/usr/bin/env bash -set -e -PREFIX="$1" -mkdir -p "$PREFIX/include" -echo '#define GREETING 1' > "$PREFIX/include/greet.h" -EOF -chmod +x install.sh - -printf '#include \nint main(){ std::printf("ok\\n"); }\n' > src/main.cpp - -emit_build_mcpp() { # $1 = the mcpp::rerun_if_changed* call under test - cat > build.mcpp < -int main() { - const std::string root = mcpp::manifest_dir(); - const std::string prefix = std::string(mcpp::out_dir()) + "/prep-install"; - - mcpp::action a; - a.id = "prep:install"; - a.role = mcpp::roles::prepare; - a.arg((root + "/install.sh").c_str()).arg(prefix.c_str()) - .input((root + "/install.sh").c_str()) - .output((prefix + ".stamp").c_str()) - .output_dir(prefix.c_str()) - .submit(); - - $1 - - return 0; -} -EOF -} - -MCPP="${MCPP:-mcpp}" - -# ── A fixed rerun_if_changed path inside the prepare directory ───────────── -emit_build_mcpp 'mcpp::rerun_if_changed((prefix + "/include/greet.h").c_str());' -rm -rf target -"$MCPP" build > b1.log 2>&1 || { cat b1.log; echo "FAIL: build with rerun_if_changed inside the prepare dir failed"; exit 1; } -grep -qi "prepare" b1.log \ - || { cat b1.log; echo "FAIL: no warning mentions the prepare action"; exit 1; } -grep -qF "include/greet.h" b1.log \ - || { cat b1.log; echo "FAIL: the warning does not name the re-run path"; exit 1; } -grep -qF "prep-install" b1.log \ - || { cat b1.log; echo "FAIL: the warning does not name the prepare directory"; exit 1; } -grep -qF "prep" b1.log \ - || { cat b1.log; echo "FAIL: the warning does not name the declaring package"; exit 1; } - -# ── A glob pattern reaching into the same directory ───────────────────────── -emit_build_mcpp 'mcpp::rerun_if_changed_glob((prefix + "/include/*.h").c_str());' -rm -rf target -"$MCPP" build > b2.log 2>&1 || { cat b2.log; echo "FAIL: build with rerun_if_changed_glob inside the prepare dir failed"; exit 1; } -grep -qi "prepare" b2.log \ - || { cat b2.log; echo "FAIL: no warning mentions the prepare action (glob case)"; exit 1; } -grep -qF "prep-install" b2.log \ - || { cat b2.log; echo "FAIL: the glob warning does not name the prepare directory"; exit 1; } - -# ── THE CONTROL: a re-run input OUTSIDE the prepare directory is silent ──── -# Without this half, any warning at all -- for any reason -- would pass the -# assertions above. -emit_build_mcpp 'mcpp::rerun_if_changed((root + "/install.sh").c_str());' -rm -rf target -"$MCPP" build > b3.log 2>&1 || { cat b3.log; echo "FAIL: control build failed"; exit 1; } -if grep -qi "prepare" b3.log; then - cat b3.log - echo "FAIL: a re-run input OUTSIDE any prepare directory was warned about" - exit 1 -fi - -echo "PASS: 792_a_rerun_input_inside_a_prepare_directory_is_warned" diff --git a/tests/e2e/793_an_unknown_action_role_is_refused.sh b/tests/e2e/793_an_unknown_action_role_is_refused.sh deleted file mode 100644 index 8c22c06fe..000000000 --- a/tests/e2e/793_an_unknown_action_role_is_refused.sh +++ /dev/null @@ -1,101 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 793_an_unknown_action_role_is_refused.sh — SPEC-007 R3.6 (design §5.4 P, -# mcpp#702): an action role that names none of the five mcpp knows is -# refused, naming the value and the list. Before this, `decode_action` mapped -# any unrecognised string to `source` silently, so a misspelt role changed -# an action's meaning without a word (a "prepare" typo'd "prepar" quietly -# became a source-generating action instead of construction). -# -# `a.role` is spelled as a bare string here, on purpose: the typed API's -# `mcpp::roles` constants exist so this exact mistake fails to COMPILE -# instead, and this test is about the OTHER half of R3.6 -- the wire-level -# refusal that still has to hold for a hand-written frozen-surface program, -# which has no constants to misuse in the first place. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -mkdir -p "$TMP/app/src" -cd "$TMP/app" - -cat > mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" -EOF -printf '#include \nint main(){ std::printf("ok\\n"); }\n' > src/main.cpp - -cat > build.mcpp <<'EOF' -import mcpp; -#include -int main() { - mcpp::action a; - a.id = "typo"; - a.role = "prepar"; // typo of "prepare" -- not one of the five roles - a.arg("true") - .output((std::string(mcpp::out_dir()) + "/typo.stamp").c_str()) - .submit(); - return 0; -} -EOF - -MCPP="${MCPP:-mcpp}" -if "$MCPP" build > b.log 2>&1; then - cat b.log - echo "FAIL: an action with an unrecognised role let the build succeed" - exit 1 -fi - -grep -qF '"prepar"' b.log \ - || { cat b.log; echo "FAIL: the refusal does not name the misspelt role"; exit 1; } -for role in source check object artifact prepare; do - grep -qF "$role" b.log \ - || { cat b.log; echo "FAIL: the refusal does not list role '$role'"; exit 1; } -done - -# THE CONTROL: each of the five real roles is accepted (well-formed -# otherwise), so this is a property of the STRING, not of `action` as a -# whole rejecting everything. `object`'s command produces a REAL object file -# (a link input) rather than a plain stamp, so this control does not fail the -# LINK for an unrelated reason and misread that as the role being refused. -echo 'int ctrl_unused_symbol(void) { return 0; }' > extra.c - -for role in source check artifact; do - cat > build.mcpp < -int main() { - mcpp::action a; - a.id = "ctrl"; - a.role = "$role"; - a.arg("true") - .output((std::string(mcpp::out_dir()) + "/ctrl.stamp").c_str()) - .submit(); - return 0; -} -EOF - rm -rf target - "$MCPP" build > c.log 2>&1 || { cat c.log; echo "FAIL: a well-known role '$role' was refused"; exit 1; } -done - -cat > build.mcpp <<'EOF' -import mcpp; -#include -int main() { - const std::string root = mcpp::manifest_dir(); - const std::string obj = std::string(mcpp::out_dir()) + "/extra.o"; - mcpp::action a; - a.id = "ctrl-object"; - a.role = "object"; - a.arg("gcc").arg("-c").arg((root + "/extra.c").c_str()).arg("-o").arg(obj.c_str()) - .input((root + "/extra.c").c_str()) - .output(obj.c_str()) - .submit(); - return 0; -} -EOF -rm -rf target -"$MCPP" build > c.log 2>&1 || { cat c.log; echo "FAIL: the well-known role 'object' was refused"; exit 1; } - -echo "PASS: 793_an_unknown_action_role_is_refused" diff --git a/tests/e2e/798_a_rule_applies_through_a_source_it_claims.sh b/tests/e2e/798_a_rule_applies_through_a_source_it_claims.sh deleted file mode 100755 index 45187c8ea..000000000 --- a/tests/e2e/798_a_rule_applies_through_a_source_it_claims.sh +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 798_a_rule_applies_through_a_source_it_claims.sh — mcpp#715. -# -# A feature activates a rule for a consumer; whether the rule has anything to -# do there is answered by the consumer's sources. Until 2026.9.27.1 the engine -# synthesised a build program for every active rule, so a package that enabled -# a rule feature only to import its module -- no device source, no -# `build.mcpp` -- compiled and ran a program that could only report "nothing -# to do", and printed `Rules` for it, on every configure. -# -# Two legs over examples/12's rule package, which is the engine's own -# reference rule and needs no network: -# 1. a consumer with a `.toy` device source and no `build.mcpp`: the rule is -# reported, the synthesised program runs it, and the kernel reaches the -# program (the direction the fix must not break); -# 2. a consumer that activates the same feature with no `.toy` source: no -# `Rules` line and no synthesised program. -set -e - -SRC="$(cd "$(dirname "$0")/../.." && pwd)/examples/12-a-new-device-language" -[[ -d "$SRC" ]] || { echo "FAIL: $SRC is missing"; exit 1; } - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cp -r "$SRC" "$TMP/ex" -find "$TMP/ex" -maxdepth 3 -type d -name target -exec rm -rf {} + 2>/dev/null || true -find "$TMP/ex" -maxdepth 3 -name mcpp.lock -delete 2>/dev/null || true - -# ── 1. a claimed source: the rule runs through the synthesised program ────── -cd "$TMP/ex/app" -rm -f build.mcpp -"$MCPP" build > b1.log 2>&1 || { cat b1.log; echo "FAIL: leg 1 did not build"; exit 1; } -grep -q 'Rules.*example.rules.toy' b1.log || { - cat b1.log; echo "FAIL: leg 1 did not report the rule it applied"; exit 1; } -find target -name build.mcpp | grep -q . || { - echo "FAIL: leg 1 synthesised no build program"; exit 1; } -out="$("$MCPP" run 2>&1 | tail -1)" -[[ "$out" == *"= 42"* ]] || { - echo "FAIL: leg 1: the kernel did not reach the program: '$out'"; exit 1; } - -# ── 2. no claimed source: nothing is synthesised ─────────────────────────── -mkdir -p "$TMP/ex/importonly/src" -cd "$TMP/ex/importonly" -cat > mcpp.toml <<'EOF' -[package] -name = "importonly" -version = "0.1.0" - -[language] -standard = "c++23" - -[dependencies] -example.rules-toy = { path = "../rules-toy", features = ["rules-toy"] } - -[build] -sources = ["src/*.cpp"] -EOF -cat > src/main.cpp <<'EOF' -#include -int main() { std::printf("IMPORT_ONLY_OK\n"); } -EOF -"$MCPP" build > b2.log 2>&1 || { cat b2.log; echo "FAIL: leg 2 did not build"; exit 1; } -if grep -q 'Rules' b2.log; then - cat b2.log; echo "FAIL: leg 2 reported a rule that claims none of its sources"; exit 1 -fi -if find target -name build.mcpp | grep -q .; then - find target -name build.mcpp - echo "FAIL: leg 2 synthesised a build program with nothing to do"; exit 1 -fi -out="$("$MCPP" run 2>&1 | tail -1)" -[[ "$out" == "IMPORT_ONLY_OK" ]] || { echo "FAIL: leg 2 program: '$out'"; exit 1; } - -echo "OK" diff --git a/tests/e2e/799_an_action_runs_with_its_env_and_cwd.sh b/tests/e2e/799_an_action_runs_with_its_env_and_cwd.sh deleted file mode 100755 index 168020537..000000000 --- a/tests/e2e/799_an_action_runs_with_its_env_and_cwd.sh +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/env bash -# 799_an_action_runs_with_its_env_and_cwd.sh — mcpp#708, protocol 13. -# -# An action's command is an argv with no shell assumed (SPEC-007 R3.1), so a -# generator configured through environment variables, or one that must run in -# a particular directory, had no portable way to be declared: `NAME=value cmd` -# and `cd dir &&` are shell syntax. `mcpp::action::env(name, value)` and -# `mcpp::action::cwd(dir)` state both, and the engine's wrapper applies them. -# -# Criteria: -# A. the command sees the variable, and runs in the package-relative -# directory `cwd` names; -# B. the declared output lands where it was declared, not under `cwd`; -# C. changing the variable's value re-runs the action; -# D. a directory the engine prepends to PATH (`--path-prepend`, which every -# action of an MSVC-ABI build receives since 2026.9.28.2, the 2026-09-28 -# design D3) goes before the PATH the action declares with `env`, and -# before the inherited PATH when it declares none. The graph half, that -# the flag is emitted, is unit-tested (NinjaBackendPeRuntime); this is -# the wrapper's half, where it meets R3.8. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" -mkdir -p src tools - -cat > mcpp.toml <<'EOF' -[package] -name = "envcwd" -version = "0.1.0" -EOF -cat > src/main.cpp <<'EOF' -#include -int main() { std::printf("ENV_CWD_OK\n"); } -EOF - -write_program() { # $1 = the value of GREETING - cat > build.mcpp < -import mcpp; -int main() { - const std::string out = std::string(mcpp::out_dir()) + "/probe.txt"; - mcpp::action a; - a.id = "probe"; - a.role = mcpp::roles::check; - a.env("GREETING", "$1") - .cwd("tools") - .arg("sh").arg("-c") - .arg("printf '%s\\\\n' \"\$GREETING\" > \"\$1\"; pwd -P >> \"\$1\"") - .arg("sh").arg(out.c_str()) - .output(out.c_str()) - .submit(); -} -EOF -} - -probe() { find target -name probe.txt | head -1; } - -write_program hello -"$MCPP" build > b1.log 2>&1 || { cat b1.log; echo "FAIL: build failed"; exit 1; } -p="$(probe)" -[[ -n "$p" ]] || { cat b1.log; echo "FAIL: B: the declared output was not written"; exit 1; } -[[ "$(sed -n 1p "$p")" == "hello" ]] || { - cat "$p"; echo "FAIL: A: the command did not see GREETING"; exit 1; } -want="$(cd tools && pwd -P)" -[[ "$(sed -n 2p "$p")" == "$want" ]] || { - cat "$p"; echo "FAIL: A: the command did not run in $want"; exit 1; } -[[ ! -e tools/probe.txt ]] || { echo "FAIL: B: the output landed under cwd"; exit 1; } -echo "ok: A, B" - -write_program goodbye -"$MCPP" build > b2.log 2>&1 || { cat b2.log; echo "FAIL: rebuild failed"; exit 1; } -[[ "$(sed -n 1p "$(probe)")" == "goodbye" ]] || { - cat "$(probe)"; echo "FAIL: C: a changed value did not re-run the action"; exit 1; } -echo "ok: C" - -case "$(uname -s)" in - MINGW* | MSYS* | CYGWIN*) - # The separator is ';' there, and an msys shell rewrites PATH on entry. - echo "ok: D not read on this host; the graph half is unit-tested" ;; - *) - "$MCPP" __action --env PATH=/declared/bin --path-prepend /first/bin \ - -- /bin/sh -c 'printf "%s" "$PATH" > "$1"' sh "$TMP/d1.txt" \ - || { echo "FAIL: D: the wrapper failed"; exit 1; } - [[ "$(cat "$TMP/d1.txt")" == "/first/bin:/declared/bin" ]] || { - echo "FAIL: D: with a declared PATH the command saw '$(cat "$TMP/d1.txt")'"; exit 1; } - "$MCPP" __action --path-prepend /first/bin \ - -- /bin/sh -c 'printf "%s" "$PATH" > "$1"' sh "$TMP/d2.txt" \ - || { echo "FAIL: D: the wrapper failed"; exit 1; } - [[ "$(cat "$TMP/d2.txt")" == "/first/bin:$PATH" ]] || { - echo "FAIL: D: without a declared PATH the command saw '$(cat "$TMP/d2.txt")'"; exit 1; } - echo "ok: D" ;; -esac - -echo "PASS: 799_an_action_runs_with_its_env_and_cwd" diff --git a/tests/e2e/808_device_source_is_not_a_compile_unit.sh b/tests/e2e/808_device_source_is_not_a_compile_unit.sh deleted file mode 100755 index 9f2511778..000000000 --- a/tests/e2e/808_device_source_is_not_a_compile_unit.sh +++ /dev/null @@ -1,87 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 808 -- a rule-claimed device source is not a C++ compile unit of the plan -# (mcpp#724 §1, design 2026-09-27 §4.1). -# -# `SourceKind::Device` names a file the engine has no compile rule for: it is -# compiled, if at all, by the package's build program through an action, never -# by a `cxx_object` edge. Until now the plan turned every graph unit into a -# `CompileUnit`, device units included, so `build.ninja` carried a dead -# `cxx_object` edge for the device source and both `compile_commands.json` and -# the S1 document (`emit build-database`) listed it with a compiler command -# that never ran on the file. Criteria, on examples/12's own fixture (a `.toy` -# kernel a rule claims and turns into generated C++): -# A. `build.ninja` has no `cxx_object` edge whose input is the device source. -# B. `compile_commands.json` lists no entry for it. -# C. The S1 document (`emit build-database --format json`) lists no unit for -# it, in any set. -# D. `--spec compile-commands` agrees with B. -# E. The build still succeeds and the kernel still reaches the program: the -# fix must not stop the device source from being compiled BY THE RULE. -set -e -# The project-root copy of the compile database is opt-in since -# 2026.10.26.1 ([build] compile_commands); this test reads it. -export MCPP_COMPILE_COMMANDS=compile_commands.json - -SRC="$(cd "$(dirname "$0")/../.." && pwd)/examples/12-a-new-device-language" -[[ -d "$SRC" ]] || { echo "FAIL: $SRC is missing"; exit 1; } - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cp -r "$SRC" "$TMP/ex" -find "$TMP/ex" -maxdepth 3 -type d -name target -exec rm -rf {} + 2>/dev/null || true -find "$TMP/ex" -maxdepth 3 -name mcpp.lock -delete 2>/dev/null || true - -cd "$TMP/ex/app" - -# ── A, B: build.ninja and compile_commands.json ───────────────────────────── -"$MCPP" build --configure-only > configure.log 2>&1 \ - || { cat configure.log; echo "FAIL: configure-only did not succeed"; exit 1; } - -ninja_file="$(find target -name build.ninja -not -path '*/.build-mcpp/*' | head -1)" -[[ -n "$ninja_file" ]] || { echo "FAIL: no build.ninja generated"; exit 1; } - -if grep -qE '^build [^:]*: cxx_object [^|]*answer\.toy' "$ninja_file"; then - grep -nE '^build [^:]*: cxx_object [^|]*answer\.toy' "$ninja_file" - echo "FAIL: A: build.ninja carries a cxx_object edge for the device source" - exit 1 -fi -echo "ok: A, no cxx_object edge for the device source" - -[[ -f compile_commands.json ]] || { echo "FAIL: no compile_commands.json"; exit 1; } -if grep -q 'answer\.toy' compile_commands.json; then - echo "FAIL: B: compile_commands.json lists the device source" - exit 1 -fi -echo "ok: B, compile_commands.json lists no entry for the device source" - -# ── C: the S1 document ─────────────────────────────────────────────────── -"$MCPP" emit build-database --format json > s1.json 2> s1.err \ - || { cat s1.err; echo "FAIL: emit build-database failed"; exit 1; } -python3 - s1.json <<'EOF' || { echo "FAIL: C"; exit 1; } -import json, sys -db = json.load(open(sys.argv[1]))["data"]["database"] -for s in db["sets"]: - for u in s["translation-units"]: - if u["source"].endswith("answer.toy"): - print("device source listed in set", s["name"], u["source"]) - sys.exit(1) -EOF -echo "ok: C, the S1 document lists no unit for the device source" - -# ── D: --spec compile-commands ─────────────────────────────────────────── -"$MCPP" emit build-database --spec compile-commands > cc.json 2> cc.err \ - || { cat cc.err; echo "FAIL: emit --spec compile-commands failed"; exit 1; } -if grep -q 'answer\.toy' cc.json; then - echo "FAIL: D: --spec compile-commands lists the device source" - exit 1 -fi -echo "ok: D, --spec compile-commands agrees" - -# ── E: the fix must not stop the rule from compiling the kernel ──────────── -"$MCPP" build > build.log 2>&1 || { cat build.log; echo "FAIL: E: the build failed"; exit 1; } -out="$("$MCPP" run 2>&1 | tail -1)" -[[ "$out" == *"= 42"* ]] || { echo "FAIL: E: the kernel did not reach the program: '$out'"; exit 1; } -echo "ok: E, the device source is still compiled by the rule and the build still runs" - -echo "PASS: 808 a device source is not a compile unit" diff --git a/tests/e2e/827_many_placements_run_as_one_process.sh b/tests/e2e/827_many_placements_run_as_one_process.sh deleted file mode 100755 index d9fccbd41..000000000 --- a/tests/e2e/827_many_placements_run_as_one_process.sh +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env bash -# requires: unix-shell -# 827_many_placements_run_as_one_process.sh -- #734 E4. -# -# The files a program's runtime needs beside it are placed by one process, not -# one per file: a first Windows build spent 4.5 s on 1270 per-file placements, -# against 0.5 s for one copying process. `mcpp stage --list` reads the -# `\t` pairs of a list the plan writes, and keeps the -# single-file semantics for each destination. -# -# S1 50 deploy entries produce one `stage_list` edge and no `stage_file` -# edge, and every file arrives; -# S2 a build with nothing changed runs no placement; -# S3 changing one source rewrites its destination only: every other -# destination keeps its time stamp; -# S4 one deploy entry keeps the per-file edge, byte for byte. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } -MCPP="${MCPP:-mcpp}" - -mkdir -p p/src p/data -cd p -for i in $(seq 1 50); do echo "$i" > data/f$i.txt; done -printf 'int main() { return 0; }\n' > src/main.cpp -printf '[package]\nname = "place827"\nversion = "0.1.0"\n' > mcpp.toml -program() { # $1 = how many entries - cat > build.mcpp < s1.log 2>&1 || fail "S1: the build failed" s1.log -NINJA=$(find target -name build.ninja -not -path '*/.build-mcpp/*' | head -1) -[ "$(grep -c ': stage_list ' "$NINJA")" = 1 ] || fail "S1: not exactly one stage_list edge" "$NINJA" -grep -q ': stage_file .*data' "$NINJA" && fail "S1: a per-file placement edge remains" "$NINJA" -OUT=$(dirname "$NINJA") -[ "$(ls "$OUT"/bin/data 2>/dev/null | wc -l)" -eq 50 ] || [ "$(find "$OUT" -path '*data/f*.txt' | wc -l)" -ge 50 ] \ - || fail "S1: not every file was placed" s1.log -DEST1=$(find "$OUT" -path '*/data/f1.txt' | head -1) -DEST2=$(find "$OUT" -path '*/data/f2.txt' | head -1) -[ -n "$DEST1" ] && [ -n "$DEST2" ] || fail "S1: the placed files are not found under $OUT" s1.log - -# S2 -before=$(grep -c "placements\|data/f" "$OUT/.ninja_log" || true) -touch src/main.cpp -"$MCPP" build > s2.log 2>&1 || fail "S2: the build failed" s2.log -after=$(grep -c "placements\|data/f" "$OUT/.ninja_log" || true) -[ "$before" = "$after" ] || fail "S2: a build with no placement change ran the placement edge" "$OUT/.ninja_log" - -# S3 -touch -r "$DEST2" s3.ref # a reference file, since the stat options differ between GNU and BSD -sleep 1.1 -echo changed > data/f1.txt -touch src/main.cpp -"$MCPP" build > s3.log 2>&1 || fail "S3: the build failed" s3.log -grep -qx changed "$DEST1" || fail "S3: the changed source did not arrive" "$DEST1" -[ -z "$(find "$DEST2" -newer s3.ref)" ] || fail "S3: an unchanged destination was rewritten" s3.log - -# S4 -program 1 -"$MCPP" build > s4.log 2>&1 || fail "S4: the build failed" s4.log -NINJA=$(find target -name build.ninja -not -path '*/.build-mcpp/*' | head -1) -grep -q ': stage_list ' "$NINJA" && fail "S4: one entry produced a list edge" "$NINJA" -grep -q ': stage_file .*f1.txt' "$NINJA" || fail "S4: the single entry lost its per-file edge" "$NINJA" - -echo "OK" diff --git a/tests/e2e/896_an_action_output_has_one_writer.sh b/tests/e2e/896_an_action_output_has_one_writer.sh deleted file mode 100755 index 747943671..000000000 --- a/tests/e2e/896_an_action_output_has_one_writer.sh +++ /dev/null @@ -1,83 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc unix-shell -# 896 -- an action's output is written by the action and by nothing else -# (D6, design 2026-10-10 §4; #778). -# -# Prepare used to write an empty placeholder for every generated translation -# unit, so the scan had a file to read. The placeholder was newer than the -# action's inputs: whenever a ninja log already held the action, ninja took -# the placeholder for the generated file, compiled it empty, and the link -# failed with `undefined reference to resource()` (#778). -# -# O1 `--configure-only` leaves no file where the action will write. -# O2 A generated file removed after a build is generated again, not -# replaced by an empty one. -# O3 A generated file that exists (here a legitimately empty one written by -# the action) is not rewritten by prepare. -set -e - -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -export MCPP_HOME="$TMP/mcpp-home" -source "$(dirname "$0")/_inherit_toolchain.sh" -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } - -mkdir -p "$TMP/ws/app" && cd "$TMP/ws" -printf '[workspace]\nmembers = ["app"]\n' > mcpp.toml -cat > app/mcpp.toml <<'TOML' -[package] -name = "app" -version = "0.0.1" -[build] -sources = [] -[targets.app] -kind = "bin" -main = "main.cpp" -TOML -printf 'extern const char* resource();\nint main() { return resource()[0] == %sR%s ? 0 : 1; }\n' "'" "'" > app/main.cpp -printf '#!/bin/sh\nprintf %sconst char* resource() { return "RESOURCE"; }\\n%s > "$1"\nprintf "" > "$2"\n' "'" "'" > app/gen.sh -cat > app/build.mcpp <<'CPP' -import std; -import mcpp; -int main() { - const auto root = std::filesystem::path(mcpp::manifest_dir()); - const auto out = std::filesystem::path(mcpp::out_dir()); - const auto gen = (root / "gen.sh").generic_string(); - const auto cpp = (out / "generated.cpp").generic_string(); - const auto empty = (out / "empty.cpp").generic_string(); - mcpp::action a; - a.id = "generate:resource"; - a.role = mcpp::roles::source; - a.arg("sh").arg(gen.c_str()).arg(cpp.c_str()).arg(empty.c_str()) - .input(gen.c_str()).output(cpp.c_str()).output(empty.c_str()).submit(); - return 0; -} -CPP - -# ── O1 ──────────────────────────────────────────────────────────────────── -"$MCPP" build -p app --configure-only > configure.log 2>&1 || fail "configure-only" configure.log -if find . -name generated.cpp | grep -q .; then - fail "prepare wrote the action's output: $(find . -name generated.cpp)" configure.log -fi -echo "ok O1" - -# ── O2 ──────────────────────────────────────────────────────────────────── -"$MCPP" build -p app > b1.log 2>&1 || fail "first build" b1.log -gen=$(find . -path '*/out/*' -name generated.cpp | head -1) -[ -s "$gen" ] || fail "the action did not write its output" b1.log -rm -f "$gen" -"$MCPP" build -p app > b2.log 2>&1 || fail "the build after the generated file was removed failed (#778)" b2.log -[ -s "$gen" ] || fail "the generated file was not generated again" b2.log -echo "ok O2" - -# ── O3 ──────────────────────────────────────────────────────────────────── -empty=$(find . -path '*/out/*' -name empty.cpp | head -1) -[ -f "$empty" ] && [ ! -s "$empty" ] || fail "the action's empty output is missing or not empty" b2.log -before=$(python3 -c 'import os,sys; print(os.stat(sys.argv[1]).st_mtime_ns)' "$empty") -sleep 1 -"$MCPP" build -p app > b3.log 2>&1 || fail "the third build" b3.log -after=$(python3 -c 'import os,sys; print(os.stat(sys.argv[1]).st_mtime_ns)' "$empty") -[ "$before" = "$after" ] || fail "an existing output was rewritten by prepare" b3.log -echo "ok O3" - -echo "PASS: 896_an_action_output_has_one_writer" diff --git a/tests/e2e/897_each_configuration_has_its_own_generated_output.sh b/tests/e2e/897_each_configuration_has_its_own_generated_output.sh deleted file mode 100755 index 5899f3743..000000000 --- a/tests/e2e/897_each_configuration_has_its_own_generated_output.sh +++ /dev/null @@ -1,91 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc unix-shell -# 897 -- `mcpp::out_dir()` is per configuration (D7, D25; design 2026-10-10 -# §4.2), and `resolution.json` says where it is. -# -# One `target/.build-mcpp/out` used to serve every configuration. A generator -# whose output depends on `mcpp::profile()` wrote one `gen.cpp`; each -# configuration's ninja log believed it current, so a dev build after a -# release build ran with the release text (R2: `GEN=release`). -# -# G1 dev, release, dev: each binary prints its own profile. -# G2 the two configurations' `outDir` (resolution.json, graph.packages) -# differ, exist, and hold the generated file. -set -e - -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -export MCPP_HOME="$TMP/mcpp-home" -source "$(dirname "$0")/_inherit_toolchain.sh" -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } - -mkdir -p "$TMP/gp/src" && cd "$TMP/gp" -cat > mcpp.toml <<'TOML' -[package] -name = "gp" -version = "0.1.0" -[build] -sources = ["src/main.cpp"] -[targets.gp] -kind = "bin" -main = "src/main.cpp" -TOML -cat > src/main.cpp <<'CPP' -#include -const char* gen_profile(); -int main() { std::printf("GEN=%s\n", gen_profile()); } -CPP -printf '#!/bin/sh\nprintf %sconst char* gen_profile() { return "%%s"; }\\n%s "$1" > "$2"\n' "'" "'" > gen.sh -cat > build.mcpp <<'CPP' -import std; -import mcpp; -int main() { - const auto root = std::filesystem::path(mcpp::manifest_dir()); - const auto out = (std::filesystem::path(mcpp::out_dir()) / "gen.cpp").generic_string(); - const auto gen = (root / "gen.sh").generic_string(); - mcpp::action a; - a.id = "gen"; - a.role = mcpp::roles::source; - a.arg("sh").arg(gen.c_str()).arg(mcpp::profile()).arg(out.c_str()) - .input(gen.c_str()).output(out.c_str()).submit(); - return 0; -} -CPP - -outdir() { # $1 = a configuration directory: the root package's outDir - python3 - "$1/resolution.json" <<'PY' -import json, sys -for p in json.load(open(sys.argv[1]))["graph"]["packages"]: - if p.get("root"): - print(p.get("outDir", "")); break -PY -} - -for p in dev release dev; do - if [ "$p" = release ]; then "$MCPP" build --release > "b-$p.log" 2>&1 || fail "$p" "b-$p.log" - else "$MCPP" build > "b-$p.log" 2>&1 || fail "$p" "b-$p.log"; fi -done - -# Each configuration directory: what its binary prints, and what its outDir's -# generated file says. They must agree, and the two must be dev and release. -seen="" -for r in $(find target -name resolution.json -not -path '*/.build-mcpp/*'); do - d=$(dirname "$r") - out=$(outdir "$d") - [ -n "$out" ] || fail "resolution.json records no outDir: $r" - [ -s "$out/gen.cpp" ] || fail "outDir $out does not hold the generated file" - text=$(grep -o '"[a-z]*"' "$out/gen.cpp" | tr -d '"') - got=$("$d/bin/gp" | tr -d '\r') - [ "$got" = "GEN=$text" ] || fail "the binary in $d prints '$got' and its outDir was generated for '$text'" - seen="$seen $text:$out" -done -case "$seen" in - *dev:*release:* | *release:*dev:*) ;; - *) fail "expected one dev and one release configuration, saw:$seen" ;; -esac -dev=$(echo "$seen" | tr ' ' '\n' | grep '^dev:' | cut -d: -f2-) -rel=$(echo "$seen" | tr ' ' '\n' | grep '^release:' | cut -d: -f2-) -[ "$dev" != "$rel" ] || fail "dev and release share one out_dir: $dev" -echo "ok G1 G2" - -echo "PASS: 897_each_configuration_has_its_own_generated_output" diff --git a/tests/e2e/982_an_action_reads_the_builds_contract.sh b/tests/e2e/982_an_action_reads_the_builds_contract.sh deleted file mode 100755 index 001b4a4a5..000000000 --- a/tests/e2e/982_an_action_reads_the_builds_contract.sh +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env bash -# requires: elf -# Design 2026-10-29 §8.1: a build program's action names `${mcpp.contract}`, -# the configuration's contract (`mcpp-contract.json`, beside its compile -# database): the compiler of each language with the words every unit is -# compiled with, and the link driver, its words and the archiver. The action -# reads it while ninja runs; nothing calls mcpp back. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" -mkdir -p src -cat > mcpp.toml <<'EOF' -[package] -name = "contract" -version = "0.1.0" -EOF -printf 'int main() { return 0; }\n' > src/main.cpp -cat > build.mcpp <<'EOF' -#include -import mcpp; -int main() { - mcpp::link_flag("-Wl,--defsym,mcpp_contract_consumer=1"); - const std::string out = std::string(mcpp::out_dir()) + "/seen-contract.json"; - mcpp::action a; - a.id = "copy-contract"; - a.role = mcpp::roles::check; - a.arg("cp").arg("${mcpp.contract}").arg(out.c_str()) - .output(out.c_str()) - .submit(); -} -EOF - -"$MCPP" build > b.log 2>&1 || { cat b.log; echo "FAIL: build failed"; exit 1; } -seen=$(find target -name seen-contract.json | head -1) -[[ -n "$seen" ]] || { cat b.log; echo "FAIL: the action did not run"; exit 1; } -python3 - "$seen" <<'PY' -import json, os, sys -d = json.load(open(sys.argv[1])) -assert d["schema"] == 1, d -assert d["target"], d -for lang in ("c", "cxx", "asm"): - t = d["compile"][lang] - assert os.path.isabs(t["program"]) and isinstance(t["args"], list), (lang, t) -cxx = d["compile"]["cxx"]["args"] -assert any(a.startswith("-O") for a in cxx), cxx -assert not any(a.startswith("-std=") or a == "-fmodules" or a.startswith("-fmodule-") for a in cxx), cxx -assert os.path.isabs(d["link"]["program"]) and isinstance(d["link"]["args"], list), d["link"] -assert "archiver" in d["link"], d["link"] -assert not any("mcpp_contract_consumer" in a for a in d["link"]["args"]), d["link"] -print("contract:", d["target"], d["dialect"], d["compile"]["cxx"]["program"]) -PY -cfg=$(dirname "$(find target -name compile_commands.json -not -path '*/.build-mcpp/*' | head -1)") -[[ -f "$cfg/mcpp-contract.json" ]] || { echo "FAIL: no contract beside the compile database"; exit 1; } -grep -q 'mcpp_contract_consumer=1' "$cfg/build.ninja" || { - echo "FAIL: excluding package link inputs from the contract removed them from the consumer"; exit 1; -} -echo "PASS: 982" diff --git a/tests/e2e/MIGRATION.tsv b/tests/e2e/MIGRATION.tsv index 55181b2a9..eb8c67a48 100644 --- a/tests/e2e/MIGRATION.tsv +++ b/tests/e2e/MIGRATION.tsv @@ -55,7 +55,7 @@ script subsystem class destination note 48_build_error_output.sh cli merge e2e/cli 49_bmi_cache_nested_custom_index.sh modules merge e2e/modules 50_package_owned_build_flags.sh build merge e2e/build -51_package_generated_files.sh actions merge e2e/actions +51_package_generated_files.sh actions merge e2e/actions::generated/51 package generated files migrated (batch 4); script removed 52_local_path_namespaced_index.sh index merge e2e/index 53_namespaced_cache_label.sh index merge e2e/index 54_package_owned_ldflags.sh build merge e2e/build @@ -115,7 +115,7 @@ script subsystem class destination note 104_asm_sources_gas.sh build merge e2e/build 105_asm_sources_nasm.sh build merge e2e/build 106_feature_gated_sources_toml.sh features merge e2e/features -107_generated_files_toml.sh actions merge e2e/actions +107_generated_files_toml.sh actions merge e2e/actions::generated/107 generated files toml migrated (batch 4); script removed 108_cfg_conditional_sources.sh features merge e2e/features 109_per_glob_flags.sh build merge e2e/build 110_build_mcpp_env_contract.sh build-program merge e2e/build-program::protocol/110 build mcpp env contract migrated (batch 2); script removed @@ -132,7 +132,7 @@ script subsystem class destination note 121_default_ns_redirect.sh index merge e2e/index 122_run_member.sh workspace merge e2e/workspace 123_same_named_main_across_dep.sh build merge e2e/build -124_gen_source_out_of_root_object_path.sh actions merge e2e/actions +124_gen_source_out_of_root_object_path.sh actions merge e2e/actions::generated/124 gen source out of root object path migrated (batch 4); script removed 125_build_mcpp_dep_dir.sh build-program merge e2e/build-program::deps/125 build mcpp dep dir migrated (batch 2); script removed 126_default_features_opt_out.sh features merge e2e/features 127_transitive_default_features_optout.sh features merge e2e/features @@ -148,7 +148,7 @@ script subsystem class destination note 138_feature_sources_gate_vs_provider.sh features merge e2e/features 139_build_program_advisory.sh build-program merge e2e/build-program::protocol/139 build program advisory migrated (batch 2); script removed 141_include_dirs_after.sh build merge e2e/build -142_generated_files_incremental.sh actions merge e2e/actions +142_generated_files_incremental.sh actions merge e2e/actions::generated/142 generated files incremental migrated (batch 4); script removed 143_build_mcpp_source_directive.sh build-program merge e2e/build-program::protocol/143 build mcpp source directive migrated (batch 2); script removed 144_build_mcpp_include_dir.sh build-program merge e2e/build-program::protocol/144 build mcpp include dir migrated (batch 2); script removed 145_root_build_mcpp_dep_dirs.sh build-program merge e2e/build-program::deps/145 root build mcpp dep dirs migrated (batch 2); script removed @@ -196,7 +196,7 @@ script subsystem class destination note 185_index_floor_degrades.sh index merge e2e/index 186_build_mcpp_protocol_and_bound.sh build-program merge e2e/build-program::protocol/186 build mcpp protocol and bound migrated (batch 2); script removed 187_dep_host_tool.sh build-program merge e2e/build-program::hosttools/187 dep host tool migrated (batch 2); script removed -188_build_actions.sh actions merge e2e/actions +188_build_actions.sh actions merge e2e/actions::declared/188 build actions migrated (batch 4); script removed 189_host_module_rules.sh build-program merge e2e/build-program::hosttools/189 host module rules migrated (batch 2); script removed 190_link_rspfile_newlines.sh build merge e2e/build 191_link_scale.sh build merge e2e/build @@ -304,11 +304,11 @@ script subsystem class destination note 310_build_dependencies.sh build-program merge e2e/build-program::deps/310 build dependencies migrated (batch 2); script removed 311_rule_build_dependencies.sh build-program merge e2e/build-program::deps/311 rule build dependencies migrated (batch 2); script removed 312_build_rules_example.sh build-program merge e2e/build-program::hosttools/312 build rules example migrated (batch 2); script removed -313_check_stamp_on_every_platform.sh actions merge e2e/actions -314_dependency_action_generated_header.sh actions merge e2e/actions -315_blocking_check_gates_compilation.sh actions merge e2e/actions +313_check_stamp_on_every_platform.sh actions merge e2e/actions::checks/313 check stamp on every platform migrated (batch 4); script removed +314_dependency_action_generated_header.sh actions merge e2e/actions::generated/314 dependency action generated header migrated (batch 4); script removed +315_blocking_check_gates_compilation.sh actions merge e2e/actions::checks/315 blocking check gates compilation migrated (batch 4); script removed 316_link_unit_with_no_inputs_is_refused.sh build merge e2e/build -317_project_build_hooks.sh actions merge e2e/actions +317_project_build_hooks.sh actions merge e2e/actions::hooks/317 project build hooks migrated (batch 4); script removed 321_workspace_inheritance.sh workspace merge e2e/workspace 322_dialect_flag_refusal.sh toolchain merge e2e/toolchain 323_post_link_record_durability.sh runtime merge e2e/runtime @@ -334,33 +334,33 @@ script subsystem class destination note 603_version_floor.sh deps merge e2e/deps::floors/603 version floor migrated (batch 3); script removed 604_offline_skips_sandbox_bootstrap.sh self merge e2e/self 605_probe_channel_and_accel_reach_the_build_program.sh build-program merge e2e/build-program::accel/605 probe channel and accel reach the build program migrated (batch 2); script removed -606_constrained_source_globs_narrow_to_the_build.sh actions merge e2e/actions -607_chained_actions_form_the_device_link.sh actions merge e2e/actions -608_object_actions_reach_a_static_library.sh actions merge e2e/actions +606_constrained_source_globs_narrow_to_the_build.sh actions merge e2e/actions::devices/606 constrained source globs narrow to the build migrated (batch 4); script removed +607_chained_actions_form_the_device_link.sh actions merge e2e/actions::devices/607 chained actions form the device link migrated (batch 4); script removed +608_object_actions_reach_a_static_library.sh actions merge e2e/actions::devices/608 object actions reach a static library migrated (batch 4); script removed 609_clean_stale.sh cache merge e2e/cache -609_shader_sources_are_device_sources.sh actions merge e2e/actions -610_feature_controlled_rule_collection.sh actions merge e2e/actions -611_variant_switch_is_not_replayed.sh actions merge e2e/actions +609_shader_sources_are_device_sources.sh actions merge e2e/actions::devices/609 shader sources are device sources migrated (batch 4); script removed +610_feature_controlled_rule_collection.sh actions merge e2e/actions::rules/610 feature controlled rule collection migrated (batch 4); script removed +611_variant_switch_is_not_replayed.sh actions merge e2e/actions::devices/611 variant switch is not replayed migrated (batch 4); script removed 612_build_program_file_input_fast_path.sh build-program merge e2e/build-program::protocol/612 build program file input fast path migrated (batch 2); script removed -613_sycl_sources_are_device_sources.sh actions merge e2e/actions +613_sycl_sources_are_device_sources.sh actions merge e2e/actions::devices/613 sycl sources are device sources migrated (batch 4); script removed 614_feature_xlings_reaches_xpkg_dir.sh index merge e2e/index 615_feature_deps_shared_library_is_linked.sh features merge e2e/features 616_examples_and_their_index_agree.sh repo sink unit/test_repo_consistency answers a parse/selection rule without a build: examples vs their index 617_build_program_knows_the_cxx_stdlib.sh build-program merge e2e/build-program::protocol/617 build program knows the cxx stdlib migrated (batch 2); script removed 618_dependency_feature_tools_reach_its_build_program.sh build-program merge e2e/build-program::deps/618 dependency feature tools reach its build program migrated (batch 2); script removed -619_a_tool_may_not_be_conditioned_on_a_layer.sh actions merge e2e/actions +619_a_tool_may_not_be_conditioned_on_a_layer.sh actions merge e2e/actions::rules/619 a tool may not be conditioned on a layer migrated (batch 4); script removed 620_link_flag_reaches_the_link_line.sh build merge e2e/build 621_exports_narrows_the_symbol_surface.sh runtime merge e2e/runtime 622_rule_declared_payload_reaches_the_consumers_build_program.sh build-program merge e2e/build-program::deps/622 rule declared payload reaches the consumers build program migrated (batch 2); script removed -623_device_sources_must_reach_an_action.sh actions merge e2e/actions +623_device_sources_must_reach_an_action.sh actions merge e2e/actions::devices/623 device sources must reach an action migrated (batch 4); script removed 624_a_missing_host_module_names_the_key.sh build-program merge e2e/build-program::hosttools/624 a missing host module names the key migrated (batch 2); script removed 625_a_payload_can_be_gated_on_the_accelerator.sh build-program merge e2e/build-program::accel/625 a payload can be gated on the accelerator migrated (batch 2); script removed 626_naming_a_subset_of_backends_is_not_a_mismatch.sh build-program merge e2e/build-program::accel/626 naming a subset of backends is not a mismatch migrated (batch 2); script removed 627_one_package_one_version.sh deps merge e2e/deps::tools/627 one package one version migrated (batch 3); script removed 628_a_pin_below_a_stated_floor_is_refused.sh deps merge e2e/deps::tools/628 a pin below a stated floor is refused migrated (batch 3); script removed 629_a_version_range_is_answered_by_xpkg_dir.sh deps merge e2e/deps::tools/629 a version range is answered by xpkg dir migrated (batch 3); script removed -630_a_rule_brings_its_own_environment.sh actions merge e2e/actions -631_action_depfile_tracks_a_runtime_discovered_include.sh actions merge e2e/actions +630_a_rule_brings_its_own_environment.sh actions merge e2e/actions::rules/630 a rule brings its own environment migrated (batch 4); script removed +631_action_depfile_tracks_a_runtime_discovered_include.sh actions merge e2e/actions::commands/631 action depfile tracks a runtime discovered include migrated (batch 4); script removed 632_a_build_program_is_told_its_package_name.sh build-program merge e2e/build-program::protocol/632 a build program is told its package name migrated (batch 2); script removed 633_host_modules_are_ordered_by_what_they_import.sh build-program merge e2e/build-program::hosttools/633 host modules are ordered by what they import migrated (batch 2); script removed 634_a_private_module_fragment_is_not_a_partition.sh modules merge e2e/modules @@ -377,7 +377,7 @@ script subsystem class destination note 645_the_fast_path_compares_the_toolchain_request.sh cache merge e2e/cache 646_runtime_deploy_places_files_in_a_directory.sh runtime merge e2e/runtime 647_abi_threads_is_one_switch_for_the_graph.sh target merge e2e/target -648_an_install_hook_sees_the_build_target.sh actions merge e2e/actions +648_an_install_hook_sees_the_build_target.sh actions merge e2e/actions::hooks/648 an install hook sees the build target migrated (batch 4); script removed 649_pack_carries_runtime_deploy_files.sh pack merge e2e/pack 650_wasm_row_names_its_launcher.sh web merge e2e/web 651_a_build_program_deploys_what_it_generated.sh build-program merge e2e/build-program::protocol/651 a build program deploys what it generated migrated (batch 2); script removed @@ -388,8 +388,8 @@ script subsystem class destination note 656_run_hands_the_distributable_to_the_runner.sh run merge e2e/run 657_a_build_program_under_the_web_row_uses_the_host_compiler.sh web merge e2e/web 658_a_program_runs_on_an_attached_android_device.sh android keep keep a physical Android device over adb -659_an_action_declaration_has_no_size_limit.sh actions merge e2e/actions -660_an_action_names_the_engine_through_mcpp_self.sh actions merge e2e/actions +659_an_action_declaration_has_no_size_limit.sh actions merge e2e/actions::commands/659 an action declaration has no size limit migrated (batch 4); script removed +660_an_action_names_the_engine_through_mcpp_self.sh actions merge e2e/actions::commands/660 an action names the engine through mcpp self migrated (batch 4); script removed 661_one_identity_two_declarations.sh deps merge e2e/deps::git/661 one identity two declarations migrated (batch 3); script removed 662_pack_stages_declared_files_before_the_closure.sh pack merge e2e/pack 663_a_graph_libcxx_over_the_payloads_c_library.sh toolchain merge e2e/toolchain @@ -466,7 +466,7 @@ script subsystem class destination note 777_c_standard_applies_to_the_package_that_declares_it.sh build merge e2e/build 778_a_graph_link_searches_no_host_directory.sh target merge e2e/target 779_a_build_program_declares_a_runtime_search_dir.sh build-program merge e2e/build-program::deps/779 a build program declares a runtime search dir migrated (batch 2); script removed -780_a_passing_check_moves_its_stamp_past_a_changed_input.sh actions merge e2e/actions +780_a_passing_check_moves_its_stamp_past_a_changed_input.sh actions merge e2e/actions::checks/780 a passing check moves its stamp past a changed input migrated (batch 4); script removed 781_deleted_root_cdb_is_restored.sh cdb merge e2e/cdb 782_foreign_root_cdb_entries_are_replaced.sh cdb merge e2e/cdb 783_cdb_switches_whole_with_the_configuration.sh cdb merge e2e/cdb @@ -476,16 +476,16 @@ script subsystem class destination note 787_emit_plans_every_member_on_its_own.sh cdb merge e2e/cdb 788_emit_defers_an_unbuilt_host_tool.sh cdb merge e2e/cdb 789_emit_describes_a_member_past_a_failed_build_program.sh cdb merge e2e/cdb -790_a_prepare_action_populates_an_unknown_directory.sh actions merge e2e/actions -791_a_prepare_action_that_creates_nothing_fails.sh actions merge e2e/actions -792_a_rerun_input_inside_a_prepare_directory_is_warned.sh actions merge e2e/actions -793_an_unknown_action_role_is_refused.sh actions merge e2e/actions +790_a_prepare_action_populates_an_unknown_directory.sh actions merge e2e/actions::prepare/790 a prepare action populates an unknown directory migrated (batch 4); script removed +791_a_prepare_action_that_creates_nothing_fails.sh actions merge e2e/actions::prepare/791 a prepare action that creates nothing fails migrated (batch 4); script removed +792_a_rerun_input_inside_a_prepare_directory_is_warned.sh actions merge e2e/actions::prepare/792 a rerun input inside a prepare directory is warned migrated (batch 4); script removed +793_an_unknown_action_role_is_refused.sh actions merge e2e/actions::prepare/793 an unknown action role is refused migrated (batch 4); script removed 794_a_windows_program_finds_a_dll_through_runtime_search_dir.sh windows merge e2e/windows 795_a_link_flag_reaches_the_linker_as_written.sh build merge e2e/build 796_runtime_search_dir_on_pe_under_wine.sh windows keep keep emulator: runs the PE program under wine 797_a_windows_program_finds_its_dlls_beside_it.sh windows keep keep emulator: runs the PE program under wine -798_a_rule_applies_through_a_source_it_claims.sh actions merge e2e/actions -799_an_action_runs_with_its_env_and_cwd.sh actions merge e2e/actions +798_a_rule_applies_through_a_source_it_claims.sh actions merge e2e/actions::rules/798 a rule applies through a source it claims migrated (batch 4); script removed +799_an_action_runs_with_its_env_and_cwd.sh actions merge e2e/actions::commands/799 an action runs with its env and cwd migrated (batch 4); script removed 800_a_feature_provides_its_host_tools.sh build-program merge e2e/build-program::hosttools/800 a feature provides its host tools migrated (batch 2); script removed 801_a_dependency_program_is_shipped_with_the_consumer.sh pack merge e2e/pack 802_a_host_build_applies_its_host_row.sh build-program merge e2e/build-program::hosttools/802 a host build applies its host row migrated (batch 2); script removed @@ -494,7 +494,7 @@ script subsystem class destination note 805_rooted_workspace_reaches_its_own_path_dependency.sh workspace merge e2e/workspace 806_dash_p_resolves_the_package_first.sh workspace merge e2e/workspace 807_a_lib_root_imports_its_own_package.sh modules merge e2e/modules -808_device_source_is_not_a_compile_unit.sh actions merge e2e/actions +808_device_source_is_not_a_compile_unit.sh actions merge e2e/actions::rules/808 device source is not a compile unit migrated (batch 4); script removed 809_failed_build_program_hides_no_device_source.sh cdb merge e2e/cdb 810_two_packages_deploy_the_same_bytes_to_one_name.sh runtime merge e2e/runtime 811_place_dlls_never_overwrites_a_declared_deploy.sh windows merge e2e/windows @@ -513,7 +513,7 @@ script subsystem class destination note 824_a_build_program_reads_the_resolved_toolchain.sh build-program merge e2e/build-program::protocol/824 a build program reads the resolved toolchain migrated (batch 2); script removed 825_a_build_program_reads_the_msvc_toolset.sh windows merge e2e/windows 826_plugin_diagnostics_features_and_names.sh build-program merge e2e/build-program::hosttools/826 plugin diagnostics features and names migrated (batch 2); script removed -827_many_placements_run_as_one_process.sh actions merge e2e/actions +827_many_placements_run_as_one_process.sh actions merge e2e/actions::outputs/827 many placements run as one process migrated (batch 4); script removed 828_a_library_states_its_interface.sh pack merge e2e/pack 829_pack_takes_a_member.sh pack merge e2e/pack 831_a_path_dependency_host_module_outside_src.sh build-program merge e2e/build-program::hosttools/831 a path dependency host module outside src migrated (batch 2); script removed @@ -581,8 +581,8 @@ script subsystem class destination note 893_the_workspace_prefix_carries_the_meaning.sh workspace merge e2e/workspace 894_the_position_reading_is_kept_and_reported.sh build merge e2e/build 895_conditional_configuration_rows_count_as_they_evaluate.sh features merge e2e/features -896_an_action_output_has_one_writer.sh actions merge e2e/actions -897_each_configuration_has_its_own_generated_output.sh actions merge e2e/actions +896_an_action_output_has_one_writer.sh actions merge e2e/actions::outputs/896 an action output has one writer migrated (batch 4); script removed +897_each_configuration_has_its_own_generated_output.sh actions merge e2e/actions::outputs/897 each configuration has its own generated output migrated (batch 4); script removed 898_module_units_in_cpp_files.sh modules merge e2e/modules 899_toolchain_flag_replacing_a_declared_toolchain_says_so.sh toolchain merge e2e/toolchain 900_publish_writes_the_workspace_layer_into_the_member.sh workspace merge e2e/workspace @@ -652,7 +652,7 @@ script subsystem class destination note 964_tests_built_once_run_from_an_archive.sh openkal merge e2e/openkal 980_an_ambiguous_dependency_name_is_no_packages.sh deps merge e2e/deps::graph/980 an ambiguous dependency name is no packages migrated (batch 3); script removed 981_a_web_link_has_the_profiles_level_and_the_wasm_exception_model.sh web merge e2e/web -982_an_action_reads_the_builds_contract.sh actions merge e2e/actions +982_an_action_reads_the_builds_contract.sh actions merge e2e/actions::outputs/982 an action reads the builds contract migrated (batch 4); script removed 983_a_pack_holds_the_legs_it_names.sh openkal merge e2e/openkal 984_a_set_of_programs_is_a_package_per_leg.sh openkal merge e2e/openkal 985_a_format_list_writes_each_container_of_one_tree.sh openkal merge e2e/openkal @@ -683,9 +683,9 @@ script subsystem class destination note 1010_the_std_module_takes_the_platforms_flags.sh toolchain merge e2e/toolchain 1011_accelerator_requests_are_complete_and_canonical.sh build-program merge e2e/build-program::accel/1011 accelerator requests are complete and canonical migrated (batch 2); script removed 1012_capabilities_bind_in_each_consumer_context.sh features merge e2e/features -1013_optional_action_progress_preserves_outputs.sh actions merge e2e/actions +1013_optional_action_progress_preserves_outputs.sh actions merge e2e/actions::progress/1013 optional action progress preserves outputs migrated (batch 4); script removed 1014_host_only_layers_do_not_select_the_target_abi.sh target merge e2e/target -1015_action_progress_paths_are_utf8_on_every_host.sh actions merge e2e/actions +1015_action_progress_paths_are_utf8_on_every_host.sh actions merge e2e/actions::progress/1015 action progress paths are utf8 on every host migrated (batch 4); script removed 1016_a_killed_build_does_not_race_the_next_one.sh cli merge e2e/cli 1017_a_system_msvc_builds_the_build_program_of_a_cross_target.sh windows merge e2e/windows 1018_a_package_and_a_namespaced_package_of_one_name_both_link.sh deps merge e2e/deps::graph/1018 a package and a namespaced package of one name both link migrated (batch 3); script removed diff --git a/tests/e2e/actions/checks.cpp b/tests/e2e/actions/checks.cpp new file mode 100644 index 000000000..b55739067 --- /dev/null +++ b/tests/e2e/actions/checks.cpp @@ -0,0 +1,130 @@ +// Check actions: the stamp the engine writes and moves, and `blocking` (formerly tests/e2e/313, +// 315, 780). +import std; +import mcpplibs.mtest; +import mcpp.test.support; + +using namespace mcpplibs::mtest; +namespace s = mcpp::test::support; +namespace fs = std::filesystem; + +namespace { + +bool failed(const e2e::outcome& r) { return r.started && r.status != 0; } + +std::optional ninja_file(const fs::path& target) { + for (const auto& f : s::find_files(target, R"(build\.ninja)")) + if (f.generic_string().find("/.build-mcpp/") == std::string::npos) return f; + return std::nullopt; +} + +E2E checks{"checks", [] { + "313 check stamp on every platform"_test = [] { + // A `role = "check"` whose command never touches its stamp, on every platform: the + // engine writes the stamp (one), and a no-op rebuild does not run the check again. + s::project p{s::files{{"mcpp.toml", "[package]\nname = \"stampprobe\"\nversion = \"0.1.0\"\n"}, + {"src/main.cpp", "#include \n#ifndef CHECK_DECLARED\n#error \"the build program never ran\"\n#endif\nint main() { std::printf(\"STAMP_PROBE_OK\\n\"); }\n"}}}; + p.write("build.mcpp", std::format(R"cpp(import std; +import mcpp; +int main() {{ + mcpp::define("CHECK_DECLARED"); + mcpp::action a; + a.id = "probe"; + a.role = "check"; + a.description = "a check whose command writes no stamp"; + // The engine itself: present on every platform this suite runs on, exits 0, + // and touches nothing. That is exactly the shape of a real analyser. + a.arg("{}").arg("--version") + .output((std::string(mcpp::out_dir()) + "/probe.stamp").c_str()) + .submit(); +}} +)cpp", fs::path(s::mcpp_path()).generic_string())); + expect(fatal(e2e::succeeded(p.mcpp({"build"})))) << "build failed"; + expect(e2e::contains(s::last_line(p.mcpp({"run"}).out), "STAMP_PROBE_OK")) << "the program did not run"; + const auto stamps = s::find_files(p.at("target"), R"(probe\.stamp)"); + expect(eq(stamps.size(), std::size_t{1})) << "expected exactly one stamp written by the engine, found " << stamps.size(); + auto b2 = p.mcpp({"build"}); + expect(fatal(e2e::succeeded(b2))) << "no-op rebuild failed"; + expect(s::lacks(s::text(b2), "CHECK")) << "the check re-ran with nothing changed — its stamp is not satisfying the edge"; + }; + + "315 blocking check gates compilation"_test[tag{"requires:gcc"}] = [] { + // `blocking = true` on a check orders every compile edge after it (its stamp joins the + // package's action phony), so a failing blocking check stops the compile; a + // non-blocking one does not. + s::needs({"gcc"}); + s::project p{s::files{{"mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n"}, + {"src/main.cpp", "#include \nint main(){ std::printf(\"ok\\n\"); }\n"}}}; + s::write_executable(p.at("check.sh"), "#!/usr/bin/env bash\nsleep 2\necho \"the check says no\" >&2\nexit 1\n"); + auto emit = [&](std::string_view blocking) { + p.write("build.mcpp", std::format(R"cpp(#include +import mcpp; +int main() {{ + const std::string root = mcpp::manifest_dir(); + mcpp::action a; + a.id = "gate"; + a.role = "check"; + a.blocking = {}; + a.arg((root + "/check.sh").c_str()) + .output("${{mcpp.out_dir}}/gate.stamp") + .submit(); +}} +)cpp", blocking)); + p.remove("target"); + }; + auto objects = [&] { return s::find_files(p.at("target"), R"(main.*\.o)"); }; + const std::string gated = R"(^build mcpp-actions-app : phony.*gate\.stamp)"; + emit("true"); + expect(fatal(failed(p.mcpp({"build"})))) << "a failing blocking check let the build succeed"; + expect(fatal(objects().empty())) << "a failing BLOCKING check did not stop the compile — the object exists"; + const auto nj = ninja_file(p.at("target")); + expect(fatal(nj.has_value())) << "no build.ninja"; + expect(s::has_line(s::read_file(*nj), gated)) << "the blocking check's stamp is not in the package's action phony"; + expect(s::count_matching(s::read_file(*nj), R"(^build [^:]*main[^:]*\.o *:.*\|\|.*mcpp-actions-app)") >= 1) << "the compile edge does not wait for the blocking check"; + emit("false"); + p.mcpp({"build"}); // still fails: the check still fails + expect(fatal(!objects().empty())) << "a NON-blocking check also stopped the compile — 'blocking' is not distinguishing anything, it is just always on"; + const auto nj2 = ninja_file(p.at("target")); + expect(fatal(nj2.has_value())) << "no build.ninja"; + expect(s::no_line(s::read_file(*nj2), gated)) << "a NON-blocking check joined the ordering phony"; + }; + + "780 a passing check moves its stamp past a changed input"_test = [] { + // A check whose command writes no stamp, after one of its inputs changed: the engine + // moves the stamp when the command passes, so the build after does not run it again. + s::project p{s::files{{"mcpp.toml", "[package]\nname = \"stampmove\"\nversion = \"0.1.0\"\n"}, + {"src/main.cpp", "#include \nint main() { std::printf(\"STAMP_MOVE_OK\\n\"); }\n"}, + {"input.txt", "one\n"}}}; + p.write("build.mcpp", std::format(R"cpp(import std; +import mcpp; +int main() {{ + mcpp::action a; + a.id = "probe"; + a.role = "check"; + a.description = "a check whose command writes no stamp"; + a.arg("{}").arg("--version") + .input("{}") + .output((std::string(mcpp::out_dir()) + "/probe.stamp").c_str()) + .submit(); +}} +)cpp", fs::path(s::mcpp_path()).generic_string(), p.at("input.txt").generic_string())); + const std::string ran = R"(__action(-stamp)? )"; + auto b1 = p.mcpp({"build", "-v"}); + expect(fatal(e2e::succeeded(b1))) << "build failed"; + expect(fatal(s::has_line(s::text(b1), ran))) << "the first build did not run the check"; + auto b2 = p.mcpp({"build", "-v"}); + expect(fatal(e2e::succeeded(b2))) << "no-op rebuild failed"; + expect(s::no_line(s::text(b2), ran)) << "the check re-ran with nothing changed"; + std::this_thread::sleep_for(std::chrono::seconds{1}); + p.write("input.txt", "two\n"); + auto b3 = p.mcpp({"build", "-v"}); + expect(fatal(e2e::succeeded(b3))) << "rebuild after the edit failed"; + expect(s::has_line(s::text(b3), ran)) << "a changed input did not re-run the check"; + auto b4 = p.mcpp({"build", "-v"}); + expect(fatal(e2e::succeeded(b4))) << "rebuild after the check failed"; + expect(s::no_line(s::text(b4), ran)) << "the check re-ran on the build after it passed -- its stamp stayed older than the input"; + expect(e2e::contains(s::last_line(p.mcpp({"run"}).out), "STAMP_MOVE_OK")) << "the program did not run"; + }; +}}; + +} // namespace diff --git a/tests/e2e/actions/commands.cpp b/tests/e2e/actions/commands.cpp new file mode 100644 index 000000000..0c9d44257 --- /dev/null +++ b/tests/e2e/actions/commands.cpp @@ -0,0 +1,284 @@ +// An action's command: a depfile it writes, no size limit on its declaration, the engine named +// through `${mcpp.self}`, its environment and directory (formerly tests/e2e/631, 659, 660, 799). +import std; +import mcpplibs.mtest; +import mcpp.test.support; + +using namespace mcpplibs::mtest; +namespace s = mcpp::test::support; +namespace fs = std::filesystem; + +namespace { + +bool failed(const e2e::outcome& r) { return r.started && r.status != 0; } + +std::optional ninja_file(const fs::path& target) { + for (const auto& f : s::find_files(target, R"(build\.ninja)")) + if (f.generic_string().find("/.build-mcpp/") == std::string::npos) return f; + return std::nullopt; +} + +long long mtime(const fs::path& p) { + return std::chrono::duration_cast(std::chrono::file_clock::to_sys(fs::last_write_time(p)).time_since_epoch()).count(); +} + +std::string mcpp_host() { return fs::absolute(s::mcpp_path()).generic_string(); } + +E2E commands{"commands", [] { + "631 action depfile tracks a runtime discovered include"_test[tag{"requires:gcc"}] = [] { + // `mcpp::action`'s `depfile`: a file the command discovers by running, named only in the + // depfile it writes, reruns the action when touched; nothing else does. + s::needs({"gcc"}); + s::project p{s::files{{"mcpp.toml", "[package]\nname = \"shaderdep\"\nversion = \"0.1.0\"\n"}, + {"src/main.cpp", "#include \nint generated_value();\nint main() { std::printf(\"VALUE=%d\\n\", generated_value()); }\n"}, + {"included.glsl", "float unused = 1.0;\n"}, + {"build.mcpp", R"cpp(#include +#include +import mcpp; +int main() { + const std::string root = mcpp::manifest_dir(); + const std::string out = std::string(mcpp::out_dir()) + "/generated.cpp"; + const std::string dep = std::string(mcpp::out_dir()) + "/generated.cpp.d"; + const std::string count = root + "/run_count.txt"; + mcpp::action a; + a.id = "genshader"; + a.role = "source"; + a.depfile = dep.c_str(); + // Deliberately NOT declared as `.input(...)`: the whole point is that + // ninja learns about it from the depfile the command writes, not from + // anything build.mcpp told the engine in advance. + a.arg((root + "/gen.sh").c_str()) + .arg((root + "/included.glsl").c_str()) + .arg(out.c_str()) + .arg(dep.c_str()) + .arg(count.c_str()) + .output(out.c_str()) + .submit(); +} +)cpp"}}}; + s::write_executable(p.at("gen.sh"), R"cpp(#!/usr/bin/env bash +n=0 +[ -f "$4" ] && n="$(cat "$4")" +n=$((n + 1)) +echo "$n" > "$4" +printf 'int generated_value() { return %s; }\n' "$n" > "$2" +# Make syntax, the shape `-MD -MF`/`--depfile` produce: OUTPUT: PREREQUISITES. +printf '%s: %s\n' "$2" "$1" > "$3" +)cpp"); + auto count = [&] { return p.exists("run_count.txt") ? s::last_line(p.read("run_count.txt")) : std::string{""}; }; + auto value = [&] { return s::last_starting(p.mcpp({"run"}).out, "VALUE="); }; + auto touch = [&] { fs::last_write_time(p.at("included.glsl"), fs::file_time_type::clock::now()); }; + expect(fatal(e2e::succeeded(p.mcpp({"build"})))) << "initial build failed"; + expect(fatal(eq(count(), std::string{"1"}))) << "the action did not run on a fresh build"; + expect(fatal(eq(value(), std::string{"VALUE=1"}))) << "unexpected initial output"; + expect(fatal(e2e::succeeded(p.mcpp({"build"})))) << "no-op rebuild failed"; + expect(fatal(eq(count(), std::string{"1"}))) << "the action reran although nothing it tracks changed"; + touch(); + expect(fatal(e2e::succeeded(p.mcpp({"build"})))) << "rebuild after touch failed"; + expect(fatal(eq(count(), std::string{"2"}))) << "touching a file the depfile named did not rerun the action"; + expect(fatal(eq(value(), std::string{"VALUE=2"}))) << "the action reran but the artifact was not regenerated"; + touch(); + expect(fatal(e2e::succeeded(p.mcpp({"build"})))) << "second touch rebuild failed"; + expect(fatal(eq(count(), std::string{"3"}))) << "the second touch did not rerun the action"; + expect(eq(value(), std::string{"VALUE=3"})) << "unexpected output after the second touch"; + }; + + "659 an action declaration has no size limit"_test = [] { + // An action's inputs, outputs and command have no declared size limit: 600 inputs and + // outputs reach the edge (and its replayed record), a 19200-byte command runs, and a + // command over the host's limit is refused naming the edge, briefly. + constexpr int n = 600; + const std::string pad = "a-directory-name-long-enough-to-make-the-edge-line-cross-the-per-argument-limit-of-the-host-os-xxxx"; + s::project app{s::files{{"mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n[targets.app]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n"}, + {"src/main.cpp", "#include \nint main() { std::puts(\"LARGE_DECLARATION_OK\"); }\n"}}}; + for (int i = 1; i <= n; ++i) app.write(std::format("in/{}/input-{}.txt", pad, i), ""); + std::string true_host; + if (!s::on_windows()) + if (auto t = s::on_path("true")) true_host = t->generic_string(); + app.write("build.mcpp", std::format(R"cpp(import std; +import mcpp; +int main() {{ + const std::string root = "{0}"; + const std::string pad = "{1}"; + mcpp::action a; + a.id = "wide"; + a.role = "source"; + a.description = "an action with {2} inputs and {2} outputs"; + a.arg("{3}").arg("--version"); + for (int i = 1; i <= {2}; ++i) {{ + a.input((root + "/in/" + pad + "/input-" + std::to_string(i) + ".txt").c_str()); + a.output((std::string(mcpp::out_dir()) + "/out/" + pad + "/output-" + std::to_string(i) + ".txt").c_str()); + }} + a.submit(); + const std::string t = "{4}"; + if (!t.empty()) {{ + mcpp::action c; + c.id = "long-command"; + c.role = "check"; + c.description = "a command past sixteen kilobytes"; + c.arg(t.c_str()); + // 300 arguments of 64 bytes: 19200 bytes of argv, past the old + // 16384-byte bound of the command list. + const std::string word(64, 'x'); + for (int i = 0; i < 300; ++i) c.arg(word.c_str()); + c.output((std::string(mcpp::out_dir()) + "/long-command.stamp").c_str()); + c.submit(); + }} +}} +)cpp", app.dir().generic_string(), pad, n, mcpp_host(), true_host)); + auto b1 = app.mcpp({"build"}); + expect(fatal(e2e::succeeded(b1))) << "a build with a wide action was refused"; + expect(fatal(e2e::contains(s::last_line(app.mcpp({"run"}).out), "LARGE_DECLARATION_OK"))) << "the program did not run"; + // `grep ': mcpp_action_' | tr -s ' ' '\n' | grep -c ` + auto count_in_edge = [&](std::string_view pattern) { + const auto nj = ninja_file(app.at("target")); + expect(fatal(nj.has_value())) << "no build.ninja"; + const std::regex re{std::string(pattern)}; + std::size_t c = 0; + for (const auto& line : s::lines_with(s::read_file(*nj), ": mcpp_action_")) { + std::istringstream words{line}; + for (std::string w; words >> w;) c += std::regex_search(w, re) ? 1 : 0; + } + return c; + }; + expect(eq(count_in_edge(R"(output-[0-9]*\.txt)"), std::size_t{n})) << "build.ninja does not name every declared output"; + expect(eq(count_in_edge(R"(input-[0-9]*\.txt)"), std::size_t{n})) << "build.ninja does not name every declared input"; + if (!true_host.empty()) + expect(eq(s::find_files(app.at("target"), R"(long-command\.stamp)").size(), std::size_t{1})) << "the long-command check did not run"; + expect(s::lacks(s::text(b1), "\"overflow\":true")) << "the overflow marker appeared"; + app.write("src/extra.cpp", "int extra() { return 1; }\n"); + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "rebuild after an unrelated source failed"; + expect(eq(count_in_edge(R"(output-[0-9]*\.txt)"), std::size_t{n})) << "after a replay build.ninja does not name every declared output"; + + s::project too_long{temp_dir() / "too-long", {{"mcpp.toml", app.read("mcpp.toml")}, {"src/main.cpp", app.read("src/main.cpp")}}}; + too_long.write("build.mcpp", std::format(R"cpp(import std; +import mcpp; +int main() {{ + mcpp::action c; + c.id = "too-long"; + c.role = "check"; + c.arg("{}"); + const std::string word(64, 'y'); + for (int i = 0; i < 2200; ++i) c.arg(word.c_str()); + c.output((std::string(mcpp::out_dir()) + "/too-long.stamp").c_str()); + c.submit(); +}} +)cpp", mcpp_host())); + auto b4 = too_long.mcpp({"build"}); + expect(fatal(failed(b4))) << "a 140800-byte literal command was accepted"; + expect(s::has_line(s::text(b4), "over the .* byte limit")) << "the refusal is not the command-length guard's"; + expect(e2e::contains(s::text(b4), "too-long.stamp")) << "the refusal does not name the edge"; + expect(lt(s::text(b4).size(), std::size_t{4000})) << "the refusal printed the whole edge line"; + }; + + "660 an action names the engine through mcpp self"_test = [] { + // `${mcpp.self}` in an action's argv is the engine's own executable, substituted before + // build.ninja, and `mcpp stage` through it is a portable copy; `${mcpp.*}` path tokens are + // spelled with forward slashes; a no-op rebuild copies nothing. + s::project p{s::files{{"mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[targets.app]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n"}, + {"src/main.cpp", "#include \nint main() { std::puts(\"SELF_COPY_OK\"); }\n"}, + {"build.mcpp", R"cpp(import std; +import mcpp; +int main() { + mcpp::action a; + a.id = "copy"; + a.role = "artifact"; + a.description = "the linked program, copied by the engine"; + a.arg("${mcpp.self}").arg("stage").arg("--verify").arg("content") + .arg("--output").arg("${mcpp.out_dir}/copied/deeper/app-copy") + .arg("${mcpp.target_file:app}") + .input("${mcpp.target_file:app}") + .output("${mcpp.out_dir}/copied/deeper/app-copy") + .submit(); +} +)cpp"}}}; + expect(fatal(e2e::succeeded(p.mcpp({"build"})))) << "build failed"; + expect(fatal(e2e::contains(s::last_line(p.mcpp({"run"}).out), "SELF_COPY_OK"))) << "the program did not run"; + const auto nj = ninja_file(p.at("target")); + expect(fatal(nj.has_value())) << "no build.ninja"; + const auto ninja = s::read_file(*nj); + expect(fatal(s::lacks(ninja, "mcpp.self"))) << "${mcpp.self} reached build.ninja unsubstituted"; + std::vector commands; + { + std::istringstream in{ninja}; + bool seen = false; + for (std::string line; std::getline(in, line);) { + if (line.starts_with("rule mcpp_action_")) { seen = true; continue; } + if (seen && line.starts_with(" command = ")) { commands.push_back(line.substr(12)); seen = false; } + } + } + expect(fatal(!commands.empty())) << "no mcpp_action_ rule in build.ninja"; + for (const auto& line : commands) { + const auto at = line.find(" -- "); + const auto argv = at == std::string::npos ? line : line.substr(at + 4); + expect(argv.find('\\') == std::string::npos) << "a ${mcpp.*} path token expanded to a native spelling:\n " << argv; + } + std::optional copy, bin; + for (const auto& f : s::find_files(p.at("target"), "app-copy")) + if (!copy && f.generic_string().ends_with("/copied/deeper/app-copy")) copy = f; + expect(fatal(copy.has_value())) << "the copy action produced nothing"; + for (const auto& f : s::find_files(p.at("target"), R"(app(\.exe)?)")) + if (!bin && f.parent_path().filename() == "bin") bin = f; + expect(fatal(bin.has_value())) << "no linked program under target/"; + expect(s::read_file(*bin) == s::read_file(*copy)) << "the copy differs from the program"; + const auto before = mtime(*copy); + std::this_thread::sleep_for(std::chrono::seconds{1}); + expect(fatal(e2e::succeeded(p.mcpp({"build"})))) << "no-op rebuild failed"; + expect(eq(mtime(*copy), before)) << "the copy was rewritten on a no-op rebuild"; + }; + + "799 an action runs with its env and cwd"_test = [] { + // `mcpp::action::env` and `::cwd`: the command sees the variable and runs in the + // directory, its declared output is not resolved against that directory, a changed + // value re-runs it; the wrapper puts a prepended PATH before a declared or inherited one. + s::project p{s::files{{"mcpp.toml", "[package]\nname = \"envcwd\"\nversion = \"0.1.0\"\n"}, + {"src/main.cpp", "#include \nint main() { std::printf(\"ENV_CWD_OK\\n\"); }\n"}}}; + fs::create_directories(p.at("tools")); + auto program = [&](std::string_view greeting) { + p.write("build.mcpp", std::format(R"cpp(#include +import mcpp; +int main() {{ + const std::string out = std::string(mcpp::out_dir()) + "/probe.txt"; + mcpp::action a; + a.id = "probe"; + a.role = mcpp::roles::check; + a.env("GREETING", "{}") + .cwd("tools") + .arg("sh").arg("-c") + .arg("printf '%s\\n' \"$GREETING\" > \"$1\"; pwd -P >> \"$1\"") + .arg("sh").arg(out.c_str()) + .output(out.c_str()) + .submit(); +}} +)cpp", greeting)); + }; + auto probe = [&] { + const auto f = s::find_file(p.at("target"), "probe.txt"); + expect(fatal(f.has_value())) << "B: the declared output was not written"; + std::vector lines; + std::istringstream in{s::read_file(*f)}; + for (std::string l; std::getline(in, l);) lines.push_back(l); + lines.resize(2); + return lines; + }; + program("hello"); + expect(fatal(e2e::succeeded(p.mcpp({"build"})))) << "build failed"; + const auto first = probe(); + expect(eq(first[0], std::string{"hello"})) << "A: the command did not see GREETING"; + expect(eq(first[1], fs::canonical(p.at("tools")).string())) << "A: the command did not run in tools"; + expect(!p.exists("tools/probe.txt")) << "B: the output landed under cwd"; + program("goodbye"); + expect(fatal(e2e::succeeded(p.mcpp({"build"})))) << "rebuild failed"; + expect(eq(probe()[0], std::string{"goodbye"})) << "C: a changed value did not re-run the action"; + if (s::on_windows()) return; // D is not read on this host; the graph half is unit-tested + const auto tmp = temp_dir(); + auto d1 = p.mcpp({"__action", "--env", "PATH=/declared/bin", "--path-prepend", "/first/bin", "--", "/bin/sh", "-c", "printf \"%s\" \"$PATH\" > \"$1\"", "sh", (tmp / "d1.txt").string()}); + expect(fatal(e2e::succeeded(d1))) << "D: the wrapper failed"; + expect(eq(s::read_file(tmp / "d1.txt"), std::string{"/first/bin:/declared/bin"})) << "D: with a declared PATH"; + auto d2 = p.mcpp({"__action", "--path-prepend", "/first/bin", "--", "/bin/sh", "-c", "printf \"%s\" \"$PATH\" > \"$1\"", "sh", (tmp / "d2.txt").string()}); + expect(fatal(e2e::succeeded(d2))) << "D: the wrapper failed"; + expect(eq(s::read_file(tmp / "d2.txt"), "/first/bin:" + s::env("PATH").value_or(""))) << "D: without a declared PATH"; + }; +}}; + +} // namespace diff --git a/tests/e2e/actions/declared.cpp b/tests/e2e/actions/declared.cpp new file mode 100644 index 000000000..76ecf8449 --- /dev/null +++ b/tests/e2e/actions/declared.cpp @@ -0,0 +1,314 @@ +// Actions a build program declares instead of doing: each role's edge, re-running only on a +// changed input, `$` in a command, target references, object actions in every image, malformed +// declarations (formerly tests/e2e/188). +import std; +import mcpplibs.mtest; +import mcpp.test.support; + +using namespace mcpplibs::mtest; +namespace s = mcpp::test::support; +namespace fs = std::filesystem; + +namespace { + +bool failed(const e2e::outcome& r) { return r.started && r.status != 0; } + +long long mtime(const fs::path& p) { + return std::chrono::duration_cast(std::chrono::file_clock::to_sys(fs::last_write_time(p)).time_since_epoch()).count(); +} + +std::optional ninja_file(const fs::path& target) { + for (const auto& f : s::find_files(target, R"(build\.ninja)")) + if (f.generic_string().find("/.build-mcpp/") == std::string::npos) return f; + return std::nullopt; +} + +E2E declared{"declared", [] { + "188 build actions"_test[tag{"requires:gcc"}] = [] { + // `mcpp:action=`: a build program declares work and the work becomes an edge: a + // source action's output is compiled, a check's stamp and an artifact appear; an action + // re-runs when its input or its command changes and not otherwise; a failing check + // fails the build; a companion header is not a translation unit; a literal `$` + // survives; an unknown target reference is refused (also beside a known one); an object + // action reaches the link, the tests and is reported when no target takes it; a + // malformed action is refused. + s::needs({"gcc"}); + const auto tmp = temp_dir(); + s::project app{tmp / "app", {{"mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n"}, + {"src/main.cpp", "#include \nint generated_value();\nint main() { std::printf(\"VALUE=%d\\n\", generated_value()); }\n"}, + {"data/value.txt", "11\n"}, + {"build.mcpp", R"cpp(#include +#include +import mcpp; +int main() { + const std::string root = mcpp::manifest_dir(); + const std::string out = std::string(mcpp::out_dir()) + "/gen.cpp"; + + mcpp::action a; + a.id = "generate"; + a.role = "source"; + a.arg((root + "/gen.sh").c_str()).arg((root + "/data/value.txt").c_str()).arg(out.c_str()) + .input((root + "/data/value.txt").c_str()) + .output(out.c_str()) + .submit(); + + mcpp::action c; + c.id = "lint"; + c.role = "check"; + c.arg((root + "/check.sh").c_str()).arg("${mcpp.out_dir}/lint.stamp") + .arg((root + "/FAIL_THE_CHECK").c_str()) + .output("${mcpp.out_dir}/lint.stamp") + .submit(); + + mcpp::action p; + p.id = "package"; + p.role = "artifact"; + p.arg((root + "/pack.sh").c_str()).arg("${mcpp.target_file:app}").arg("${mcpp.out_dir}/app.pack") + .input("${mcpp.target_file:app}") + .output("${mcpp.out_dir}/app.pack") + .submit(); +} +)cpp"}}}; + s::write_executable(app.at("gen.sh"), R"cpp(#!/usr/bin/env bash +printf 'int generated_value() { return %s; }\n' "$(cat "$1")" > "$2" +)cpp"); + s::write_executable(app.at("check.sh"), R"cpp(#!/usr/bin/env bash +# The marker path is passed in: ninja runs commands with cwd = the BUILD dir, +# not the project root, so a bare relative name would never be found. +[ -f "$2" ] && { echo "check: refusing"; exit 1; } +: > "$1" +)cpp"); + s::write_executable(app.at("pack.sh"), R"cpp(#!/usr/bin/env bash +# $1 = the linked binary, $2 = the artifact to produce +cp "$1" "$2" +)cpp"); + auto value = [&] { return s::last_starting(app.mcpp({"run"}).out, "VALUE="); }; + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "build with actions failed"; + expect(fatal(eq(value(), std::string{"VALUE=11"}))) << "generated source not linked"; + const auto pack = s::find_file(app.at("target"), "app.pack"); + expect(fatal(pack.has_value())) << "artifact action produced nothing"; + const auto outdir = pack->parent_path(); + expect(fatal(fs::exists(outdir / "lint.stamp"))) << "check action produced no stamp"; + app.write("data/value.txt", "23\n"); + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "rebuild failed"; + expect(fatal(eq(value(), std::string{"VALUE=23"}))) << "action did not re-run when its input changed"; + const auto gen = s::find_file(app.at("target"), "gen.cpp"); + expect(fatal(gen.has_value())) << "no gen.cpp"; + const auto before = mtime(*gen); + fs::last_write_time(app.at("src/main.cpp"), fs::file_time_type::clock::now()); + std::this_thread::sleep_for(std::chrono::seconds{1}); // coarser than any filesystem's mtime granularity here + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "rebuild failed"; + expect(fatal(eq(mtime(*s::find_file(app.at("target"), "gen.cpp")), before))) << "the action re-ran although its inputs were unchanged"; + { + auto program = app.read("build.mcpp"); + for (std::size_t at; (at = program.find("\"/data/value.txt\"")) != std::string::npos;) + program.replace(at, 17, "\"/data/other.txt\""); + app.write("build.mcpp", program); + } + app.write("data/other.txt", "31\n"); + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "rebuild after editing build.mcpp failed"; + expect(fatal(eq(value(), std::string{"VALUE=31"}))) << "an edited action command did not take effect"; + app.write("FAIL_THE_CHECK", ""); + fs::remove(outdir / "lint.stamp"); + expect(fatal(failed(app.mcpp({"build"})))) << "a failing check did not fail the build"; + app.remove("FAIL_THE_CHECK"); + s::write_executable(app.at("genpair.sh"), R"cpp(#!/usr/bin/env bash +# $1 = .cc to write, $2 = .h to write +printf '#include "%s"\nint paired() { return 13; }\n' "$(basename "$2")" > "$1" +printf 'int paired();\n' > "$2" +)cpp"); + app.write("build.mcpp", R"cpp(#include +#include +import mcpp; +int main() { + const std::string root = mcpp::manifest_dir(); + const std::string out = mcpp::out_dir(); + mcpp::action a; + a.id = "pair"; a.role = "source"; + a.arg((root + "/genpair.sh").c_str()) + .arg((out + "/p.cc").c_str()) + .arg((out + "/p.h").c_str()) + .output((out + "/p.cc").c_str()) + .output((out + "/p.h").c_str()) // companion: produced, NOT compiled + .submit(); + mcpp::include_dir(out.c_str()); + // Keep the earlier generated source in the build so main.cpp still links. + mcpp::action g; + g.id = "generate"; g.role = "source"; + g.arg((root + "/gen.sh").c_str()).arg((root + "/data/other.txt").c_str()) + .arg((out + "/gen.cpp").c_str()) + .input((root + "/data/other.txt").c_str()) + .output((out + "/gen.cpp").c_str()) + .submit(); +} +)cpp"); + app.remove("target"); + auto b2c = app.mcpp({"build"}); + expect(fatal(e2e::succeeded(b2c))) << "a companion (non-source) action output broke the build"; + expect(fatal(s::lacks(s::text(b2c), "object path collision"))) << "the header was adopted as a translation unit"; + + s::project edge{tmp / "edge", {{"mcpp.toml", "[package]\nname = \"edge\"\nversion = \"0.1.0\"\n"}, + {"src/main.cpp", "int main() {}\n"}, + {"build.mcpp", R"cpp(#include +#include +import mcpp; +int main() { + const std::string root = mcpp::manifest_dir(); + mcpp::action a; + a.id = "dollar"; a.role = "check"; + a.arg((root + "/dollar.sh").c_str()).arg("-Wl,-rpath,$ORIGIN") + .arg("${mcpp.out_dir}/dollar.stamp") + .output("${mcpp.out_dir}/dollar.stamp") + .submit(); +} +)cpp"}}}; + s::write_executable(edge.at("dollar.sh"), R"cpp(#!/usr/bin/env bash +# $1 must arrive containing a literal dollar sign +case "$1" in *'$'*) : > "$2";; *) echo "lost the dollar: [$1]" >&2; exit 1;; esac +)cpp"); + edge.remove("target"); + expect(fatal(e2e::succeeded(edge.mcpp({"build"})))) << "a literal $ in an action command did not survive"; + edge.write("build.mcpp", R"cpp(#include +import mcpp; +int main() { + mcpp::action a; + a.id = "bad-ref"; a.role = "artifact"; + a.arg("/bin/true").arg("${mcpp.target_file:no_such_target}") + .input("${mcpp.target_file:no_such_target}") + .output("${mcpp.out_dir}/x.out") + .submit(); +} +)cpp"); + edge.remove("target"); + auto b3c = edge.mcpp({"build"}); + expect(fatal(failed(b3c))) << "an unknown target reference was accepted"; + expect(fatal(e2e::contains(s::text(b3c), "no_such_target"))) << "error does not name the unknown target"; + + s::project objrole{tmp / "objrole", {{"mcpp.toml", "[package]\nname = \"objrole\"\nversion = \"0.1.0\"\n"}, + {"src/main.cpp", "int main() { return 0; }\n"}}}; + expect(fatal(e2e::succeeded(objrole.mcpp({"build"})))) << "probe build failed"; + const auto obj_ninja = ninja_file(objrole.at("target")); + expect(fatal(obj_ninja.has_value())) << "no build.ninja"; + std::smatch cxx_line; + const auto ninja_text = s::read_file(*obj_ninja); + expect(fatal(std::regex_search(ninja_text, cxx_line, std::regex{R"((^|\n)cxx *= *([^\n]*))"}))) << "could not read the compiler out of build.ninja"; + const auto obj_cxx = cxx_line[2].str(); + objrole.write("src/main.cpp", R"cpp(#include +extern "C" int blob_value(); +int main() { std::printf("BLOB=%d\n", blob_value()); return blob_value() == 7 ? 0 : 1; } +)cpp"); + objrole.write("blob.cpp", "extern \"C\" int blob_value() { return 7; }\n"); + s::write_executable(objrole.at("mkobj.sh"), std::format("#!/usr/bin/env bash\n# $1 = source, $2 = object to produce\n{} -c \"$1\" -o \"$2\"\n", obj_cxx)); + objrole.write("build.mcpp", R"cpp(#include +#include +import mcpp; +int main() { + const std::string root = mcpp::manifest_dir(); + const std::string out = mcpp::out_dir(); + mcpp::action o; + o.id = "blob"; o.role = "object"; + o.arg((root + "/mkobj.sh").c_str()) + .arg((root + "/blob.cpp").c_str()) + .arg((out + "/blob.o").c_str()) + .input((root + "/blob.cpp").c_str()) + .output((out + "/blob.o").c_str()) + .submit(); +} +)cpp"); + expect(fatal(e2e::succeeded(objrole.mcpp({"build"})))) << "role=object build failed"; + auto o2 = objrole.mcpp({"run"}); + expect(fatal(e2e::succeeded(o2))) << "the object was not linked in"; + expect(fatal(e2e::contains(s::text(o2), "BLOB=7"))) << "wrong value from the linked object"; + std::this_thread::sleep_for(std::chrono::seconds{1}); + objrole.write("blob.cpp", "extern \"C\" int blob_value() { return 9; }\n"); + auto o3 = objrole.mcpp({"run"}); + expect(fatal(failed(o3))) << "expected the changed object to be relinked (main asserts ==7)"; + expect(fatal(e2e::contains(s::text(o3), "BLOB=9"))) << "editing the object's input did not reach the link"; + objrole.write("blob.cpp", "extern \"C\" int blob_value() { return 7; }\n"); + objrole.write("build.mcpp", R"cpp(#include +#include +import mcpp; +int main() { + const std::string root = mcpp::manifest_dir(); + const std::string out = mcpp::out_dir(); + mcpp::action o; + o.id = "blob"; o.role = "object"; + o.arg((root + "/mkobj.sh").c_str()) + .arg((root + "/blob.cpp").c_str()) + .arg((out + "/blob.o").c_str()) + .input((root + "/blob.cpp").c_str()) + .output((out + "/blob.o").c_str()) + .target("no_such_target") + .submit(); +} +)cpp"); + objrole.remove("target"); + auto o4 = objrole.mcpp({"build"}); + expect(fatal(failed(o4))) << "role=object accepted an unknown target"; + expect(fatal(e2e::contains(s::text(o4), "no_such_target"))) << "error does not name the unknown target"; + { + auto program = objrole.read("build.mcpp"); + const std::string_view one = ".target(\"no_such_target\")"; + program.replace(program.find(one), one.size(), ".target(\"objrole\").target(\"no_such_target\")"); + objrole.write("build.mcpp", program); + } + objrole.remove("target"); + auto o4b = objrole.mcpp({"build"}); + expect(fatal(failed(o4b))) << "an unknown target was silently dropped because a sibling matched"; + expect(fatal(e2e::contains(s::text(o4b), "no_such_target"))) << "error does not name the unknown target"; + + s::project objtest{tmp / "objtest", {{"mcpp.toml", "[package]\nname = \"objtest\"\nversion = \"0.1.0\"\n"}, + {"src/engine.cppm", "export module objtest.engine;\nextern \"C\" int blob_value();\nexport int get(){ return blob_value(); }\n"}, + {"src/main.cpp", "import objtest.engine;\nint main(){ return get()==7?0:1; }\n"}, + {"tests/t_engine.cpp", "import objtest.engine;\nint main(){ return get()==7?0:1; }\n"}, + {"blob.cpp", "extern \"C\" int blob_value() { return 7; }\n"}, + {"build.mcpp", R"cpp(#include +#include +import mcpp; +int main() { + const std::string root = mcpp::manifest_dir(); + const std::string out = mcpp::out_dir(); + mcpp::action o; + o.id = "blob"; o.role = "object"; + o.arg((root + "/mkobj.sh").c_str()) + .arg((root + "/blob.cpp").c_str()) + .arg((out + "/blob.o").c_str()) + .input((root + "/blob.cpp").c_str()) + .output((out + "/blob.o").c_str()) + .submit(); // no .target(): every image, tests included +} +)cpp"}}}; + s::write_executable(objtest.at("mkobj.sh"), objrole.read("mkobj.sh")); + expect(fatal(e2e::succeeded(objtest.mcpp({"build"})))) << "objtest build failed"; + auto o6 = objtest.mcpp({"test"}); + expect(fatal(e2e::succeeded(o6))) << "the test binary did not receive the object (undefined symbol)"; + expect(fatal(e2e::contains(s::text(o6), "1 passed"))) << "the test did not run"; + + s::project objnone{tmp / "objnone", {{"mcpp.toml", "[package]\nname = \"objnone\"\nversion = \"0.1.0\"\n\n[features]\ngated = {}\n\n[targets.objnone]\nkind = \"lib\"\nrequired_features = [\"gated\"]\n"}, + {"src/objnone.cppm", "export module objnone;\nexport int f(){return 1;}\n"}, + {"blob.cpp", objtest.read("blob.cpp")}, + {"build.mcpp", objtest.read("build.mcpp")}}}; + s::write_executable(objnone.at("mkobj.sh"), objtest.read("mkobj.sh")); + auto o7 = objnone.mcpp({"build"}); + expect(fatal(e2e::succeeded(o7))) << "objnone build failed"; + expect(fatal(e2e::contains(s::text(o7), "produces no target to put its outputs into"))) << "an object with no consumer must be reported"; + auto o7b = objnone.mcpp({"build", "--features", "gated"}); + expect(fatal(e2e::succeeded(o7b))) << "objnone build with the feature failed"; + expect(fatal(s::lacks(s::text(o7b), "produces no target to put its outputs into"))) << "the object was reported as having no consumer while its target existed"; + + edge.write("build.mcpp", R"cpp(#include +int main() { + std::printf("mcpp:protocol=1\n"); + // No `command`, no `outputs` — mcpp fixes the source set during prepare, + // so an output whose NAME is unknown cannot be built. + std::printf("mcpp:action={\"id\":\"broken\",\"role\":\"source\"}\n"); +} +)cpp"); + edge.remove("target"); + auto b5 = edge.mcpp({"build"}); + expect(fatal(failed(b5))) << "a malformed action was accepted"; + expect(e2e::contains(s::text(b5), "malformed action")) << "no diagnostic for the malformed action"; + }; +}}; + +} // namespace diff --git a/tests/e2e/actions/devices.cpp b/tests/e2e/actions/devices.cpp new file mode 100644 index 000000000..41cd6cc49 --- /dev/null +++ b/tests/e2e/actions/devices.cpp @@ -0,0 +1,303 @@ +// Device sources and the actions that consume them: constrained globs, chained actions, object +// actions in a static library, shader extensions (formerly tests/e2e/606, 607, 608, 609). +import std; +import mcpplibs.mtest; +import mcpp.test.support; + +using namespace mcpplibs::mtest; +namespace s = mcpp::test::support; +namespace fs = std::filesystem; + +namespace { + +bool failed(const e2e::outcome& r) { return r.started && r.status != 0; } + +// The build program of 606 and 609: a `check` action per device source (mcpp refuses a device +// source that reached no action), then a warning listing them; `{}` is the warning's argument. +constexpr std::string_view accounting = R"cpp(import std; +import mcpp; +int main() {{ + const char* d = mcpp::device_sources(); + std::string flat(d); + std::size_t n = 0, start = 0; + while (start <= flat.size()) {{ + auto nl = flat.find('\n', start); + auto one = flat.substr(start, nl == std::string::npos ? flat.size() - start : nl - start); + start = nl == std::string::npos ? flat.size() + 1 : nl + 1; + if (one.empty()) continue; + auto stamp = std::string(mcpp::out_dir()) + "/dev-" + std::to_string(n++) + ".stamp"; + mcpp::action a; + a.id = "seen"; + a.role = "check"; + a.description = "account for a device source"; + auto abs = std::string(mcpp::manifest_dir()) + "/" + one; + a.arg("cp").arg(abs.c_str()).arg(stamp.c_str()); + a.input(abs.c_str()); + a.output(stamp.c_str()); + a.submit(); + }} + {} + return 0; +}} +)cpp"; + +// `mcpp new ` without its module sources. +s::project scaffold(std::string_view name) { + auto p = s::project::scaffold(name); + for (const auto& f : s::glob(p.at("src"), "*.cppm")) fs::remove(f); + return p; +} + +E2E devices{"devices", [] { + "606 constrained source globs narrow to the build"_test[tag{"requires:gcc"}] = [] { + // A `[build] sources` entry may carry the accel it is for: covered, its C++ compiles and + // its device sources reach the build program; under --no-accel it is left out; outside + // the build's accel it is refused naming the glob and both sides (accel-mismatch); a + // constrained glob matching nothing is refused. + s::needs({"gcc"}); + auto p = scaffold("narrow"); + p.write("src/main.cpp", "int main() { return 0; }\n"); + p.write("src/dev/helper.cpp", "int widget_helper() { return 42; }\n"); + p.write("src/dev/kernel.cu", "__global__ void k() {}\n"); + auto manifest = [&](std::string_view build, std::string_view glob_accel, std::string_view glob) { + p.write("mcpp.toml", std::format("[package]\nname = \"narrow\"\nversion = \"0.1.0\"\n[language]\nstandard = \"c++23\"\n[build]\naccel = \"{}\"\nsources = [\"src/*.cpp\", {{ glob = \"{}\", accel = \"{}\" }}]\n[targets.narrow]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n", build, glob, glob_accel)); + p.write("build.mcpp", std::vformat(accounting, std::make_format_args("mcpp::warning((*d ? d : \"(no device sources)\"));"))); + }; + manifest("widget9+{w1,w2}", "widget9+{w1}", "src/dev/**/*"); + auto covered = p.mcpp({"build", "-v"}); + expect(fatal(e2e::succeeded(covered))) << "a covered constraint failed the build"; + expect(e2e::contains(s::text(covered), "helper.cpp")) << "the constrained glob's C++ file was not compiled"; + expect(e2e::contains(s::text(covered), "src/dev/kernel.cu")) << "the device source did not reach the build program"; + auto none = p.mcpp({"build", "-v", "--no-accel"}); + expect(fatal(e2e::succeeded(none))) << "--no-accel failed the build"; + expect(s::lacks(s::text(none), "helper.cpp")) << "the constrained glob was compiled under --no-accel"; + expect(e2e::contains(s::text(none), "(no device sources)")) << "device sources were listed under --no-accel"; + manifest("widget9+{w2}", "widget9+{w1}", "src/dev/**/*"); + auto mismatch = p.mcpp({"build"}); + expect(fatal(failed(mismatch))) << "a constraint outside the build's accel was accepted"; + expect(e2e::contains(s::text(mismatch), "src/dev/**/*")) << "refusal does not name the glob"; + expect(e2e::contains(s::text(mismatch), "widget9+{w1}")) << "refusal does not name the constraint"; + expect(e2e::contains(s::text(mismatch), "widget9+{w2}")) << "refusal does not name what the build targets"; + const auto why = s::json(p.mcpp({"why", "toolchain", "--format", "json"}).out); + const auto reason = why.contains("data") && why["data"].contains("reason") ? why["data"]["reason"].get() : std::string{"-"}; + expect(eq(reason, std::string{"accel-mismatch"})) << "reason is '" << reason << "', expected accel-mismatch"; + manifest("widget9+{w1,w2}", "widget9+{w1}", "src/nowhere/**/*"); + auto empty = p.mcpp({"build"}); + expect(fatal(failed(empty))) << "a constrained glob matching nothing was accepted"; + expect(e2e::contains(s::text(empty), "src/nowhere")) << "refusal does not name the empty glob"; + }; + + "607 chained actions form the device link"_test[tag{"requires:gcc"}] = [] { + // One action consumes what another produced; the engine orders and fingerprints them: + // an artifact action's output read by an object action whose output joins the link, + // and an edit at the head of the chain reaches the link. + s::needs({"gcc"}); + auto p = scaffold("chain"); + p.write("src/main.cpp", "extern \"C\" int joined_value();\nint main() { return joined_value() == 42 ? 0 : 1; }\n"); + p.write("src/parts/a.inc", "extern \"C\" int joined_value() { return\n"); + p.write("src/parts/b.inc", "42; }\n"); + p.write("mcpp.toml", "[package]\nname = \"chain\"\nversion = \"0.1.0\"\n[language]\nstandard = \"c++23\"\n[targets.chain]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n"); + p.write("build.mcpp", R"cpp(import std; +import mcpp; +int main() { + mcpp::rerun_if_changed("src/parts/a.inc"); + mcpp::rerun_if_changed("src/parts/b.inc"); + const std::string root = mcpp::manifest_dir(), out = mcpp::out_dir(); + const std::string joined = out + "/joined.cpp", obj = out + "/joined.o"; + { // Step 1: an ARTIFACT. Its output is data as far as the link is concerned. + mcpp::action a; + a.id = "join"; a.role = "artifact"; a.description = "join parts"; + a.arg("sh"); a.arg("-c"); + a.arg(("cat '" + root + "/src/parts/a.inc' '" + root + "/src/parts/b.inc' > '" + joined + "'").c_str()); + a.input((root + "/src/parts/a.inc").c_str()); + a.input((root + "/src/parts/b.inc").c_str()); + a.output(joined.c_str()); + a.submit(); + } + { // Step 2: an OBJECT that consumes step 1's output. The engine sees a + // path; ninja orders the two by it. + mcpp::action a; + a.id = "compile-joined"; a.role = "object"; a.description = "compile joined"; + a.arg((std::string(mcpp::toolchain_dir()) + "/bin/g++").c_str()); + a.arg("-c"); a.arg(joined.c_str()); a.arg("-o"); a.arg(obj.c_str()); + a.input(joined.c_str()); + a.output(obj.c_str()); + a.submit(); + } + return 0; +} +)cpp"); + expect(fatal(e2e::succeeded(p.mcpp({"build"})))) << "the chained build failed"; + expect(fatal(e2e::succeeded(p.mcpp({"run"})))) << "the linked object did not carry the joined value"; + p.write("src/parts/b.inc", "43; }\n"); + expect(fatal(e2e::succeeded(p.mcpp({"build"})))) << "rebuild after editing an input failed"; + expect(failed(p.mcpp({"run"}))) << "the edit at the head of the chain did not reach the link"; + }; + + "608 object actions reach a static library"_test[tag{"requires:gcc"}] = [] { + // An `object`-role action's outputs join a static library, not only an executable: the + // archive holds the member and its symbol. + s::needs({"gcc"}); + auto p = scaffold("archived"); + p.remove("src/main.cpp"); + p.write("src/host.c", "extern int from_action(void);\nint host_value(void) { return from_action(); }\n"); + p.write("mcpp.toml", "[package]\nname = \"archived\"\nversion = \"0.1.0\"\n[language]\nstandard = \"c++23\"\n[build]\nsources = [\"src/*.c\"]\n[targets.archived]\nkind = \"lib\"\n"); + p.write("build.mcpp", R"cpp(import std; +import mcpp; +int main() { + const std::string out = std::string(mcpp::out_dir()); + const std::string src = out + "/piece.c"; + { std::ofstream f(src, std::ios::trunc); + f << "int from_action(void) { return 7; }\n"; } + const std::string obj = out + "/piece.o"; + mcpp::action a; + a.id = "piece"; a.role = "object"; a.description = "compile the piece"; + a.arg((std::string(mcpp::toolchain_dir()) + "/bin/gcc").c_str()); + a.arg("-c"); a.arg(src.c_str()); a.arg("-o"); a.arg(obj.c_str()); + a.input(src.c_str()); + a.output(obj.c_str()); + a.submit(); + return 0; +} +)cpp"); + auto b = p.mcpp({"build"}); + expect(fatal(e2e::succeeded(b))) << "the build failed"; + expect(s::lacks(s::text(b), "produces no target")) << "the action was dropped for want of a linked image"; + const auto lib = s::find_file(p.at("target"), "libarchived.a"); + expect(fatal(lib.has_value())) << "no static library was produced"; + const auto members = s::archive_members(*lib); + expect(std::ranges::contains(members, std::string{"piece.o"})) << "piece.o is not a member of the archive"; + expect(std::ranges::contains(s::archive_symbols(*lib), std::string{"from_action"})) << "the archive has the member but not its symbol"; + }; + + "609 shader sources are device sources"_test[tag{"requires:gcc"}] = [] { + // A shader is a device source for every language a separate compiler consumes: all 18 + // extensions reach the build program and none is offered to the C++ compiler; an + // extension with no role is refused by name; the default globs exclude them. + s::needs({"gcc"}); + auto p = scaffold("shaders"); + p.write("src/main.cpp", "int main() { return 0; }\n"); + p.write("build.mcpp", std::vformat(accounting, std::make_format_args("for (auto& c : flat) if (c == '\\n') c = ' ';\n mcpp::warning((\"device=[\" + flat + \"]\").c_str());"))); + auto manifest = [&](std::string_view accel, std::string_view sources) { + p.write("mcpp.toml", std::format("[package]\nname = \"shaders\"\nversion = \"0.1.0\"\n[language]\nstandard = \"c++23\"\n[build]\n{}\nsources = [{}]\n[targets.shaders]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n", accel, sources)); + }; + const std::vector exts{"comp", "vert", "frag", "geom", "tesc", "tese", "mesh", "task", "rgen", "rint", "rahit", "rchit", "rmiss", "rcall", "glsl", "hlsl", "cl", "metal"}; + for (const auto& e : exts) p.write(std::format("shaders/s.{}", e), std::format("// {}\n", e)); + manifest("accel = \"widget9+{w1}\"", "\"src/*.cpp\", { glob = \"shaders/*\", accel = \"widget9+{w1}\" }"); + auto all = p.mcpp({"build"}); + expect(fatal(e2e::succeeded(all))) << "a project with shader sources failed to build"; + std::string missing; + for (const auto& e : exts) + if (!e2e::contains(s::text(all), std::format("shaders/s.{}", e))) missing += " " + e; + expect(missing.empty()) << "not handed to the build program:" << missing; + const auto verbose = s::text(p.mcpp({"build", "-v"})); + for (const auto& e : exts) + expect(!e2e::contains(verbose, std::format("s.{} -o", e)) && !e2e::contains(verbose, std::format("s.{}.o", e))) << "s." << e << " was compiled"; + p.write("shaders/s.wgsl", "// wgsl\n"); + manifest("accel = \"widget9+{w1}\"", "\"src/*.cpp\", { glob = \"shaders/s.wgsl\", accel = \"widget9+{w1}\" }"); + auto unknown = p.mcpp({"build"}); + expect(fatal(failed(unknown))) << "an extension with no role was accepted"; + expect(e2e::contains(s::text(unknown), "no role for the extension")) << "refusal does not say the extension has no role"; + expect(e2e::contains(s::text(unknown), ".wgsl")) << "refusal does not name the extension"; + p.remove("shaders/s.wgsl"); + p.write("src/shaders/s.comp", p.read("shaders/s.comp")); + p.write("mcpp.toml", "[package]\nname = \"shaders\"\nversion = \"0.1.0\"\n[language]\nstandard = \"c++23\"\n[targets.shaders]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n"); + auto def = p.mcpp({"build"}); + expect(fatal(e2e::succeeded(def))) << "the default-glob build failed"; + expect(e2e::contains(s::text(def), "device=[]")) << "a shader under src/ reached a build program with no sources entry"; + }; + + "611 variant switch is not replayed"_test[tag{"requires:gcc"}] = [] { + // The device variant is part of the fingerprint: a plain build after `--no-accel` + // builds and runs the device variant again (the fast path does not replay the last + // directory), and each graph records the selection that wrote it. + s::needs({"gcc"}); + auto p = scaffold("probe"); + p.write("mcpp.toml", "[package]\nname = \"probe\"\nversion = \"0.1.0\"\n[language]\nstandard = \"c++23\"\n[build]\naccel = \"widget9+{w1}\"\n[target.'cfg(accelerator = \"widget\")'.build]\ndefines = [\"WIDGET_ON=1\"]\n[targets.probe]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n"); + p.write("src/main.cpp", "#include \n#ifndef WIDGET_ON\n#define WIDGET_ON 0\n#endif\nint main() { std::printf(\"WIDGET=%d\\n\", WIDGET_ON); }\n"); + auto variant = [&](std::vector extra = {}) { + std::vector args{"run"}; + args.insert(args.end(), extra.begin(), extra.end()); + return s::last_starting(p.mcpp(args).out, "WIDGET="); + }; + expect(fatal(e2e::succeeded(p.mcpp({"build"})))) << "the device build failed"; + expect(fatal(eq(variant(), std::string{"WIDGET=1"}))) << "the first build is not the device variant"; + expect(fatal(e2e::succeeded(p.mcpp({"build", "--no-accel"})))) << "--no-accel failed"; + expect(fatal(eq(variant({"--no-accel"}), std::string{"WIDGET=0"}))) << "--no-accel did not produce the CPU variant"; + expect(fatal(e2e::succeeded(p.mcpp({"build"})))) << "the plain build after --no-accel failed"; + expect(eq(variant(), std::string{"WIDGET=1"})) << "a plain build after --no-accel handed back the CPU variant"; + p.mcpp({"build", "--no-accel"}); + expect(eq(variant({"--no-accel"}), std::string{"WIDGET=0"})) << "--no-accel after a plain build is not the CPU variant"; + std::size_t by_default = 0, by_override = 0; + for (const auto& nj : s::glob(p.at("target"), "*/*/build.ninja")) { + const auto text = s::read_file(nj); + by_default += s::has_line(text, "^# mcpp:graph=normal;schedule=[a-z-]*;accel=default") ? 1 : 0; + by_override += s::has_line(text, "^# mcpp:graph=normal;schedule=[a-z-]*;accel=override") ? 1 : 0; + } + expect(by_default == 1 && by_override == 1) << "expected one default and one override graph, got default=" << by_default << " override=" << by_override; + }; + + "613 sycl sources are device sources"_test[tag{"requires:gcc"}] = [] { + // A SYCL unit is a device source because of the compiler that consumes it: a .sycl unit + // in a constrained glob reaches the build program and is not compiled by mcpp; the same + // content named .cpp is C++; --no-accel and the default globs leave .sycl out. + s::needs({"gcc"}); + auto p = scaffold("sycltest"); + p.write("src/main.cpp", "int main() { return 0; }\n"); + p.write("build.mcpp", std::vformat(accounting, std::make_format_args("for (auto& c : flat) if (c == '\\n') c = ' ';\n mcpp::warning((\"device=[\" + flat + \"]\").c_str());"))); + auto manifest = [&](std::string_view accel, std::string_view sources) { + p.write("mcpp.toml", std::format("[package]\nname = \"sycltest\"\nversion = \"0.1.0\"\n[language]\nstandard = \"c++23\"\n[build]\n{}\nsources = [{}]\n[targets.sycltest]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n", accel, sources)); + }; + p.write("src/kernels/saxpy.sycl", "// Ordinary C++. What makes this a device unit is which compiler consumes it.\nextern \"C\" int saxpy_device(int n) { return n; }\n"); + manifest("accel = \"sycl\"", "\"src/*.cpp\", { glob = \"src/kernels/*.sycl\", accel = \"sycl\" }"); + auto one = p.mcpp({"build"}); + expect(fatal(e2e::succeeded(one))) << "a project with a .sycl source failed to build"; + expect(s::has_line(s::text(one), R"(device=\[.*src/kernels/saxpy.sycl.*\])")) << "the .sycl unit did not reach MCPP_DEVICE_SOURCES"; + expect(s::no_line(s::text(p.mcpp({"build", "-v"})), R"(saxpy\.sycl -o|saxpy\.sycl\.o)")) << "the .sycl unit was offered to the C++ compiler"; + p.remove("src/kernels/saxpy.sycl"); + p.write("src/kernels/saxpy.cpp", "extern \"C\" int saxpy_device(int n) { return n; }\n"); + manifest("accel = \"sycl\"", "\"src/*.cpp\", \"src/kernels/*.cpp\""); + auto three = p.mcpp({"build", "-v"}); + expect(fatal(e2e::succeeded(three))) << "the .cpp spelling failed to build"; + expect(e2e::contains(s::text(three), "device=[]")) << "a .cpp was handed to the build program as a device source"; + expect(e2e::contains(s::text(three), "saxpy.cpp")) << "the .cpp was not compiled"; + p.remove("src/kernels/saxpy.cpp"); + p.write("src/kernels/saxpy.sycl", "extern \"C\" int saxpy_device(int n) { return n; }\n"); + manifest("accel = \"sycl\"", "\"src/*.cpp\", { glob = \"src/kernels/*.sycl\", accel = \"sycl\" }"); + auto four = p.mcpp({"build", "--no-accel"}); + expect(fatal(e2e::succeeded(four))) << "--no-accel failed to build"; + expect(e2e::contains(s::text(four), "device=[]")) << "--no-accel still handed the .sycl unit to the build program"; + p.write("mcpp.toml", "[package]\nname = \"sycltest\"\nversion = \"0.1.0\"\n[language]\nstandard = \"c++23\"\n[targets.sycltest]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n"); + auto five = p.mcpp({"build"}); + expect(fatal(e2e::succeeded(five))) << "the default-glob project failed to build"; + expect(e2e::contains(s::text(five), "device=[]")) << "a default glob picked up the .sycl source"; + }; + + "623 device sources must reach an action"_test[tag{"requires:elf"}][tag{"requires:gcc"}] = [] { + // A device-kind source no action compiles is refused naming the file: without a build + // program, and with one that declared no action; --no-accel leaves it out and builds. + s::needs({"elf", "gcc"}); + const auto tmp = temp_dir(); + auto mk = [&] { + fs::remove_all(tmp / "p"); + return s::project{tmp / "p", {{"src/main.cpp", "extern \"C\" void k();\nint main() { return 0; }\n"}, + {"src/kernels/k.cu", "extern \"C\" __global__ void k() {}\n"}, + {"mcpp.toml", "[package]\nname = \"orphan\"\nversion = \"0.1.0\"\naccelerators = [\"cuda\"]\n[build]\naccel = \"cuda12.9+{sm_89}\"\nsources = [\n \"src/*.cpp\",\n { glob = \"src/kernels/*.cu\", accel = \"cuda12.9+{sm_89}\" },\n]\n[targets.orphan]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n"}}}; + }; + auto p = mk(); + auto none = p.mcpp({"build"}); + expect(fatal(failed(none))) << "a package with an uncompilable device source built"; + expect(e2e::contains(s::text(none), "src/kernels/k.cu")) << "the refusal does not name the file"; + expect(e2e::contains(s::text(none), "no `build.mcpp`")) << "the refusal does not say the build program is missing"; + p = mk(); + p.write("build.mcpp", "import std;\nimport mcpp;\nint main() { return 0; }\n"); + auto unclaimed = p.mcpp({"build"}); + expect(fatal(failed(unclaimed))) << "an unclaimed device source built"; + expect(e2e::contains(s::text(unclaimed), "src/kernels/k.cu")) << "the refusal does not name the file"; + expect(e2e::contains(s::text(unclaimed), "ran but declared no action")) << "the refusal does not distinguish a program that claimed nothing"; + p = mk(); + expect(e2e::succeeded(p.mcpp({"build", "--no-accel"}))) << "--no-accel must not be refused; the constrained glob is left out"; + }; +}}; + +} // namespace diff --git a/tests/e2e/actions/generated.cpp b/tests/e2e/actions/generated.cpp new file mode 100644 index 000000000..ef492180c --- /dev/null +++ b/tests/e2e/actions/generated.cpp @@ -0,0 +1,215 @@ +// Generated files: a package's and a manifest's [generated_files], a build program's generated +// sources and headers (formerly tests/e2e/51, 107, 124, 142, 314). +import std; +import mcpplibs.mtest; +import mcpp.test.support; + +using namespace mcpplibs::mtest; +namespace s = mcpp::test::support; +namespace fs = std::filesystem; + +namespace { + +bool failed(const e2e::outcome& r) { return r.started && r.status != 0; } + +// A file's modification time in whole seconds (`stat -c %Y`). +long long mtime(const fs::path& p) { + return std::chrono::duration_cast(std::chrono::file_clock::to_sys(fs::last_write_time(p)).time_since_epoch()).count(); +} + +// The build.ninja of the build, not of a build program's. +std::optional ninja_file(const fs::path& target) { + for (const auto& f : s::find_files(target, R"(build\.ninja)")) + if (f.generic_string().find("/.build-mcpp/") == std::string::npos) return f; + return std::nullopt; +} + +E2E generated{"generated", [] { + "51 package generated files"_test[tag{"requires:gcc"}][tag{"requires:fresh-sandbox"}] = [] { + // A package descriptor's generated_files are materialized in the package before it is + // scanned and compiled. + s::needs({"gcc", "fresh-sandbox"}); + const auto tmp = temp_dir(); + s::project{tmp / "local-index", {{"pkgs/t/tinycfg.lua", R"cpp(package = { + spec = "1", + name = "tinycfg", + description = "Generated config header package", + licenses = {"MIT"}, + type = "package", + xpm = { + linux = { + ["1.0.0"] = { + url = "https://example.invalid/tinycfg-1.0.0.tar.gz", + sha256 = "0000000000000000000000000000000000000000000000000000000000000000", + }, + }, + }, + mcpp = { + language = "c++23", + import_std = false, + sources = { "src/*.c" }, + include_dirs = { "mcpp_generated/include" }, + generated_files = { + ["mcpp_generated/include/generated_config.h"] = "#pragma once\n#define TINYCFG_VALUE 42\n", + }, + targets = { ["tinycfg"] = { kind = "lib" } }, + deps = {}, + }, +} +)cpp"}}}; + s::project app{tmp / "project/app", {{".mcpp/.xlings/data/xpkgs/local-dev.tinycfg/1.0.0/src/tinycfg.c", "#include \"generated_config.h\"\nint tinycfg_value(void) {\n return TINYCFG_VALUE;\n}\n"}, + {"src/main.cpp", "extern \"C\" int tinycfg_value(void);\nint main() {\n return tinycfg_value() == 42 ? 0 : 1;\n}\n"}}}; + app.write("mcpp.toml", std::format("[package]\nname = \"app\"\nversion = \"0.1.0\"\n[indices]\nlocal-dev = {{ path = \"{}\" }}\n[dependencies]\n\"local-dev.tinycfg\" = \"1.0.0\"\n[targets.app]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n", (tmp / "local-index").generic_string())); + const s::options inherited{.where = s::home::inherited}; + expect(fatal(e2e::succeeded(app.mcpp({"build"}, inherited)))) << "build failed"; + const auto made = ".mcpp/.xlings/data/xpkgs/local-dev.tinycfg/1.0.0/mcpp_generated/include/generated_config.h"; + expect(fatal(app.exists(made))) << "generated file was not materialized"; + expect(e2e::contains(app.read(made), "TINYCFG_VALUE 42")) << "generated file content mismatch"; + expect(e2e::succeeded(app.mcpp({"run"}, inherited))) << "run failed"; + }; + + "107 generated files toml"_test[tag{"requires:gcc"}] = [] { + // [generated_files] entries materialize in the project root before the scan, enter the + // fingerprint (a content change rebuilds), and a path escaping the root is an error. + s::needs({"gcc"}); + auto p = s::project::scaffold("genf"); + p.write("mcpp.toml", "[package]\nname = \"genf\"\nversion = \"0.1.0\"\n[generated_files]\n\"src/gen/answer.cpp\" = \"\"\"\nextern \"C\" int gen_answer() { return 41; }\n\"\"\"\n"); + p.write("src/main.cpp", "import std;\nextern \"C\" int gen_answer();\nint main() {\n std::println(\"gen = {}\", gen_answer());\n return 0;\n}\n"); + expect(fatal(e2e::succeeded(p.mcpp({"build"})))) << "build failed"; + expect(fatal(p.exists("src/gen/answer.cpp"))) << "generated file not materialized"; + expect(eq(s::last_line(p.mcpp({"run"}).out), std::string{"gen = 41"})) << "unexpected output"; + auto manifest = p.read("mcpp.toml"); + manifest.replace(manifest.find("return 41"), 9, "return 42"); + p.write("mcpp.toml", manifest); + expect(fatal(e2e::succeeded(p.mcpp({"build"})))) << "rebuild failed"; + expect(eq(s::last_line(p.mcpp({"run"}).out), std::string{"gen = 42"})) << "stale after content change"; + p.write("mcpp.toml", "[package]\nname = \"genf\"\nversion = \"0.1.0\"\n[generated_files]\n\"../evil.cpp\" = \"int x;\"\n"); + auto escape = p.mcpp({"build"}); + expect(fatal(failed(escape))) << "escaping path was accepted"; + expect(s::contains_nocase(s::text(escape), "generated_files")) << "wrong error"; + }; + + "124 gen source out of root object path"_test[tag{"requires:elf"}][tag{"requires:gcc"}] = [] { + // A dependency's build program emits a source into OUT_DIR, outside the dependency's + // root; its object path stays under obj/ (no `..`, not absolute), beside the consumer's + // own gen.cpp. + s::needs({"elf", "gcc"}); + const auto tmp = temp_dir(); + s::project{tmp / "gdep", {{"build.mcpp", R"cpp(#include +#include +import mcpp; +int main() { + std::string p = std::string(mcpp::out_dir()) + "/gen.cpp"; + std::FILE* f = std::fopen(p.c_str(), "w"); + std::fputs("int gen_val() { return 7; }\n", f); + std::fclose(f); + mcpp::generated("gen.cpp"); + return 0; +} +)cpp"}, + {"src/lib.cpp", "extern int gen_val();\nint gdep_use() { return gen_val(); }\n"}, + {"mcpp.toml", "[package]\nname = \"gdep\"\nversion = \"0.1.0\"\n[modules]\nsources = [\"src/**/*.cpp\"]\n[targets.gdep]\nkind = \"lib\"\n"}}}; + s::project consumer{tmp / "consumer", {{"src/gen.cpp", "int consumer_gen() { return 100; }\n"}, + {"src/main.cpp", "import std;\nextern int gdep_use();\nextern int consumer_gen();\nint main() {\n std::println(\"sum={}\", gdep_use() + consumer_gen());\n return (gdep_use() == 7 && consumer_gen() == 100) ? 0 : 1;\n}\n"}, + {"mcpp.toml", "[package]\nname = \"consumer\"\nversion = \"0.1.0\"\n[modules]\nsources = [\"src/**/*.cpp\"]\n[dependencies]\ngdep = { path = \"../gdep\" }\n[targets.consumer]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n"}}}; + expect(fatal(e2e::succeeded(consumer.mcpp({"build"})))) << "build failed (expected: out-of-root generated source must map under obj/)"; + const auto nj = ninja_file(consumer.at("target")); + expect(fatal(nj.has_value())) << "no build.ninja generated"; + const auto ninja = s::read_file(*nj); + std::string bad; + const std::regex object{R"(build ([^ ]+\.o) : cxx_object)"}; + const std::regex escapes{R"((^|/)\.\.(/|$)|^/)"}; + for (std::sregex_iterator it(ninja.begin(), ninja.end(), object), end; it != end; ++it) + if (const auto o = (*it)[1].str(); std::regex_search(o, escapes)) bad += o + "\n"; + expect(bad.empty()) << "object path escapes obj/:\n" << bad; + expect(eq(s::last_line(consumer.mcpp({"run"}).out), std::string{"sum=107"})) << "unexpected output"; + }; + + "142 generated files incremental"_test[tag{"requires:gcc"}] = [] { + // An unchanged [generated_files] entry is not rewritten when an unrelated source change + // re-runs the materialization, so its includers are not recompiled; a changed one is + // rewritten and its includers recompiled. + s::needs({"gcc"}); + auto p = s::project::scaffold("genincr"); + p.write("mcpp.toml", "[package]\nname = \"genincr\"\nversion = \"0.1.0\"\n[generated_files]\n\"src/gen/config.h\" = \"\"\"\n#define GEN_ANSWER 41\n\"\"\"\n"); + p.write("src/main.cpp", "#include \"gen/config.h\"\n#include \nextern int other();\nint main() {\n std::printf(\"gen = %d\\n\", GEN_ANSWER + other());\n return 0;\n}\n"); + p.write("src/other.cpp", "int other() { return 0; }\n"); + expect(fatal(e2e::succeeded(p.mcpp({"build"})))) << "build failed"; + expect(fatal(p.exists("src/gen/config.h"))) << "generated file not materialized"; + const auto mains = s::find_files(p.at("target"), R"(main.*\.o)"); + const auto others = s::find_files(p.at("target"), R"(other.*\.o)"); + expect(fatal(!mains.empty() && !others.empty())) << "object files not found under target/"; + const auto gen_before = mtime(p.at("src/gen/config.h")); + const auto main_before = mtime(mains.front()); + std::this_thread::sleep_for(std::chrono::seconds{1}); // a rewrite observable at 1 s granularity + fs::last_write_time(p.at("src/other.cpp"), fs::file_time_type::clock::now()); + expect(fatal(e2e::succeeded(p.mcpp({"build"})))) << "incremental rebuild failed"; + expect(fatal(mtime(others.front()) > gen_before)) << "other.o was not recompiled — test premise broken"; + const auto gen_after = mtime(p.at("src/gen/config.h")); + expect(eq(gen_after, gen_before)) << "generated file rewritten on incremental rebuild"; + expect(eq(mtime(mains.front()), main_before)) << "main.o recompiled on incremental rebuild"; + auto manifest = p.read("mcpp.toml"); + manifest.replace(manifest.find("GEN_ANSWER 41"), 13, "GEN_ANSWER 42"); + p.write("mcpp.toml", manifest); + std::this_thread::sleep_for(std::chrono::seconds{1}); + expect(fatal(e2e::succeeded(p.mcpp({"build"})))) << "rebuild after content change failed"; + const auto gen_changed = mtime(p.at("src/gen/config.h")); + expect(gen_after != gen_changed) << "generated file not rewritten after content change"; + expect(e2e::contains(p.read("src/gen/config.h"), "GEN_ANSWER 42")) << "generated file has stale content"; + long long main_changed = 0; + for (const auto& f : s::find_files(p.at("target"), R"(main.*\.o)")) main_changed = std::max(main_changed, mtime(f)); + expect(main_changed >= gen_changed) << "main.o not recompiled after content change"; + expect(eq(s::last_line(p.mcpp({"run"}).out), std::string{"gen = 42"})) << "stale binary after content change"; + }; + + "314 dependency action generated header"_test[tag{"requires:gcc"}] = [] { + // A package that generates its own header compiles after the generator (mcpp#534): the + // header is the generator's output, and every compile edge of the package waits for + // the package's action phony, of which there is one. + s::needs({"gcc"}); + const auto tmp = temp_dir(); + s::project proto{tmp / "proto", {{"mcpp.toml", "[package]\nname = \"proto\"\nversion = \"0.1.0\"\n[targets.proto]\nkind = \"lib\"\n"}, + {"src/proto.cppm", "module;\n#include \"proto_generated.h\"\nexport module proto;\nexport int proto_answer() { return PROTO_ANSWER; }\n"}, + {"build.mcpp", R"cpp(#include +import mcpp; +int main() { + const std::string root = mcpp::manifest_dir(); + const std::string out = mcpp::out_dir(); + mcpp::action a; + a.id = "proto:header"; + a.role = "source"; + a.arg((root + "/gen.sh").c_str()) + .arg((out + "/proto_generated.h").c_str()) + .output((out + "/proto_generated.h").c_str()) // HEADER, and the only output + .submit(); + mcpp::include_dir(out.c_str()); +} +)cpp"}}}; + s::write_executable(proto.at("gen.sh"), "#!/usr/bin/env bash\nsleep 2\nprintf '#define PROTO_ANSWER 42\\n' > \"$1\"\n"); + s::project app{tmp / "app", {{"mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n[dependencies]\nproto = { path = \"../proto\" }\n"}, + {"src/main.cpp", "#include \nimport proto;\nint main() { std::printf(\"ANSWER=%d\\n\", proto_answer()); }\n"}}}; + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "a dependency whose action generates its only header did not build"; + expect(eq(s::last_line(app.mcpp({"run"}).out), std::string{"ANSWER=42"})) << "expected ANSWER=42 from the generated header"; + const auto hdr = s::find_file(tmp, "proto_generated.h"); + expect(fatal(hdr.has_value())) << "no proto_generated.h anywhere"; + expect(fatal(fs::file_size(*hdr) > 0)) << hdr->string() << " is empty — the placeholder, not the generator's output"; + expect(e2e::contains(s::read_file(*hdr), "PROTO_ANSWER")) << hdr->string() << " is not what gen.sh writes"; + const auto nj = ninja_file(app.at("target")); + expect(fatal(nj.has_value())) << "no build.ninja"; + const auto ninja = s::read_file(*nj); + std::smatch m; + const bool any = std::regex_search(ninja, m, std::regex{R"((^|\n)build (mcpp-actions-[^ ]*proto[^ ]*) : phony)"}); + expect(fatal(any)) << "no action phony was emitted for the dependency"; + const auto phony = m[2].str(); + const auto quoted = std::regex_replace(phony, std::regex{R"([.^$|()\[\]{}*+?\\])"}, R"(\$&)"); + expect(eq(s::count_matching(ninja, std::format("^build {} : phony", quoted)), std::size_t{1})) << "expected exactly one '" << phony << "' edge"; + const std::string rules = "cxx_object|c_object|cxx_module|cxx_module_obj|cxx_module_bmi|cxx_scan|asm_object"; + const std::string src = R"(proto/src/proto\.cppm)"; + const auto edges = s::count_matching(ninja, std::format("^build .*: *({}) .*{}", rules, src)); + const auto ordered = s::count_matching(ninja, std::format(R"(^build .*: *({}) .*{}.*\|\| *{})", rules, src, quoted)); + expect(fatal(edges > 0)) << "no compile edge for the dependency was found at all — the assertion below would have passed by describing nothing"; + expect(eq(ordered, edges)) << ordered << " of " << edges << " dependency compile edges wait for the action"; + }; +}}; + +} // namespace diff --git a/tests/e2e/actions/hooks.cpp b/tests/e2e/actions/hooks.cpp new file mode 100644 index 000000000..1d0c20e7c --- /dev/null +++ b/tests/e2e/actions/hooks.cpp @@ -0,0 +1,240 @@ +// Project-level build hooks: lifecycle order, policy, timeout, the directory a hook runs in, and +// the two places nothing may fire; a dependency's install hook (formerly tests/e2e/317, 648). +import std; +import mcpplibs.mtest; +import mcpp.test.support; + +using namespace mcpplibs::mtest; +namespace s = mcpp::test::support; +namespace fs = std::filesystem; + +namespace { + +bool failed(const e2e::outcome& r) { return r.started && r.status != 0; } + +// A file's text without CR and without trailing newlines (`$(tr -d '\r' < file)`); "" when absent. +std::string body(const fs::path& file) { + if (!fs::exists(file)) return {}; + auto t = s::read_file(file); + std::erase(t, '\r'); + while (!t.empty() && t.back() == '\n') t.pop_back(); + return t; +} + +// `expect_log `: the file holds exactly these lines. +void expect_log(const fs::path& file, std::vector lines) { + std::string want; + for (auto l : lines) want += std::string(l) + "\n"; + while (!want.empty() && want.back() == '\n') want.pop_back(); + expect(fatal(eq(body(file), want))) << file.filename().string(); +} + +std::uintmax_t size_of(const fs::path& f) { return fs::exists(f) ? fs::file_size(f) : 0; } + +// The file grew, and then stopped growing. +bool frozen(const fs::path& beat) { + const auto before = size_of(beat); + std::this_thread::sleep_for(std::chrono::seconds{1}); + return before != 0 && before == size_of(beat); +} + +E2E hooks{"hooks", [] { + "317 project build hooks"_test = [] { + // [hooks]: start/finished/failed in order, run in the project root; a failing or timed + // out hook warns and keeps the result; side_effect = true is refused, enabled = false + // runs nothing, an invalid timeout runs nothing, an unknown key is reported; nothing + // fires when prepare fails; during_build ends with the build (grandchildren included), + // loops on request and gives up warning; loop is refused elsewhere; Ctrl-C ends it; a + // dependency's hooks and a workspace root's own hooks do not fire. + const auto tmp = temp_dir(); + s::project app{tmp / "app", {{"src/main.cpp", "int main() { return 0; }\n"}}}; + fs::create_directories(app.at("nested")); + auto manifest = [&](std::string_view hooks, std::string_view rest = "") { + app.write("mcpp.toml", std::format("[package]\nname = \"hookprobe\"\nversion = \"0.1.0\"\n\n[hooks]\n{}{}", hooks, rest)); + }; + manifest("build_start = \"echo start>>hooks.log\"\nbuild_failed = \"echo failed>>hooks.log\"\nbuild_finished = \"echo finished>>hooks.log\"\ntimeout_seconds = 10\nenabled = true\nside_effect = false\n"); + expect(fatal(e2e::succeeded(app.mcpp_in("nested", {"build"})))) << "hooked build failed"; + expect(fatal(e2e::succeeded(app.mcpp_in("nested", {"build"})))) << "second hooked build failed"; + expect(fatal(!app.exists("nested/hooks.log"))) << "hook ran in the invocation directory, not the project root"; + expect_log(app.at("hooks.log"), {"start", "finished", "start", "finished"}); + + app.write("src/main.cpp", "#error \"intentional hook failure path\"\nint main() { return 0; }\n"); + app.write("hooks.log", ""); + expect(fatal(failed(app.mcpp({"build"})))) << "broken source built"; + expect_log(app.at("hooks.log"), {"start", "failed"}); + app.write("src/main.cpp", "int main() { return 0; }\n"); + + manifest("build_finished = \"mcpp-hook-command-that-does-not-exist\"\n"); + auto ignored = app.mcpp({"build"}); + expect(fatal(e2e::succeeded(ignored))) << "a failing hook changed the build result"; + expect(fatal(e2e::contains(s::text(ignored), "warning: hook 'build_finished' exited with status"))) << "the hook failure was not reported"; + expect(fatal(s::lacks(s::text(ignored), "error: hook 'build_finished'"))) << "an experimental hook failure was reported as an error"; + manifest("build_finished = \"mcpp-hook-command-that-does-not-exist\"\nside_effect = false\n"); + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "side_effect=false was not accepted"; + manifest("build_finished = \"echo done\"\nside_effect = true\n"); + auto side = app.mcpp({"build"}); + expect(fatal(failed(side))) << "side_effect=true was accepted"; + expect(fatal(e2e::contains(s::text(side), "experimental"))) << "the refusal does not say why"; + manifest("build_start = \"echo disabled>>disabled.log\"\nbuild_finished = \"echo disabled>>disabled.log\"\nenabled = false\n"); + app.remove("disabled.log"); + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "disabled hook build failed"; + expect(fatal(!app.exists("disabled.log"))) << "enabled=false still ran hooks"; + + const bool windows = s::on_windows(); + const std::string slow = windows ? "ping -n 6 127.0.0.1 >NUL" : "sleep 5"; + const std::string pause_2s = windows ? "ping -n 3 127.0.0.1 >NUL" : "sleep 2"; + const std::string pause_5s = windows ? "ping -n 6 127.0.0.1 >NUL" : "sleep 5"; + manifest(std::format("build_finished = \"{}\"\ntimeout_seconds = 1\n", slow)); + auto timeout = app.mcpp({"build"}); + expect(fatal(e2e::succeeded(timeout))) << "a timed-out hook changed the build result"; + expect(fatal(s::has_line(s::text(timeout), R"(^warning: hook 'build_finished' timed out after 1s\r?$)"))) << "'warning: hook 'build_finished' timed out after 1s' is not a line of the output"; + manifest("build_start = \"echo should-not-run>>invalid.log\"\ntimeout_seconds = 0\n"); + app.remove("invalid.log"); + auto invalid = app.mcpp({"build"}); + expect(fatal(failed(invalid))) << "invalid hooks were accepted"; + expect(fatal(!app.exists("invalid.log"))) << "invalid hook ran"; + expect(fatal(e2e::contains(s::text(invalid), "[hooks].timeout_seconds must be a positive integer"))) << "invalid hook diagnostic is missing"; + app.write("hooks.log", ""); + manifest("build_start = \"echo start>>hooks.log\"\nbuild_finsihed = \"echo typo>>hooks.log\"\n"); + auto unknown = app.mcpp({"build"}); + expect(fatal(e2e::succeeded(unknown))) << "unknown hook key was fatal"; + expect(fatal(e2e::contains(s::text(unknown), "[hooks] has unsupported key 'build_finsihed'"))) << "unknown hook key was not reported"; + expect_log(app.at("hooks.log"), {"start"}); + app.write("hooks.log", ""); + manifest("build_start = \"echo start>>hooks.log\"\nbuild_failed = \"echo failed>>hooks.log\"\n", "\n[dependencies]\nabsent = { path = \"no/such/dependency\" }\n"); + expect(fatal(failed(app.mcpp({"build"})))) << "a missing path dependency built"; + expect_log(app.at("hooks.log"), {}); + + // during_build, with a grandchild that writes a heartbeat. + std::string writer; + if (windows) { + app.write("writer.cmd", "@echo off\n:loop\necho tick>>beat.log\nping -n 2 127.0.0.1 >NUL\ngoto loop\n"); + writer = "start /b cmd /c writer.cmd"; + } else { + app.write("writer.sh", "while :; do echo tick>>beat.log; sleep 0.2; done\n"); + writer = "sh writer.sh & wait"; + } + app.write("hooks.log", ""); + app.remove("beat.log"); + manifest(std::format("build_start = \"{}\"\nbuild_finished = \"echo finished>>hooks.log\"\nduring_build = \"{}\"\n", pause_2s, writer)); + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "build with during_build failed"; + expect(fatal(frozen(app.at("beat.log")))) << "during_build kept running after the build (or never ran)"; + expect_log(app.at("hooks.log"), {"finished"}); + auto runs = [&] { return s::count_lines(app.exists("runs.log") ? app.read("runs.log") : "", "run"); }; + app.remove("runs.log"); + manifest(std::format("build_start = \"{}\"\nduring_build = {{ cmd = \"echo run>>runs.log\", loop = true }}\n", pause_2s)); + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "looped during_build failed the build"; + expect(fatal(runs() >= 2)) << "loop=true ran " << runs() << " time(s), expected >= 2"; + app.remove("runs.log"); + manifest(std::format("build_start = \"{}\"\nduring_build = \"echo run>>runs.log\"\n", pause_2s)); + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "unlooped during_build failed the build"; + expect(fatal(eq(runs(), std::size_t{1}))) << "loop=false ran " << runs() << " time(s), expected 1"; + manifest(std::format("build_start = \"{}\"\nduring_build = {{ cmd = \"mcpp-hook-command-that-does-not-exist\", loop = true }}\n", pause_5s)); + auto giveup = app.mcpp({"build"}); + expect(fatal(e2e::succeeded(giveup))) << "a during_build that never ran changed the build result"; + expect(fatal(e2e::contains(s::text(giveup), "warning: hook 'during_build' failed to stay up"))) << "giving up was not reported"; + manifest("build_start = { cmd = \"echo start\", loop = true }\n"); + auto misplaced = app.mcpp({"build"}); + expect(fatal(failed(misplaced))) << "loop on build_start was accepted"; + expect(fatal(e2e::contains(s::text(misplaced), "during_build"))) << "the diagnostic does not name the event that supports loop"; + if (!windows) { + // Ctrl-C: the shell runs the build in the background and sends it SIGINT after 3 s. + app.remove("beat.log"); + manifest(std::format("build_start = \"sleep 20\"\nduring_build = \"{}\"\n", writer)); + const auto sh = s::on_path("sh"); + expect(fatal(sh.has_value())) << "no sh"; + s::run(*sh, {"-c", "\"$0\" build > interrupted.log 2>&1 & pid=$!; sleep 3; kill -INT \"$pid\" 2>/dev/null; wait \"$pid\" 2>/dev/null; true", s::mcpp_path()}, app.dir()); + expect(fatal(frozen(app.at("beat.log")))) << "Ctrl-C left during_build running"; + } + + // A dependency's hooks do not fire. + s::project dep{tmp / "dep", {{"src/dep.cppm", "export module dep;\nexport int dep_answer() { return 0; }\n"}, + {"mcpp.toml", "[package]\nname = \"dep\"\nversion = \"0.1.0\"\n\n[targets.dep]\nkind = \"lib\"\n\n[hooks]\nbuild_start = \"echo dependency>>dependency-hook.log\"\nbuild_finished = \"echo dependency>>dependency-hook.log\"\n"}}}; + app.write("hooks.log", ""); + app.remove("dependency-hook.log"); + manifest("build_start = \"echo start>>hooks.log\"\nbuild_finished = \"echo finished>>hooks.log\"\n", std::format("\n[dependencies]\ndep = {{ path = \"{}\" }}\n", dep.dir().generic_string())); + app.write("src/main.cpp", "import dep;\nint main() { return dep_answer(); }\n"); + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "build with a hooked dependency failed"; + expect_log(app.at("hooks.log"), {"start", "finished"}); + expect(fatal(!dep.exists("dependency-hook.log") && !app.exists("dependency-hook.log"))) << "a dependency's hooks ran"; + + // A workspace root's own hooks do not fire; each member's do, in the member. + s::project ws{tmp / "ws", {{"mcpp.toml", "[workspace]\nmembers = [\"alpha\", \"beta\"]\n\n[hooks]\nbuild_start = \"echo root>>root.log\"\nbuild_finished = \"echo root>>root.log\"\n"}}}; + for (std::string_view m : {"alpha", "beta"}) { + ws.write(std::format("{}/src/main.cpp", m), "int main() { return 0; }\n"); + ws.write(std::format("{}/mcpp.toml", m), std::format("[package]\nname = \"{0}\"\nversion = \"0.1.0\"\n\n[hooks]\nbuild_start = \"echo {0}-start>>hooks.log\"\nbuild_finished = \"echo {0}-finished>>hooks.log\"\n", m)); + } + expect(fatal(e2e::succeeded(ws.mcpp({"build"})))) << "workspace build with hooks failed"; + expect_log(ws.at("alpha/hooks.log"), {"alpha-start", "alpha-finished"}); + expect_log(ws.at("beta/hooks.log"), {"beta-start", "beta-finished"}); + expect(!ws.exists("root.log")) << "the workspace root's own hooks fired"; + expect(!ws.exists("hooks.log")) << "a member hook ran in the workspace root"; + }; + "648 an install hook sees the build target"_test[tag{"requires:elf"}] = [] { + // A dependency's install hook receives the build's target under the names a build + // program uses, and the two toolchain names present and empty (the toolchain is resolved + // after the graph), whatever the caller's environment holds. + s::needs({"elf"}); + s::project p{s::files{{"local-index/pkgs/h/acme.hookprobe.lua", R"cpp(package = { + spec = "1", + namespace = "acme", + name = "hookprobe", + description = "Compiles in what its install hook saw", + licenses = {"MIT"}, + type = "package", + xpm = { + linux = { ["latest"] = { ref = "1.0.0" }, ["1.0.0"] = { } }, + macosx = { ["latest"] = { ref = "1.0.0" }, ["1.0.0"] = { } }, + windows = { ["latest"] = { ref = "1.0.0" }, ["1.0.0"] = { } }, + }, + mcpp = { + language = "c++23", + import_std = false, + sources = { "src/hookprobe.cppm" }, + targets = { ["hookprobe"] = { kind = "lib" } }, + deps = {}, + }, +} + +import("xim.libxpkg.pkginfo") + +function install() + local dir = pkginfo.install_dir() + os.mkdir(path.join(dir, "src")) + local function value(name) return os.getenv(name) or "" end + io.writefile(path.join(dir, "src", "hookprobe.cppm"), string.format([[ +export module hookprobe; +export const char* hook_compiler() { return "%s"; } +export const char* hook_stdlib() { return "%s"; } +export const char* hook_target_os() { return "%s"; } +export const char* hook_target() { return "%s"; } +]], value("MCPP_COMPILER"), value("MCPP_CXX_STDLIB"), value("MCPP_TARGET_OS"), + value("MCPP_TARGET"))) + return true +end +)cpp"}, + {"src/main.cpp", R"cpp(#include +import hookprobe; +int main() { + std::printf("compiler=%s stdlib=%s os=%s target=%s\n", + hook_compiler(), hook_stdlib(), hook_target_os(), hook_target()); + return 0; +} +)cpp"}, + {"mcpp.toml", R"cpp([package] +name = "consumer" +version = "0.1.0" + +[dependencies.acme] +hookprobe = "1.0.0" + +[indices] +acme = { path = "local-index" } +)cpp"}}}; + auto r = p.mcpp({"run"}, {.where = s::home::inherited, .env = {{"MCPP_COMPILER", "inherited"}, {"MCPP_CXX_STDLIB", "inherited"}}}); + expect(fatal(e2e::succeeded(r))) << "the build or the run failed"; + expect(s::has_line(s::text(r), "^compiler= stdlib= os=linux target=[a-z0-9_]+-linux")) << "the install hook did not see the build's target with empty toolchain names"; + }; +}}; + +} // namespace diff --git a/tests/e2e/actions/outputs.cpp b/tests/e2e/actions/outputs.cpp new file mode 100644 index 000000000..a1ffdc420 --- /dev/null +++ b/tests/e2e/actions/outputs.cpp @@ -0,0 +1,227 @@ +// Where actions write and what they read: placements staged by one process, an output's one +// writer, the out_dir of each configuration, the build's contract (formerly tests/e2e/827, 896, +// 897, 982). +import std; +import mcpplibs.mtest; +import mcpp.test.support; + +using namespace mcpplibs::mtest; +namespace s = mcpp::test::support; +namespace fs = std::filesystem; + +namespace { + +std::optional ninja_file(const fs::path& target) { + for (const auto& f : s::find_files(target, R"(build\.ninja)")) + if (f.generic_string().find("/.build-mcpp/") == std::string::npos) return f; + return std::nullopt; +} + +// The first file under `root` whose generic path ends with `suffix` (`find -path '*'`). +std::optional ending(const fs::path& root, std::string_view name, std::string_view suffix) { + for (const auto& f : s::find_files(root, name)) + if (f.generic_string().ends_with(suffix)) return f; + return std::nullopt; +} + +E2E outputs{"outputs", [] { + "827 many placements run as one process"_test[tag{"requires:unix-shell"}] = [] { + // The files a program's runtime needs beside it are placed by one `stage_list` edge, not + // one per file; the edge does not run when no placement changed; a changed source + // arrives and an unchanged destination is not rewritten; one entry keeps its own edge. + s::needs({"unix-shell"}); + s::project p{s::files{{"src/main.cpp", "int main() { return 0; }\n"}, + {"mcpp.toml", "[package]\nname = \"place827\"\nversion = \"0.1.0\"\n"}}}; + for (int i = 1; i <= 50; ++i) p.write(std::format("data/f{}.txt", i), std::format("{}\n", i)); + auto program = [&](int n) { + p.write("build.mcpp", std::format(R"cpp(import std; +import mcpp.core; +int main() {{ + for (int i = 1; i <= {}; ++i) {{ + const std::string f = "data/f" + std::to_string(i) + ".txt"; + mcpp::deploy(f.c_str(), "data"); + }} + return 0; +}} +)cpp", n)); + }; + auto touch_main = [&] { fs::last_write_time(p.at("src/main.cpp"), fs::file_time_type::clock::now()); }; + program(50); + expect(fatal(e2e::succeeded(p.mcpp({"build"})))) << "S1: the build failed"; + auto nj = ninja_file(p.at("target")); + expect(fatal(nj.has_value())) << "S1: no build.ninja"; + expect(eq(s::count_lines(s::read_file(*nj), ": stage_list "), std::size_t{1})) << "S1: not exactly one stage_list edge"; + expect(s::no_line(s::read_file(*nj), ": stage_file .*data")) << "S1: a per-file placement edge remains"; + const auto out = nj->parent_path(); + std::size_t placed = 0; + for (const auto& f : s::find_files(out, R"(f[0-9]+\.txt)")) + placed += f.generic_string().find("data/f") != std::string::npos ? 1 : 0; + const auto in_bin = fs::is_directory(out / "bin/data") ? std::ranges::distance(fs::directory_iterator(out / "bin/data"), fs::directory_iterator{}) : 0; + expect(in_bin == 50 || placed >= 50) << "S1: not every file was placed"; + const auto dest1 = ending(out, R"(f1\.txt)", "/data/f1.txt"); + const auto dest2 = ending(out, R"(f2\.txt)", "/data/f2.txt"); + expect(fatal(dest1 && dest2)) << "S1: the placed files are not found under " << out.string(); + auto ran = [&] { return s::count_matching(s::read_file(out / ".ninja_log"), "placements|data/f"); }; + const auto before = ran(); + touch_main(); + expect(fatal(e2e::succeeded(p.mcpp({"build"})))) << "S2: the build failed"; + expect(eq(ran(), before)) << "S2: a build with no placement change ran the placement edge"; + const auto ref = fs::last_write_time(*dest2); + std::this_thread::sleep_for(std::chrono::milliseconds{1100}); + p.write("data/f1.txt", "changed\n"); + touch_main(); + expect(fatal(e2e::succeeded(p.mcpp({"build"})))) << "S3: the build failed"; + expect(s::has_line(s::read_file(*dest1), "^changed$")) << "S3: the changed source did not arrive"; + expect(fs::last_write_time(*dest2) <= ref) << "S3: an unchanged destination was rewritten"; + program(1); + expect(fatal(e2e::succeeded(p.mcpp({"build"})))) << "S4: the build failed"; + nj = ninja_file(p.at("target")); + expect(fatal(nj.has_value())) << "S4: no build.ninja"; + expect(s::lacks(s::read_file(*nj), ": stage_list ")) << "S4: one entry produced a list edge"; + expect(s::has_line(s::read_file(*nj), R"(: stage_file .*f1.txt)")) << "S4: the single entry lost its per-file edge"; + }; + + "896 an action output has one writer"_test[tag{"requires:gcc"}][tag{"requires:unix-shell"}] = [] { + // An action's output is written by the action alone: prepare writes no placeholder, a + // removed output is generated again, and an existing (empty) output is not rewritten. + s::needs({"gcc", "unix-shell"}); + const s::options inherited{.where = s::home::inherited}; + s::project ws{s::files{{"mcpp.toml", "[workspace]\nmembers = [\"app\"]\n"}, + {"app/mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.0.1\"\n[build]\nsources = []\n[targets.app]\nkind = \"bin\"\nmain = \"main.cpp\"\n"}, + {"app/main.cpp", "extern const char* resource();\nint main() { return resource()[0] == 'R' ? 0 : 1; }\n"}, + {"app/gen.sh", "#!/bin/sh\nprintf 'const char* resource() { return \"RESOURCE\"; }\\n' > \"$1\"\nprintf \"\" > \"$2\"\n"}, + {"app/build.mcpp", R"cpp(import std; +import mcpp; +int main() { + const auto root = std::filesystem::path(mcpp::manifest_dir()); + const auto out = std::filesystem::path(mcpp::out_dir()); + const auto gen = (root / "gen.sh").generic_string(); + const auto cpp = (out / "generated.cpp").generic_string(); + const auto empty = (out / "empty.cpp").generic_string(); + mcpp::action a; + a.id = "generate:resource"; + a.role = mcpp::roles::source; + a.arg("sh").arg(gen.c_str()).arg(cpp.c_str()).arg(empty.c_str()) + .input(gen.c_str()).output(cpp.c_str()).output(empty.c_str()).submit(); + return 0; +} +)cpp"}}}; + expect(fatal(e2e::succeeded(ws.mcpp({"build", "-p", "app", "--configure-only"}, inherited)))) << "configure-only"; + expect(fatal(!s::find_file(ws.dir(), "generated.cpp").has_value())) << "prepare wrote the action's output"; + expect(fatal(e2e::succeeded(ws.mcpp({"build", "-p", "app"}, inherited)))) << "first build"; + const auto gen = ending(ws.dir(), R"(generated\.cpp)", "/generated.cpp"); + expect(fatal(gen && gen->generic_string().find("/out/") != std::string::npos && fs::file_size(*gen) > 0)) << "the action did not write its output"; + fs::remove(*gen); + expect(fatal(e2e::succeeded(ws.mcpp({"build", "-p", "app"}, inherited)))) << "the build after the generated file was removed failed (#778)"; + expect(fatal(fs::exists(*gen) && fs::file_size(*gen) > 0)) << "the generated file was not generated again"; + std::optional empty; + for (const auto& f : s::find_files(ws.dir(), R"(empty\.cpp)")) + if (!empty && f.generic_string().find("/out/") != std::string::npos) empty = f; + expect(fatal(empty && fs::file_size(*empty) == 0)) << "the action's empty output is missing or not empty"; + const auto before = fs::last_write_time(*empty); + std::this_thread::sleep_for(std::chrono::seconds{1}); + expect(fatal(e2e::succeeded(ws.mcpp({"build", "-p", "app"}, inherited)))) << "the third build"; + expect(fs::last_write_time(*empty) == before) << "an existing output was rewritten by prepare"; + }; + + "897 each configuration has its own generated output"_test[tag{"requires:gcc"}][tag{"requires:unix-shell"}] = [] { + // `mcpp::out_dir()` is per configuration and resolution.json records it: after dev, + // release, dev, each configuration's binary prints what its own out_dir was generated + // for, and dev and release do not share one. + s::needs({"gcc", "unix-shell"}); + const s::options inherited{.where = s::home::inherited}; + s::project p{s::files{{"mcpp.toml", "[package]\nname = \"gp\"\nversion = \"0.1.0\"\n[build]\nsources = [\"src/main.cpp\"]\n[targets.gp]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n"}, + {"src/main.cpp", "#include \nconst char* gen_profile();\nint main() { std::printf(\"GEN=%s\\n\", gen_profile()); }\n"}, + {"gen.sh", "#!/bin/sh\nprintf 'const char* gen_profile() { return \"%s\"; }\\n' \"$1\" > \"$2\"\n"}, + {"build.mcpp", R"cpp(import std; +import mcpp; +int main() { + const auto root = std::filesystem::path(mcpp::manifest_dir()); + const auto out = (std::filesystem::path(mcpp::out_dir()) / "gen.cpp").generic_string(); + const auto gen = (root / "gen.sh").generic_string(); + mcpp::action a; + a.id = "gen"; + a.role = mcpp::roles::source; + a.arg("sh").arg(gen.c_str()).arg(mcpp::profile()).arg(out.c_str()) + .input(gen.c_str()).output(out.c_str()).submit(); + return 0; +} +)cpp"}}}; + expect(fatal(e2e::succeeded(p.mcpp({"build"}, inherited)))) << "dev"; + expect(fatal(e2e::succeeded(p.mcpp({"build", "--release"}, inherited)))) << "release"; + expect(fatal(e2e::succeeded(p.mcpp({"build"}, inherited)))) << "dev"; + std::map seen; // profile -> out_dir + for (const auto& r : s::find_files(p.at("target"), R"(resolution\.json)")) { + if (r.generic_string().find("/.build-mcpp/") != std::string::npos) continue; + std::string out; + for (const auto& pkg : s::json_file(r)["graph"]["packages"]) + if (pkg.value("root", false)) { out = pkg.value("outDir", ""); break; } + expect(fatal(!out.empty())) << "resolution.json records no outDir: " << r.string(); + const auto gen = fs::path(out) / "gen.cpp"; + expect(fatal(fs::exists(gen) && fs::file_size(gen) > 0)) << "outDir " << out << " does not hold the generated file"; + std::smatch m; + const auto text = s::read_file(gen); + const std::string profile = std::regex_search(text, m, std::regex{R"re("([a-z]*)")re"}) ? m[1].str() : ""; + auto got = s::run(r.parent_path() / "bin/gp").out; + std::erase(got, '\r'); + expect(eq(s::last_line(got), "GEN=" + profile)) << "the binary in " << r.parent_path().string() << " and its outDir disagree"; + seen[profile] = out; + } + expect(fatal(seen.contains("dev") && seen.contains("release"))) << "expected one dev and one release configuration"; + expect(seen["dev"] != seen["release"]) << "dev and release share one out_dir: " << seen["dev"]; + }; + + "982 an action reads the builds contract"_test[tag{"requires:elf"}] = [] { + // `${mcpp.contract}` names the configuration's contract beside its compile database: + // each language's compiler and the words every unit gets (no dialect or module flags), + // the link driver, its words and the archiver, without a package's own link inputs — + // which still reach the consumer. + s::needs({"elf"}); + s::project p{s::files{{"mcpp.toml", "[package]\nname = \"contract\"\nversion = \"0.1.0\"\n"}, + {"src/main.cpp", "int main() { return 0; }\n"}, + {"build.mcpp", R"cpp(#include +import mcpp; +int main() { + mcpp::link_flag("-Wl,--defsym,mcpp_contract_consumer=1"); + const std::string out = std::string(mcpp::out_dir()) + "/seen-contract.json"; + mcpp::action a; + a.id = "copy-contract"; + a.role = mcpp::roles::check; + a.arg("cp").arg("${mcpp.contract}").arg(out.c_str()) + .output(out.c_str()) + .submit(); +} +)cpp"}}}; + expect(fatal(e2e::succeeded(p.mcpp({"build"})))) << "build failed"; + const auto seen = s::find_file(p.at("target"), "seen-contract.json"); + expect(fatal(seen.has_value())) << "the action did not run"; + const auto d = s::json_file(*seen); + expect(eq(d["schema"].get(), 1)) << d.dump(); + expect(!d["target"].get().empty()) << d.dump(); + for (std::string_view lang : {"c", "cxx", "asm"}) { + const auto& t = d["compile"][std::string(lang)]; + expect(fs::path(t["program"].get()).is_absolute() && t["args"].is_array()) << lang << ": " << t.dump(); + } + bool optimised = false, dialect = false; + for (const auto& a : d["compile"]["cxx"]["args"]) { + const auto w = a.get(); + optimised = optimised || w.starts_with("-O"); + dialect = dialect || w.starts_with("-std=") || w == "-fmodules" || w.starts_with("-fmodule-"); + } + expect(optimised) << d["compile"]["cxx"]["args"].dump(); + expect(!dialect) << d["compile"]["cxx"]["args"].dump(); + expect(fs::path(d["link"]["program"].get()).is_absolute() && d["link"]["args"].is_array()) << d["link"].dump(); + expect(d["link"].contains("archiver")) << d["link"].dump(); + bool leaked = false; + for (const auto& a : d["link"]["args"]) leaked = leaked || a.get().find("mcpp_contract_consumer") != std::string::npos; + expect(!leaked) << d["link"].dump(); + std::optional db; + for (const auto& f : s::find_files(p.at("target"), R"(compile_commands\.json)")) + if (!db && f.generic_string().find("/.build-mcpp/") == std::string::npos) db = f; + expect(fatal(db.has_value())) << "no compile database"; + expect(fs::exists(db->parent_path() / "mcpp-contract.json")) << "no contract beside the compile database"; + expect(e2e::contains(s::read_file(db->parent_path() / "build.ninja"), "mcpp_contract_consumer=1")) << "excluding package link inputs from the contract removed them from the consumer"; + }; +}}; + +} // namespace diff --git a/tests/e2e/actions/prepare.cpp b/tests/e2e/actions/prepare.cpp new file mode 100644 index 000000000..eaa0b2a49 --- /dev/null +++ b/tests/e2e/actions/prepare.cpp @@ -0,0 +1,217 @@ +// The `prepare` role and the roles an action may name (formerly tests/e2e/790, 791, 792, 793). +import std; +import mcpplibs.mtest; +import mcpp.test.support; + +using namespace mcpplibs::mtest; +namespace s = mcpp::test::support; +namespace fs = std::filesystem; + +namespace { + +bool failed(const e2e::outcome& r) { return r.started && r.status != 0; } + +constexpr std::string_view prep_manifest = "[package]\nname = \"prep\"\nversion = \"0.1.0\"\n\n[toolchain]\nlinux = \"gcc@16.1.0\"\n\n[targets.prep]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n"; + +E2E prepare{"prepare", [] { + "790 a prepare action populates an unknown directory"_test[tag{"requires:gcc"}] = [] { + // A `prepare` action declares only the directory a command fills; the build reads it by + // directory. It runs on the first build, not on an unchanged one (nor does the compile), + // again when an input changes, and not on the build after it passed. + s::needs({"gcc"}); + s::project p{s::files{{"mcpp.toml", prep_manifest}, + {"recipe.txt", "recipe v1\n"}, + {"src/main.cpp", "#include \nint main() { return prep_greet() == 99 ? 0 : 1; }\n"}, + {"build.mcpp", R"cpp(import mcpp; +#include +int main() { + const std::string root = mcpp::manifest_dir(); + const std::string prefix = std::string(mcpp::out_dir()) + "/prep-install"; + + mcpp::action a; + a.id = "prep:install"; + a.role = mcpp::roles::prepare; + a.arg((root + "/install.sh").c_str()).arg(prefix.c_str()) + .input((root + "/install.sh").c_str()) + .input((root + "/recipe.txt").c_str()) + .output((prefix + ".stamp").c_str()) + .output_dir(prefix.c_str()) + .submit(); + + mcpp::include_dir((prefix + "/include").c_str()); + mcpp::link_lib("prepgreet"); + mcpp::link_search((prefix + "/lib").c_str()); + mcpp::runtime_search_dir((prefix + "/lib").c_str()); + return 0; +} +)cpp"}}}; + s::write_executable(p.at("install.sh"), R"cpp(#!/usr/bin/env bash +set -e +PREFIX="$1" +mkdir -p "$PREFIX/include" "$PREFIX/lib" +cat > "$PREFIX/include/greet.h" < "$PREFIX/lib/greet.c" <\nint main(){ std::printf(\"ok\\n\"); }\n"}}}; + for (std::string_view where : {"beside", "inside"}) { + p.remove("target"); + p.write("build.mcpp", std::format(R"cpp(import mcpp; +#include +int main() {{ + const std::string prefix = std::string(mcpp::out_dir()) + "/never-created"; + const std::string stamp = std::string("{}") == "inside" + ? prefix + "/installed.stamp" : prefix + ".stamp"; + mcpp::action a; + a.id = "empty:prepare"; + a.role = mcpp::roles::prepare; + a.arg("true").output(stamp.c_str()).output_dir(prefix.c_str()).submit(); + return 0; +}} +)cpp", where)); + auto b = p.mcpp({"build"}); + expect(fatal(failed(b))) << "(" << where << ") a prepare action whose command created nothing let the build succeed"; + expect(e2e::contains(s::text(b), "never-created")) << "(" << where << ") the failure does not name the declared directory"; + expect(s::contains_nocase(s::text(b), "does not exist or holds no file")) << "(" << where << ") the failure does not state the post-condition"; + bool stamped = false; + for (const auto& f : s::find_files(p.at("target"), R"(.*\.stamp)")) + stamped = stamped || f.generic_string().find("never-created") != std::string::npos; + expect(!stamped) << "(" << where << ") a stamp was written although the directory holds nothing"; + } + }; + + "792 a rerun input inside a prepare directory is warned"_test[tag{"requires:gcc"}] = [] { + // A build program's re-run input inside a prepare action's directory (a file or a glob) + // is warned about, naming the path, the directory and the package; one outside is not. + s::needs({"gcc"}); + s::project p{s::files{{"mcpp.toml", prep_manifest}, + {"src/main.cpp", "#include \nint main(){ std::printf(\"ok\\n\"); }\n"}}}; + s::write_executable(p.at("install.sh"), "#!/usr/bin/env bash\nset -e\nPREFIX=\"$1\"\nmkdir -p \"$PREFIX/include\"\necho '#define GREETING 1' > \"$PREFIX/include/greet.h\"\n"); + auto build = [&](std::string_view call) { + p.write("build.mcpp", std::format(R"cpp(import mcpp; +#include +int main() {{ + const std::string root = mcpp::manifest_dir(); + const std::string prefix = std::string(mcpp::out_dir()) + "/prep-install"; + + mcpp::action a; + a.id = "prep:install"; + a.role = mcpp::roles::prepare; + a.arg((root + "/install.sh").c_str()).arg(prefix.c_str()) + .input((root + "/install.sh").c_str()) + .output((prefix + ".stamp").c_str()) + .output_dir(prefix.c_str()) + .submit(); + + {} + + return 0; +}} +)cpp", call)); + p.remove("target"); + return p.mcpp({"build"}); + }; + auto b1 = build("mcpp::rerun_if_changed((prefix + \"/include/greet.h\").c_str());"); + expect(fatal(e2e::succeeded(b1))) << "build with rerun_if_changed inside the prepare dir failed"; + expect(s::contains_nocase(s::text(b1), "prepare")) << "no warning mentions the prepare action"; + expect(e2e::contains(s::text(b1), "include/greet.h")) << "the warning does not name the re-run path"; + expect(e2e::contains(s::text(b1), "prep-install")) << "the warning does not name the prepare directory"; + expect(e2e::contains(s::text(b1), "prep")) << "the warning does not name the declaring package"; + auto b2 = build("mcpp::rerun_if_changed_glob((prefix + \"/include/*.h\").c_str());"); + expect(fatal(e2e::succeeded(b2))) << "build with rerun_if_changed_glob inside the prepare dir failed"; + expect(s::contains_nocase(s::text(b2), "prepare")) << "no warning mentions the prepare action (glob case)"; + expect(e2e::contains(s::text(b2), "prep-install")) << "the glob warning does not name the prepare directory"; + auto b3 = build("mcpp::rerun_if_changed((root + \"/install.sh\").c_str());"); + expect(fatal(e2e::succeeded(b3))) << "control build failed"; + expect(!s::contains_nocase(s::text(b3), "prepare")) << "a re-run input OUTSIDE any prepare directory was warned about"; + }; + + "793 an unknown action role is refused"_test[tag{"requires:gcc"}] = [] { + // A role naming none of the five is refused naming the value and the list; each of the + // five is accepted. + s::needs({"gcc"}); + s::project p{s::files{{"mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n"}, + {"src/main.cpp", "#include \nint main(){ std::printf(\"ok\\n\"); }\n"}}}; + auto program = [](std::string_view role) { + return std::format(R"cpp(import mcpp; +#include +int main() {{ + mcpp::action a; + a.id = "{}"; + a.role = "{}"; + a.arg("true") + .output((std::string(mcpp::out_dir()) + "/{}.stamp").c_str()) + .submit(); + return 0; +}} +)cpp", role == "prepar" ? "typo" : "ctrl", role, role == "prepar" ? "typo" : "ctrl"); + }; + p.write("build.mcpp", program("prepar")); + auto b = p.mcpp({"build"}); + expect(fatal(failed(b))) << "an action with an unrecognised role let the build succeed"; + expect(e2e::contains(s::text(b), "\"prepar\"")) << "the refusal does not name the misspelt role"; + for (std::string_view role : {"source", "check", "object", "artifact", "prepare"}) + expect(e2e::contains(s::text(b), role)) << "the refusal does not list role '" << role << "'"; + p.write("extra.c", "int ctrl_unused_symbol(void) { return 0; }\n"); + for (std::string_view role : {"source", "check", "artifact"}) { + p.write("build.mcpp", program(role)); + p.remove("target"); + expect(e2e::succeeded(p.mcpp({"build"}))) << "a well-known role '" << role << "' was refused"; + } + p.write("build.mcpp", R"cpp(import mcpp; +#include +int main() { + const std::string root = mcpp::manifest_dir(); + const std::string obj = std::string(mcpp::out_dir()) + "/extra.o"; + mcpp::action a; + a.id = "ctrl-object"; + a.role = "object"; + a.arg("gcc").arg("-c").arg((root + "/extra.c").c_str()).arg("-o").arg(obj.c_str()) + .input((root + "/extra.c").c_str()) + .output(obj.c_str()) + .submit(); + return 0; +} +)cpp"); + p.remove("target"); + expect(e2e::succeeded(p.mcpp({"build"}))) << "the well-known role 'object' was refused"; + }; +}}; + +} // namespace diff --git a/tests/e2e/1013_optional_action_progress_preserves_outputs.sh b/tests/e2e/actions/progress.cpp old mode 100755 new mode 100644 similarity index 56% rename from tests/e2e/1013_optional_action_progress_preserves_outputs.sh rename to tests/e2e/actions/progress.cpp index 61de545fb..ac3e38c1c --- a/tests/e2e/1013_optional_action_progress_preserves_outputs.sh +++ b/tests/e2e/actions/progress.cpp @@ -1,14 +1,18 @@ -#!/usr/bin/env bash -# requires: gcc unix-shell python3 -set -e -export MCPP="${MCPP:-mcpp}" -work=$(mktemp -d) -trap 'rm -rf "$work"' EXIT -export MCPP_HOME="$work/home" -source "$(dirname "$0")/_inherit_toolchain.sh" -export PROGRESS_TEST_WORK="$work" -python3 - <<'PY' -from pathlib import Path +// Action progress: an optional execution channel that preserves every role's outputs and +// results, and its paths on every host (formerly tests/e2e/1013, 1015). The action workers are +// Python programs and the first case needs a terminal and a process group, so each case hands its +// script's Python driver to python3, unchanged. +import std; +import mcpplibs.mtest; +import mcpp.test.support; + +using namespace mcpplibs::mtest; +namespace s = mcpp::test::support; +namespace fs = std::filesystem; + +namespace { + +constexpr std::string_view preserves = R"py(from pathlib import Path import json,os,pty,subprocess,sys,time,select,fcntl,termios,struct,signal work=Path(os.environ['PROGRESS_TEST_WORK']);app=work/'app';(app/'src').mkdir(parents=True) (app/'mcpp.toml').write_text('[package]\nname="action-progress"\nversion="0.1.0"\n') @@ -119,4 +123,78 @@ with (work/'cancel.log').open('w') as log: assert process.wait(timeout=15)!=0,'cancelled build succeeded' assert not channel.exists(),'cancelled execution channel remained' print('PASS: optional action progress preserves all role outputs and command results') -PY +)py"; + +constexpr std::string_view portable = R"py(from pathlib import Path +import json,os,subprocess,sys,tempfile +with tempfile.TemporaryDirectory(prefix='mcpp-progress-') as temporary: + root=Path(temporary)/'项目-进度';(root/'src').mkdir(parents=True) + (root/'mcpp.toml').write_text('[package]\nname="portable-progress"\nversion="0.1.0"\n',encoding='utf-8') + (root/'src/main.cpp').write_text('int main(){return 0;}\n',encoding='utf-8') + worker=root/'worker.py';proof=root/'target/proof.json' + worker.write_text('''from pathlib import Path +import json,os,sys,time +file=os.environ.get('MCPP_ACTION_PROGRESS_FILE','') +assert file, 'the enabled action has no execution channel' +p=Path(file) +for seq in range(2): + state=dict(v=1,seq=seq,phase='compile',done=seq,total=1) + staged=p.with_suffix('.tmp');staged.write_text(json.dumps(state),encoding='utf-8') + os.replace(staged,p);time.sleep(.25) +Path(sys.argv[1]).write_text(json.dumps(dict(channel=file,snapshot=state)),encoding='utf-8') +''',encoding='utf-8') + quote=lambda value:json.dumps(str(value),ensure_ascii=False) + (root/'build.mcpp').write_text('''import std; +import mcpp; +int main(){ + mcpp::action action; + action.id="portable-worker";action.role=mcpp::roles::check;action.progress=true; + const auto stamp=std::string(mcpp::out_dir())+"/worker.stamp"; + action.arg(PYTHON).arg(WORKER).arg(PROOF).input(WORKER).output(stamp.c_str()); + action.submit(); +} +'''.replace('PYTHON',quote(sys.executable)).replace('WORKER',quote(worker)).replace('PROOF',quote(proof)),encoding='utf-8') + exe=os.environ['MCPP_PROGRESS_TEST_EXE'] + result=subprocess.run([exe,'build'],cwd=root,env=dict(os.environ,MCPP_PROGRESS='plain'),capture_output=True,text=True,encoding='utf-8',errors='replace',timeout=180) + assert result.returncode==0,result.stdout+result.stderr + result=json.loads(proof.read_text(encoding='utf-8')) + assert '项目-进度' in result['channel'],result + assert result['snapshot']==dict(v=1,seq=1,phase='compile',done=1,total=1),result + assert not Path(result['channel']).exists(),'execution channel was not cleaned' +print('PASS: native action progress paths are UTF-8 and cleaned') +)py"; + +// python3 running `driver` (written to the case's directory) with `env`. +e2e::outcome python(std::string_view driver, std::vector>> env) { + const auto python3 = s::on_path("python3"); + expect(fatal(python3.has_value())) << "python3 is not on PATH"; + const auto file = temp_dir() / "driver.py"; + s::project{file.parent_path(), {}}.write(file.filename().string(), driver); + return e2e::run({.argv = {python3->string(), file.string()}, .env = std::move(env), .timeout = std::chrono::minutes{10}}); +} + +E2E progress{"progress", [] { + "1013 optional action progress preserves outputs"_test[tag{"requires:gcc"}][tag{"requires:unix-shell"}][tag{"requires:python3"}] = [] { + // An action that reports progress through its execution channel, for every role: the + // channel is shown through a terminal (plain and verbose) and removed after; turning it + // off leaves the outputs usable; a failure after a complete count is a failure; a + // cancelled build removes its channel. + s::needs({"gcc", "unix-shell", "python3"}); + const auto work = temp_dir() / "work"; + fs::create_directories(work); + auto r = python(preserves, {{"MCPP", s::mcpp_path()}, {"MCPP_HOME", s::case_home(s::home::inherited).string()}, {"PROGRESS_TEST_WORK", work.string()}}); + expect(e2e::succeeded(r)) << "the progress driver failed"; + expect(e2e::contains(r.out, "PASS: optional action progress preserves all role outputs and command results")) << "the driver did not finish"; + }; + + "1015 action progress paths are utf8 on every host"_test[tag{"requires:python3"}] = [] { + // The worker, environment and file transport of action progress in a non-ASCII project + // directory, without a terminal or signals, so every host runs it. + s::needs({"python3"}); + auto r = python(portable, {{"MCPP_PROGRESS_TEST_EXE", fs::path(s::mcpp_path()).make_preferred().string()}}); + expect(e2e::succeeded(r)) << "the progress driver failed"; + expect(e2e::contains(r.out, "PASS: native action progress paths are UTF-8 and cleaned")) << "the driver did not finish"; + }; +}}; + +} // namespace diff --git a/tests/e2e/actions/rules.cpp b/tests/e2e/actions/rules.cpp new file mode 100644 index 000000000..ada15c31e --- /dev/null +++ b/tests/e2e/actions/rules.cpp @@ -0,0 +1,226 @@ +// Rule packages: a collection whose rules a consumer selects by feature, the tools a rule may +// declare and when, the environment a rule brings, the sources a rule claims (formerly +// tests/e2e/610, 619, 630, 798, 808). +import std; +import mcpplibs.mtest; +import mcpp.test.support; + +using namespace mcpplibs::mtest; +namespace s = mcpp::test::support; +namespace fs = std::filesystem; + +namespace { + +bool failed(const e2e::outcome& r) { return r.started && r.status != 0; } + +// examples/12-a-new-device-language copied into the case, without build trees or locks +// (`find -maxdepth 3`). +fs::path example_copy() { + const auto src = s::repository() / "examples/12-a-new-device-language"; + expect(fatal(fs::is_directory(src))) << src.string() << " is missing"; + const auto ex = temp_dir() / "ex"; + fs::copy(src, ex, fs::copy_options::recursive); + std::vector doomed; + for (auto it = fs::recursive_directory_iterator(ex); it != fs::recursive_directory_iterator(); ++it) { + const auto depth = it.depth() + 1; + if (depth > 3) { it.disable_recursion_pending(); continue; } + const auto name = it->path().filename(); + if ((it->is_directory() && name == "target") || name == "mcpp.lock") { + doomed.push_back(it->path()); + if (it->is_directory()) it.disable_recursion_pending(); + } + } + for (const auto& d : doomed) fs::remove_all(d); + return ex; +} + +E2E rules{"rules", [] { + "610 feature controlled rule collection"_test[tag{"requires:gcc"}] = [] { + // One package, several rules, the set chosen by the consumer's feature request: an + // activated feature's unit is importable under its declared name, an inactive one is + // not compiled; the mcpp namespace draws no reserved-prefix warning, another draws one + // per unit; a lib whose only unit implements an undeclared module keeps its diagnostic. + s::needs({"gcc"}); + const auto tmp = temp_dir(); + auto collection = [&](std::string_view ns) { + fs::remove_all(tmp / "plugins"); + s::project p{tmp / "plugins", {{"src/plugins.cppm", "export module mcpp.plugins;\nimport std;\nexport namespace mcpp::plugins {\ninline constexpr std::string_view version = \"0.1.0\";\n}\n"}, + {"rules/a.cppm", R"cpp(export module mcpp.rules.a; +import std; +import mcpp; +import mcpp.plugins; // the lib root precedes every feature unit +export namespace mcpp::rules::a { +inline void apply() { + mcpp::cxxflag("-DRULE_A=1"); + // Unquoted on purpose: the token is stringized by the consumer, and a + // quoted value would have to survive two layers of shell quoting. + mcpp::cxxflag(std::format("-DPLUGINS_VERSION={}", mcpp::plugins::version).c_str()); +} +} +)cpp"}, + {"rules/b.cppm", "export module mcpp.rules.b;\nimport std;\nimport mcpp;\nexport namespace mcpp::rules::b {\ninline void apply() { mcpp::cxxflag(\"-DRULE_B=1\"); }\n}\n"}}}; + p.write("mcpp.toml", std::format("[package]\nname = \"plugins\"\nnamespace = \"{}\"\nversion = \"0.1.0\"\n[language]\nstandard = \"c++23\"\n[build]\nsources = [\"src/plugins.cppm\"]\n[features]\nrules-a = {{ sources = [\"rules/a.cppm\"] }}\nrules-b = {{ sources = [\"rules/b.cppm\"] }}\n[targets.plugins]\nkind = \"lib\"\n", ns)); + }; + auto app = [&](std::string_view ns, std::string_view features, std::vector imports) { + fs::remove_all(tmp / "app"); + s::project a{tmp / "app", {{"src/main.cpp", R"cpp(#include +#ifndef RULE_A +#define RULE_A 0 +#endif +#ifndef RULE_B +#define RULE_B 0 +#endif +#ifndef PLUGINS_VERSION +#define PLUGINS_VERSION none +#endif +#define STR_(x) #x +#define STR(x) STR_(x) +int main() { std::printf("A=%d B=%d V=%s\n", RULE_A, RULE_B, STR(PLUGINS_VERSION)); } +)cpp"}}}; + a.write("mcpp.toml", std::format("[package]\nname = \"app\"\nversion = \"0.1.0\"\n[language]\nstandard = \"c++23\"\n[dependencies.{}]\nplugins = {{ path = \"../plugins\", features = [{}], host-module = true }}\n", ns, features)); + std::string program = "import std;\nimport mcpp;\n"; + for (const auto& m : imports) program += std::format("import {};\n", m); + program += "int main() {\n"; + for (const auto& m : imports) { + std::string qualified; // `${m//./::}` + for (char c : m) qualified += c == '.' ? std::string("::") : std::string(1, c); + program += std::format(" {}::apply();\n", qualified); + } + program += "}\n"; + a.write("build.mcpp", program); + return a; + }; + const std::string reserved = "belongs to the modules maintained by the mcpp project"; + collection("mcpp"); + auto a1 = app("mcpp", "\"rules-a\"", {"mcpp.rules.a"}); + auto b1 = a1.mcpp({"build"}); + expect(fatal(e2e::succeeded(b1))) << "a feature-selected rule did not build"; + expect(eq(s::last_starting(a1.mcpp({"run"}).out, "A="), std::string{"A=1 B=0 V=0.1.0"})) << "expected A=1 B=0 V=0.1.0"; + expect(s::lacks(s::text(b1), reserved)) << "the mcpp namespace drew a reserved-prefix warning"; + auto a2 = app("mcpp", "\"rules-a\"", {"mcpp.rules.b"}); + auto b2 = a2.mcpp({"build"}); + expect(fatal(failed(b2))) << "a rule whose feature is not active was importable"; + expect(e2e::contains(s::text(b2), "mcpp.rules.b")) << "the refusal does not name the module"; + auto a3 = app("mcpp", "\"rules-a\", \"rules-b\"", {"mcpp.rules.a", "mcpp.rules.b"}); + expect(fatal(e2e::succeeded(a3.mcpp({"build"})))) << "two feature-selected rules did not build"; + expect(eq(s::last_starting(a3.mcpp({"run"}).out, "A="), std::string{"A=1 B=1 V=0.1.0"})) << "expected A=1 B=1 V=0.1.0"; + collection("acme"); + auto a4 = app("acme", "\"rules-a\", \"rules-b\"", {"mcpp.rules.a", "mcpp.rules.b"}); + auto b4 = a4.mcpp({"build"}); + expect(fatal(e2e::succeeded(b4))) << "the acme collection did not build"; + expect(eq(s::count_lines(s::text(b4), reserved), std::size_t{3})) << "expected 3 reserved-prefix warnings (one per unit)"; + + fs::remove_all(tmp / "plugins"); + fs::remove_all(tmp / "app"); + s::project{tmp / "plugins", {{"mcpp.toml", "[package]\nname = \"plugins\"\nversion = \"0.1.0\"\n[build]\nsources = [\"rules/*.cppm\"]\n[targets.plugins]\nkind = \"lib\"\n"}, + {"rules/impl.cppm", "module nothing.exported; // an implementation unit is not an interface\n"}}}; + s::project a5{tmp / "app", {{"mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n[dependencies]\nplugins = { path = \"../plugins\", host-module = true }\n"}, + {"build.mcpp", "import mcpp;\nint main() {}\n"}, + {"src/main.cpp", "int main() {}\n"}}}; + auto b5 = a5.mcpp({"build"}); + expect(fatal(failed(b5))) << "a library whose only unit implements a module no unit declares built"; + expect(e2e::contains(s::text(b5), "nothing.exported")) << "the diagnostic does not name the module"; + expect(e2e::contains(s::text(b5), "rules/impl.cppm")) << "the diagnostic does not name the unit"; + expect(e2e::contains(s::text(b5), "the build programs' workspace")) << "the diagnostic does not say where to reproduce it"; + }; + + "619 a tool may not be conditioned on a layer"_test[tag{"requires:gcc"}] = [] { + // `[target..xlings…]` refuses a predicate naming a target-side layer, naming + // the tool, the predicate and the feature-xlings way out. + s::needs({"gcc"}); + s::project p{s::files{{"mcpp.toml", "[package]\nname = \"layer-gated-tool\"\nversion = \"0.1.0\"\n[target.'cfg(c-abi = \"musl\")'.xlings.workspace]\n\"xim:mcpp-e2e-no-such-tool\" = \"1.0\"\n"}, + {"src/main.cpp", "int main() { return 0; }\n"}}}; + auto r = p.mcpp({"build"}); + expect(fatal(failed(r))) << "the build succeeded; a tool conditioned on a layer was accepted and then never installed"; + expect(e2e::contains(s::text(r), "xim:mcpp-e2e-no-such-tool")) << "the refusal does not name the tool"; + expect(e2e::contains(s::text(r), "c-abi = \"musl\"")) << "the refusal does not name the predicate"; + expect(e2e::contains(s::text(r), "feature-xlings")) << "the refusal does not name the gate form as the way out"; + }; + + "630 a rule brings its own environment"_test[tag{"requires:gcc"}] = [] { + // A rule package declares the payloads its rule needs, gated by the accelerator and the + // feature: a build naming no accelerator installs nothing; a device build installs the + // payload and the rule's build program finds it. + s::needs({"gcc"}); + const auto tmp = temp_dir(); + const s::options fresh{.where = s::home::fresh}; + s::project dep{tmp / "dep", {{"src/lib.cpp", "int dep_touch() { return 1; }\n"}, + {"build.mcpp", R"cpp(#include +#include +import mcpp; +int main() { + const char* d = mcpp::xpkg_dir("xim", "zoxide"); + std::string out = std::string(mcpp::manifest_dir()) + "/answered.txt"; + std::FILE* f = std::fopen(out.c_str(), "w"); + if (f == nullptr) return 3; + std::fprintf(f, "%s\n", d == nullptr ? "" : d); + std::fclose(f); + return 0; +} +)cpp"}, + {"mcpp.toml", "[package]\nname = \"ruleowner\"\nversion = \"0.1.0\"\n[modules]\nsources = [\"src/**/*.cpp\"]\n[features]\ndefault = []\nrules-toy = []\n[target.'cfg(accelerator = \"cuda\")'.feature-xlings.rules-toy]\n\"xim:zoxide\" = \">=0.9.9\"\n[targets.ruleowner]\nkind = \"lib\"\n"}}}; + s::project app{tmp / "app", {{"src/main.cpp", "int main() { return 0; }\n"}, + {"mcpp.toml", "[package]\nname = \"consumer\"\nversion = \"0.1.0\"\naccelerators = [\"cuda\"]\n[dependencies]\nruleowner = { path = \"../dep\", features = [\"rules-toy\"] }\n[targets.consumer]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n"}}}; + const auto store = s::case_home(s::home::fresh) / "registry/data/xpkgs/xim-x-zoxide"; + expect(fatal(e2e::succeeded(app.mcpp({"build"}, fresh)))) << "the CPU-only build did not build"; + expect(fatal(!fs::is_directory(store))) << "a build that named no accelerator downloaded the device payload"; + expect(fatal(e2e::succeeded(app.mcpp({"build", "--accel", "cuda12.9+{sm_89}"}, fresh)))) << "the device build did not build"; + expect(fatal(fs::is_directory(store))) << "the rule's payload was never installed"; + expect(fatal(dep.exists("answered.txt") && !dep.read("answered.txt").empty())) << "the rule's build program recorded nothing"; + expect(e2e::contains(dep.read("answered.txt"), "xim-x-zoxide")) << "the rule cannot find the payload it declared: " << dep.read("answered.txt"); + }; + + "798 a rule applies through a source it claims"_test[tag{"requires:gcc"}] = [] { + // An active rule applies to a consumer through the sources it claims (mcpp#715): the + // example's app gets a synthesised build program and its kernel; a package that only + // imports the rule's module gets neither a report nor a build program. + s::needs({"gcc"}); + const auto ex = example_copy(); + s::project app{ex / "app", {}}; + app.remove("build.mcpp"); + auto b1 = app.mcpp({"build"}); + expect(fatal(e2e::succeeded(b1))) << "leg 1 did not build"; + expect(fatal(s::has_line(s::text(b1), "Rules.*example.rules.toy"))) << "leg 1 did not report the rule it applied"; + expect(fatal(s::find_file(app.at("target"), "build.mcpp").has_value())) << "leg 1 synthesised no build program"; + expect(fatal(e2e::contains(s::last_line(app.mcpp({"run"}).out), "= 42"))) << "leg 1: the kernel did not reach the program"; + s::project only{ex / "importonly", {{"mcpp.toml", "[package]\nname = \"importonly\"\nversion = \"0.1.0\"\n\n[language]\nstandard = \"c++23\"\n\n[dependencies]\nexample.rules-toy = { path = \"../rules-toy\", features = [\"rules-toy\"] }\n\n[build]\nsources = [\"src/*.cpp\"]\n"}, + {"src/main.cpp", "#include \nint main() { std::printf(\"IMPORT_ONLY_OK\\n\"); }\n"}}}; + auto b2 = only.mcpp({"build"}); + expect(fatal(e2e::succeeded(b2))) << "leg 2 did not build"; + expect(s::lacks(s::text(b2), "Rules")) << "leg 2 reported a rule that claims none of its sources"; + expect(!s::find_file(only.at("target"), "build.mcpp").has_value()) << "leg 2 synthesised a build program with nothing to do"; + expect(eq(s::last_line(only.mcpp({"run"}).out), std::string{"IMPORT_ONLY_OK"})) << "leg 2 program"; + }; + + "808 device source is not a compile unit"_test[tag{"requires:gcc"}] = [] { + // A rule-claimed device source is no C++ compile unit of the plan: no cxx_object edge, + // no entry in compile_commands.json, in the build database or its compile-commands + // spelling; the rule still compiles it and the program runs. + s::needs({"gcc"}); + const auto ex = example_copy(); + s::project app{ex / "app", {}}; + const s::options cc{.env = {{"MCPP_COMPILE_COMMANDS", "compile_commands.json"}}}; + expect(fatal(e2e::succeeded(app.mcpp({"build", "--configure-only"}, cc)))) << "configure-only did not succeed"; + std::optional nj; + for (const auto& f : s::find_files(app.at("target"), R"(build\.ninja)")) + if (!nj && f.generic_string().find("/.build-mcpp/") == std::string::npos) nj = f; + expect(fatal(nj.has_value())) << "no build.ninja generated"; + expect(s::no_line(s::read_file(*nj), R"(^build [^:]*: cxx_object [^|]*answer\.toy)")) << "A: build.ninja carries a cxx_object edge for the device source"; + expect(fatal(app.exists("compile_commands.json"))) << "no compile_commands.json"; + expect(s::lacks(app.read("compile_commands.json"), "answer.toy")) << "B: compile_commands.json lists the device source"; + auto s1 = app.mcpp({"emit", "build-database", "--format", "json"}, cc); + expect(fatal(e2e::succeeded(s1))) << "emit build-database failed"; + std::string listed; + for (const auto& set : s::json(s1.out)["data"]["database"]["sets"]) + for (const auto& u : set["translation-units"]) + if (u["source"].get().ends_with("answer.toy")) listed += set["name"].get() + " "; + expect(listed.empty()) << "C: device source listed in set " << listed; + auto cdb = app.mcpp({"emit", "build-database", "--spec", "compile-commands"}, cc); + expect(fatal(e2e::succeeded(cdb))) << "emit --spec compile-commands failed"; + expect(s::lacks(cdb.out, "answer.toy")) << "D: --spec compile-commands lists the device source"; + expect(fatal(e2e::succeeded(app.mcpp({"build"}, cc)))) << "E: the build failed"; + expect(e2e::contains(s::last_line(app.mcpp({"run"}, cc).out), "= 42")) << "E: the kernel did not reach the program"; + }; +}}; + +} // namespace diff --git a/tests/support/src/support.cpp b/tests/support/src/support.cpp index b63c2a48a..0f2699591 100644 --- a/tests/support/src/support.cpp +++ b/tests/support/src/support.cpp @@ -778,4 +778,91 @@ header read_header(const fs::path& file) { return h; } +namespace { +// One member of an `ar` archive: its raw name field and its contents. +struct ar_member { + std::string name; + std::string data; +}; + +std::vector ar_members(const fs::path& file) { + const auto bytes = read_file(file); + std::vector out; + if (!bytes.starts_with("!\n")) return out; + std::size_t at = 8; + while (at + 60 <= bytes.size()) { + auto name = bytes.substr(at, 16); + const auto size = std::stoull(bytes.substr(at + 48, 10)); + at += 60; + if (at + size > bytes.size()) break; + auto data = bytes.substr(at, size); + while (!name.empty() && name.back() == ' ') name.pop_back(); + // BSD: `#1/`, the name is the first n bytes of the data. + if (name.starts_with("#1/")) { + const auto n = std::stoull(name.substr(3)); + name = data.substr(0, n); + data = data.substr(n); + while (!name.empty() && name.back() == '\0') name.pop_back(); + } + out.push_back({std::move(name), std::move(data)}); + at += size + (size % 2); + } + return out; +} +} // namespace + +std::vector archive_members(const fs::path& file) { + const auto all = ar_members(file); + std::string long_names; + for (const auto& m : all) + if (m.name == "//") long_names = m.data; + std::vector out; + for (const auto& m : all) { + if (m.name == "/" || m.name == "//" || m.name == "/SYM64/" || m.name.starts_with("__.SYMDEF")) continue; + std::string name = m.name; + if (name.size() > 1 && name[0] == '/' && std::isdigit(static_cast(name[1]))) { + const auto off = std::stoull(name.substr(1)); + const auto end = long_names.find('\n', off); + name = long_names.substr(off, end == std::string::npos ? std::string::npos : end - off); + } + if (name.ends_with("/")) name.pop_back(); + out.push_back(name); + } + return out; +} + +std::vector archive_symbols(const fs::path& file) { + std::vector out; + for (const auto& m : ar_members(file)) { + // System V/GNU: a big-endian count (4 bytes; 8 in /SYM64/), as many offsets, the names. + const std::size_t width = m.name == "/" ? 4 : m.name == "/SYM64/" ? 8 : 0; + if (width != 0 && m.data.size() >= width) { + std::uint64_t n = 0; + for (std::size_t i = 0; i < width; ++i) n = (n << 8) | static_cast(m.data[i]); + std::size_t at = width + n * width; + for (std::uint64_t i = 0; i < n && at < m.data.size(); ++i) { + const auto end = m.data.find('\0', at); + out.push_back(m.data.substr(at, end - at)); + at = end == std::string::npos ? m.data.size() : end + 1; + } + } + // BSD `__.SYMDEF`: ranlib entries (string offset, member offset) and a string table. + if (m.name.starts_with("__.SYMDEF") && m.data.size() >= 4) { + auto le32 = [&](std::size_t at) { + std::uint32_t v = 0; + for (int i = 3; i >= 0; --i) v = (v << 8) | static_cast(m.data[at + i]); + return v; + }; + const auto ranlib = le32(0); + const std::size_t strings = 4 + ranlib + 4; + for (std::size_t e = 4; e + 8 <= 4 + ranlib && strings <= m.data.size(); e += 8) { + const auto at = strings + le32(e); + const auto end = m.data.find('\0', at); + if (at < m.data.size()) out.push_back(m.data.substr(at, end - at)); + } + } + } + return out; +} + } // namespace mcpp::test::support diff --git a/tests/support/src/support.cppm b/tests/support/src/support.cppm index 20212bbd5..5e19ec6ef 100644 --- a/tests/support/src/support.cppm +++ b/tests/support/src/support.cppm @@ -201,6 +201,11 @@ struct header { // `readelf` or `llvm-readobj`). header read_header(const fs::path& file); +// The members of a static library (`ar t`) and the symbols its index names (`nm` of the +// archive's defined globals), read by this process: System V/GNU and BSD archives. +std::vector archive_members(const fs::path& file); +std::vector archive_symbols(const fs::path& file); + namespace elf_machine { inline constexpr std::uint32_t x86 = 3, arm = 40, x86_64 = 62, aarch64 = 183, riscv = 243; } From e1ce16a6afb93d86477c844206dd3ebbad5afc84 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 13 Nov 2026 18:49:42 +0900 Subject: [PATCH 008/115] e2e: the features subsystem as C++ cases (37 scripts) The 37 scripts MIGRATION.tsv assigns to `features` (feature activation, the --strict schema gate, feature defines and their propagation, capabilities and their providers, feature-deps, target-conditional configuration, feature-gated sources, default features and forwarding, layer predicates) are 37 cases of the program e2e/features (tests/e2e/features/*.cpp, 5 files: activation, conditional, defaults, layers, sources). Each case is named by its script's sentence and keeps its number; its `# requires:` words are its tags. Scripts: 67 71 80 81 82 83 85 86 100 106 108 119 126 127 128 138 146 149 195 247 308 328 601 615 637 654 677 710 711 714 739 813 895 1007 1012 1019 1045. No workflow runs any of them by path; all 37 are removed. Old run (the scripts, against this branch's mcpp, Linux x86_64): 37 passed. New run (`mcpp test e2e/features`): 37 passed, 0 failed, 0 skipped. Every assertion is kept. Where a reading changed: - python3 and jq readings of JSON are nlohmann::json. - 108 reads the host architecture from the compiler's predefined macros instead of `uname -m`. - 739's shell glob over the whole failed build (`*"sdk.h"*"No such file"*`) is the same search: "sdk.h", then one of the three phrases after it. - 813's `grep -rl` over the home's std-module records reads the records in sorted order, so "the first record holding the flag" is one record on every run (the script took find's order). - sed's line edits (1012, 711, 813) are the same edits on the manifest text; a fixture line an edit needs and does not find fails the case. --- ..._requirement_a_layer_decides_is_checked.sh | 67 -- tests/e2e/100_feature_sources_test_mode.sh | 99 --- ...abilities_bind_in_each_consumer_context.sh | 158 ---- ...dge_walks_the_feature_deps_it_activates.sh | 67 -- ...d_capability_takes_its_default_provider.sh | 69 -- tests/e2e/106_feature_gated_sources_toml.sh | 74 -- tests/e2e/108_cfg_conditional_sources.sh | 57 -- tests/e2e/119_dep_cfg_sources.sh | 106 --- tests/e2e/126_default_features_opt_out.sh | 140 ---- .../127_transitive_default_features_optout.sh | 117 --- tests/e2e/128_feature_forwarding.sh | 142 ---- .../138_feature_sources_gate_vs_provider.sh | 147 ---- tests/e2e/146_feature_flags.sh | 155 ---- tests/e2e/149_conditional_glob_flags.sh | 111 --- tests/e2e/195_target_cfg_feature_deps.sh | 147 ---- .../e2e/247_bare_triple_conditional_native.sh | 94 --- tests/e2e/308_dependency_required_features.sh | 94 --- ...redicates_apply_and_unknown_ones_say_so.sh | 235 ------ tests/e2e/601_exclusive_capability.sh | 81 -- ...5_feature_deps_shared_library_is_linked.sh | 114 --- ...ditional_runtime_table_alone_is_applied.sh | 129 --- .../654_a_requirement_on_the_target_axis.sh | 132 --- ...pendency_replaces_the_unconditional_one.sh | 104 --- tests/e2e/67_features_strict.sh | 131 --- ..._forward_reaches_every_dependency_table.sh | 167 ---- ...ature_deps_restatement_names_one_source.sh | 164 ---- ...714_features_names_a_dependency_feature.sh | 121 --- tests/e2e/71_required_features_gate.sh | 62 -- ...feature_dep_does_not_reach_the_consumer.sh | 126 --- tests/e2e/80_feature_defines.sh | 55 -- ..._conditional_dialect_cxxflags_root_only.sh | 227 ------ tests/e2e/81_capability_binding.sh | 106 --- tests/e2e/82_feature_optional_deps.sh | 67 -- tests/e2e/83_feature_defines_propagate.sh | 91 --- tests/e2e/85_target_cfg_build_flags.sh | 67 -- tests/e2e/86_target_cfg_dependencies.sh | 63 -- ...nfiguration_rows_count_as_they_evaluate.sh | 62 -- tests/e2e/MIGRATION.tsv | 74 +- tests/e2e/features/activation.cpp | 750 ++++++++++++++++++ tests/e2e/features/conditional.cpp | 586 ++++++++++++++ tests/e2e/features/defaults.cpp | 397 +++++++++ tests/e2e/features/layers.cpp | 299 +++++++ tests/e2e/features/sources.cpp | 223 ++++++ 43 files changed, 2292 insertions(+), 4185 deletions(-) delete mode 100755 tests/e2e/1007_a_requirement_a_layer_decides_is_checked.sh delete mode 100755 tests/e2e/100_feature_sources_test_mode.sh delete mode 100755 tests/e2e/1012_capabilities_bind_in_each_consumer_context.sh delete mode 100755 tests/e2e/1019_a_later_edge_walks_the_feature_deps_it_activates.sh delete mode 100755 tests/e2e/1045_a_required_capability_takes_its_default_provider.sh delete mode 100755 tests/e2e/106_feature_gated_sources_toml.sh delete mode 100755 tests/e2e/108_cfg_conditional_sources.sh delete mode 100755 tests/e2e/119_dep_cfg_sources.sh delete mode 100755 tests/e2e/126_default_features_opt_out.sh delete mode 100755 tests/e2e/127_transitive_default_features_optout.sh delete mode 100755 tests/e2e/128_feature_forwarding.sh delete mode 100755 tests/e2e/138_feature_sources_gate_vs_provider.sh delete mode 100755 tests/e2e/146_feature_flags.sh delete mode 100755 tests/e2e/149_conditional_glob_flags.sh delete mode 100755 tests/e2e/195_target_cfg_feature_deps.sh delete mode 100755 tests/e2e/247_bare_triple_conditional_native.sh delete mode 100755 tests/e2e/308_dependency_required_features.sh delete mode 100755 tests/e2e/328_layer_predicates_apply_and_unknown_ones_say_so.sh delete mode 100755 tests/e2e/601_exclusive_capability.sh delete mode 100755 tests/e2e/615_feature_deps_shared_library_is_linked.sh delete mode 100755 tests/e2e/637_a_conditional_runtime_table_alone_is_applied.sh delete mode 100755 tests/e2e/654_a_requirement_on_the_target_axis.sh delete mode 100755 tests/e2e/677_a_conditional_dependency_replaces_the_unconditional_one.sh delete mode 100755 tests/e2e/67_features_strict.sh delete mode 100755 tests/e2e/710_a_forward_reaches_every_dependency_table.sh delete mode 100755 tests/e2e/711_a_feature_deps_restatement_names_one_source.sh delete mode 100755 tests/e2e/714_features_names_a_dependency_feature.sh delete mode 100755 tests/e2e/71_required_features_gate.sh delete mode 100755 tests/e2e/739_a_private_feature_dep_does_not_reach_the_consumer.sh delete mode 100755 tests/e2e/80_feature_defines.sh delete mode 100755 tests/e2e/813_conditional_dialect_cxxflags_root_only.sh delete mode 100755 tests/e2e/81_capability_binding.sh delete mode 100755 tests/e2e/82_feature_optional_deps.sh delete mode 100755 tests/e2e/83_feature_defines_propagate.sh delete mode 100755 tests/e2e/85_target_cfg_build_flags.sh delete mode 100755 tests/e2e/86_target_cfg_dependencies.sh delete mode 100755 tests/e2e/895_conditional_configuration_rows_count_as_they_evaluate.sh create mode 100644 tests/e2e/features/activation.cpp create mode 100644 tests/e2e/features/conditional.cpp create mode 100644 tests/e2e/features/defaults.cpp create mode 100644 tests/e2e/features/layers.cpp create mode 100644 tests/e2e/features/sources.cpp diff --git a/tests/e2e/1007_a_requirement_a_layer_decides_is_checked.sh b/tests/e2e/1007_a_requirement_a_layer_decides_is_checked.sh deleted file mode 100755 index 72fd5f4fe..000000000 --- a/tests/e2e/1007_a_requirement_a_layer_decides_is_checked.sh +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env bash -# requires: unix-shell -# 1007 -- a requirement stated in a table whose selector names a layer -# (`[target.'cfg(compiler = "llvm")'] requires = [...]`) is checked once the -# layers are resolved, as a requirement of `[package]` is. -# -# openkal-musl links its C library with `-nostdlib`, and on PE under clang the -# routines a compiler emits calls to are compiler-rt's, which the graph supplies -# (openkal-llvm-runtime). It states that as a requirement of the clang build -# only; the table's requirement was dropped without a word, and a graph without -# the layer reached the link with `___chkstk_ms` undefined. -# -# Criteria: A a dependency whose layer-selected table requires a layer this -# build does not resolve is refused before anything is compiled, the message -# naming the dependency, the layer and what it requires; B the same table under -# a selector this build does not match states nothing, and the build succeeds. -set -e - -MCPP="${MCPP:-mcpp}" -work="$(mktemp -d)" -trap 'rm -rf "$work"' EXIT - -needs() { # needs - mkdir -p "$work/needs/src" - cat > "$work/needs/mcpp.toml" < "$work/needs/src/needs.cpp" -} - -mkdir -p "$work/app/src" -cat > "$work/app/mcpp.toml" < "$work/app/src/main.cpp" - -# A: a selector naming a layer, which every build matches. -needs 'cfg(not(compiler = "nonesuch"))' -if (cd "$work/app" && "$MCPP" build > "$work/a.log" 2>&1); then - cat "$work/a.log"; echo "FAIL: A: a requirement a layer decides was not checked"; exit 1 -fi -grep -q 'needs@0.1.0` requires the compiler-runtime to be `nonesuch`' "$work/a.log" \ - || { cat "$work/a.log"; echo "FAIL: A: the refusal does not name the dependency, the layer and what it requires"; exit 1; } -if grep -q 'Compiling' "$work/a.log"; then - cat "$work/a.log"; echo "FAIL: A: something was compiled before the refusal"; exit 1 -fi -echo "ok: A: a requirement a layer decides is checked when the layers are known" - -# B: the same table under a selector this build does not match. -needs 'cfg(compiler = "nonesuch")' -rm -rf "$work/app/target" -(cd "$work/app" && "$MCPP" build > "$work/b.log" 2>&1) \ - || { cat "$work/b.log"; echo "FAIL: B: a table this build does not match stated a requirement"; exit 1; } -echo "ok: B: a table this build does not match states nothing" diff --git a/tests/e2e/100_feature_sources_test_mode.sh b/tests/e2e/100_feature_sources_test_mode.sh deleted file mode 100755 index d770d1419..000000000 --- a/tests/e2e/100_feature_sources_test_mode.sh +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env bash -# requires: -# 100_feature_sources_test_mode.sh — a dependency's feature-gated `sources` must -# be compiled under `mcpp test`, not just `mcpp build`. -# -# Regression: prepare_build() gated the whole feature-source resolution (drop + -# add) on `!includeDevDeps`, so `mcpp test` never added an active feature's -# sources. Descriptors that list a glob ONLY under `features` (xpkg's -# `features.X.sources` never lands in base `sources`) therefore compiled fine -# under `mcpp build` but failed to link under `mcpp test` with `undefined -# reference` — compat.cjson's `utils`, compat.spdlog's `compiled`, and -# compat.eigen's `eigen_blas` all hit this. The drop stays build-mode only (see -# 79_gtest_regular_dep_feature_main.sh); the add now runs in both modes. -# -# compat.cjson is the vehicle: plain C, small, and `utils` (cJSON_Utils.c) is -# feature-gated with no other dependency. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT - -cd "$TMP" -"$MCPP" new app > /dev/null -cd app -rm -f src/main.cpp -# The skeleton's test is mtest's; this manifest names no dev-dependency. -rm -f tests/test_smoke.cpp -mkdir -p tests - -cat > mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" - -[dependencies.compat] -cjson = { version = "1.7.19", features = ["utils"] } -EOF - -# Calls into BOTH the base sources (cJSON.c) and the feature-gated ones -# (cJSON_Utils.c) — a link failure on either fails the test. -cat > tests/t.cpp <<'EOF' -#include -#include -int main() { - cJSON* root = cJSON_Parse("{\"a\":1}"); - if (!root) return 1; - char* p = cJSONUtils_FindPointerFromObjectTo(root, root); - int ok = (p != 0); - cJSON_Delete(root); - return ok ? 0 : 1; -} -EOF - -# (1) THE REGRESSION: `mcpp test` must compile the feature's sources and link. -"$MCPP" test > /dev/null 2>&1 || { - echo "FAIL: feature-gated sources not compiled under \`mcpp test\`" - "$MCPP" test 2>&1 | tail -5 - exit 1 -} -nj=$(find target -name test.ninja -not -path '*/.build-mcpp/*' | xargs ls -t 2>/dev/null | head -1) -grep -q 'cJSON_Utils' "$nj" || { - echo "FAIL: cJSON_Utils.c absent from the test-mode build graph"; exit 1 -} - -# (2) `mcpp build` keeps working (the path that was always correct). -cat > src/main.cpp <<'EOF' -#include -#include -int main() { - cJSON* root = cJSON_Parse("{\"a\":1}"); - char* p = cJSONUtils_FindPointerFromObjectTo(root, root); - (void)p; cJSON_Delete(root); return 0; -} -EOF -"$MCPP" build > /dev/null || { echo "FAIL: build with feature sources failed"; exit 1; } -nj=$(find target -name build.ninja -not -path '*/.build-mcpp/*' | xargs ls -t 2>/dev/null | head -1) -grep -q 'cJSON_Utils' "$nj" || { echo "FAIL: cJSON_Utils.c absent from build graph"; exit 1; } - -# (3) The drop still works: without the feature, the gated source stays OUT of a -# `mcpp build` graph (guards against "add everything unconditionally"). -cat > mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" - -[dependencies.compat] -cjson = "1.7.19" -EOF -cat > src/main.cpp <<'EOF' -#include -int main() { cJSON* r = cJSON_Parse("{\"a\":1}"); cJSON_Delete(r); return 0; } -EOF -"$MCPP" build > /dev/null || { echo "FAIL: default (no feature) build failed"; exit 1; } -nj=$(find target -name build.ninja -not -path '*/.build-mcpp/*' | xargs ls -t 2>/dev/null | head -1) -if grep -q 'cJSON_Utils' "$nj"; then - echo "FAIL: cJSON_Utils.c compiled without the \`utils\` feature"; exit 1 -fi - -echo "OK" diff --git a/tests/e2e/1012_capabilities_bind_in_each_consumer_context.sh b/tests/e2e/1012_capabilities_bind_in_each_consumer_context.sh deleted file mode 100755 index c0fbdaebb..000000000 --- a/tests/e2e/1012_capabilities_bind_in_each_consumer_context.sh +++ /dev/null @@ -1,158 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc unix-shell python3 -# Independent products may select different providers. A library requirement -# still accepts a provider supplied by its final application's sibling edge. -set -e -MCPP="${MCPP:-mcpp}" -work=$(mktemp -d) -trap 'rm -rf "$work"' EXIT -export MCPP_HOME="$work/home" -source "$(dirname "$0")/_inherit_toolchain.sh" -cd "$work" -fail() { echo "FAIL: $*"; cat out.log; exit 1; } -cat > mcpp.toml <<'TOML' -[workspace] -members = ["app-a", "app-b", "helper", "provider-a", "provider-b"] -TOML -for suffix in a b; do - mkdir -p "provider-$suffix/src" "app-$suffix/src" - cat > "provider-$suffix/mcpp.toml" < "provider-$suffix/src/anchor.c" - cat > "app-$suffix/mcpp.toml" < "app-$suffix/src/main.cpp" -done -mkdir -p helper/src -cat > helper/mcpp.toml <<'TOML' -[package] -name = "helper" -version = "0.1.0" -[features] -default = ["need-provider"] -need-provider = { requires = ["probe-interface"] } -[targets.helper] -kind = "lib" -TOML -echo 'int helper_anchor(void){return 0;}' > helper/src/anchor.c -"$MCPP" build --workspace > out.log 2>&1 || fail 'independent exclusive providers collided' -"$MCPP" run -p app-a > out.log 2>&1 || fail 'app-a did not run' -"$MCPP" run -p app-b > out.log 2>&1 || fail 'app-b did not run' - -# A standalone selection still needs its own binding. The enclosing -# applications do not supply an executable or test owned by the library. -if "$MCPP" build -p helper > out.log 2>&1; then fail 'standalone library borrowed an unselected consumer'; fi -grep -q "consumer 'helper'" out.log || fail 'standalone library lost its context' -mkdir -p helper/tests -echo 'int main(){return 0;}' > helper/tests/context.cpp -if "$MCPP" test --workspace > out.log 2>&1; then fail 'library test borrowed an application provider'; fi -grep -q "consumer 'helper'" out.log || fail 'library test lost its own context' -rm -rf helper/tests - -# Test link units and a shared provider keep the same per-consumer boundary. -for suffix in a b; do - mkdir -p "app-$suffix/tests" - echo 'int main(){return 0;}' > "app-$suffix/tests/context.cpp" -done -sed 's/kind = "lib"/kind = "shared"/' provider-a/mcpp.toml > changed.toml -mv changed.toml provider-a/mcpp.toml -"$MCPP" test --workspace > out.log 2>&1 || fail 'test/shared contexts crossed independent products' -"$MCPP" run -p app-a > out.log 2>&1 || fail 'shared provider consumer did not run' - -# A provider elsewhere in the workspace cannot satisfy a missing dependency. -cp app-a/mcpp.toml app-a/saved.toml -sed '/^provider-a =/d' app-a/saved.toml > app-a/mcpp.toml -if "$MCPP" build --workspace > out.log 2>&1; then fail 'another product supplied a missing provider'; fi -grep -q "no package provides capability 'probe-interface'" out.log || fail 'wrong missing-provider diagnosis' -grep -q "consumer 'app-a'" out.log || fail 'missing consumer context' -# Host-only providers cannot satisfy the missing target-side requirement. -cat >> app-a/mcpp.toml <<'TOML' -[build-dependencies] -provider-a = { path = "../provider-a" } -TOML -echo 'int main(){return 0;}' > app-a/build.mcpp -if "$MCPP" build --workspace > out.log 2>&1; then fail 'a host-only provider satisfied a target requirement'; fi -grep -q "no package provides capability 'probe-interface'" out.log || fail 'wrong host/target scope diagnosis' -rm app-a/build.mcpp -cp app-a/saved.toml app-a/mcpp.toml - -# A pin cannot remove the second implementation from one actual link closure. -cat >> app-a/mcpp.toml <<'TOML' -[capabilities] -probe-interface = "provider-a" -TOML -sed '/^helper =/a provider-b = { path = "../provider-b" }' app-a/mcpp.toml > changed.toml -mv changed.toml app-a/mcpp.toml -if "$MCPP" build --workspace > out.log 2>&1; then fail 'pin hid an exclusive collision'; fi -grep -qi exclusive out.log || fail 'wrong exclusive diagnosis' -grep -q "consumer 'app-a'" out.log || fail 'exclusive diagnosis omitted consumer' - -# Without exclusivity both providers may coexist; the member pin resolves its -# requirement while the other member binds implicitly to its own provider. -for suffix in a b; do - sed '/^exclusive =/d' "provider-$suffix/mcpp.toml" > changed.toml - mv changed.toml "provider-$suffix/mcpp.toml" -done -"$MCPP" build --workspace > out.log 2>&1 || fail 'member pin did not resolve local ambiguity' -"$MCPP" build --workspace --cap probe-interface=provider-b > out.log 2>&1 \ - || fail 'CLI pin did not override the member pin' -if "$MCPP" build --workspace --cap probe-interface=provider-a > out.log 2>&1; then - fail 'CLI pin escaped the second consumer closure' -fi -grep -q "consumer 'app-b'" out.log || fail 'invalid pin omitted its consumer' - -# Qualified provider identities distinguish packages sharing a short name. -mkdir -p namespaced/src -for ns in first second; do - mkdir -p "$ns/src" - cat > "$ns/mcpp.toml" < "$ns/src/anchor.c" -done -cat > namespaced/mcpp.toml <<'TOML' -[package] -name = "namespaced-consumer" -version = "0.1.0" -[features] -default = ["need"] -need = { requires = ["namespaced-interface"] } -[dependencies.first] -implementation = { path = "../first" } -[dependencies.second] -implementation = { path = "../second" } -[capabilities] -namespaced-interface = "implementation" -TOML -echo 'int main(){return 0;}' > namespaced/src/main.cpp -cd namespaced -if "$MCPP" build > out.log 2>&1; then fail 'ambiguous short provider identity was accepted'; fi -grep -q 'first:implementation' out.log || fail 'missing first qualified candidate' -grep -q 'second:implementation' out.log || fail 'missing second qualified candidate' -sed 's/= "implementation"$/= "first:implementation"/' mcpp.toml > changed.toml -mv changed.toml mcpp.toml -"$MCPP" build > out.log 2>&1 || fail 'qualified provider pin did not bind' - -echo 'PASS: capabilities bind in each consumer context' diff --git a/tests/e2e/1019_a_later_edge_walks_the_feature_deps_it_activates.sh b/tests/e2e/1019_a_later_edge_walks_the_feature_deps_it_activates.sh deleted file mode 100755 index 8270f550c..000000000 --- a/tests/e2e/1019_a_later_edge_walks_the_feature_deps_it_activates.sh +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env bash -# A LATER EDGE WALKS THE FEATURE-DEPS IT ACTIVATES (mcpp#897). -# `v` asks `a` for `f1`, and its feature `x` asks `a` for `f2` through -# `[feature-deps.x]`; `s` asks `v` for `x`. With `v` selected beside `s`, `v` -# was walked as a member first, without `x`, and `s`'s edge later added `x` to -# it: `x`'s sources were compiled, `x`'s feature-deps never reached the graph, -# and `a` was built without `f2` (`a.f2: failed to read compiled module`). The -# feature-deps and forwards of a feature a later edge activates are walked now. -set -e -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } -MCPP="${MCPP:-mcpp}" - -cd "$TMP" -mkdir -p a/src/f1 a/src/f2 v/src/base v/src/x s/src -printf '[workspace]\nmembers = ["a", "v", "s"]\n' > mcpp.toml -cat > a/mcpp.toml <<'EOF' -[package] -name = "a" -version = "0.1.0" -[targets.a] -kind = "lib" -[build] -sources = [] -[features] -f1 = { sources = ["src/f1/*.cppm"] } -f2 = { sources = ["src/f2/*.cppm"] } -EOF -printf 'export module a.f1;\nexport int f1() { return 1; }\n' > a/src/f1/f1.cppm -printf 'export module a.f2;\nexport int f2() { return 2; }\n' > a/src/f2/f2.cppm -cat > v/mcpp.toml <<'EOF' -[package] -name = "v" -version = "0.1.0" -[targets.v] -kind = "lib" -[build] -sources = ["src/base/*.cppm"] -[dependencies] -a = { path = "../a", features = ["f1"] } -[features] -x = { sources = ["src/x/*.cppm"] } -[feature-deps.x] -a = { path = "../a", features = ["f2"] } -EOF -printf 'export module v;\nimport a.f1;\nexport int vv() { return f1(); }\n' > v/src/base/v.cppm -printf 'export module v.x;\nimport a.f2;\nexport int vx() { return f2(); }\n' > v/src/x/x.cppm -cat > s/mcpp.toml <<'EOF' -[package] -name = "s" -version = "0.1.0" -[targets.s] -kind = "bin" -main = "src/main.cpp" -[dependencies] -v = { path = "../v", features = ["x"] } -EOF -printf 'import std;\nimport v;\nimport v.x;\nint main() { std::println("sum {}", vv() + vx()); }\n' > s/src/main.cpp - -for order in "-p s -p v" "-p v -p s"; do - rm -rf target - "$MCPP" build $order > build.log 2>&1 || fail "mcpp build $order" build.log -done -"$MCPP" run -p s > run.log 2>&1 || fail "the program" run.log -grep -q "sum 3" run.log || fail "the program's output" run.log -echo "PASS: 1019 a later edge walks the feature-deps it activates" diff --git a/tests/e2e/1045_a_required_capability_takes_its_default_provider.sh b/tests/e2e/1045_a_required_capability_takes_its_default_provider.sh deleted file mode 100755 index 4ffb9c00f..000000000 --- a/tests/e2e/1045_a_required_capability_takes_its_default_provider.sh +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env bash -# requires: elf -source "$(dirname "${BASH_SOURCE[0]}")/_toolchain_env.sh" -# 1045 -- a required capability takes its package's default provider (mcpp#911 G7; SPEC-002 §3.6). -# -# A package requires a capability (`requires = ["acme:greeter"]`) and states its default providers -# per target (`[defaults."acme:greeter"] linux = ..., "*" = ...`). With no provider in the graph, -# the default for the target is the package's dependency; a selector that names the target wins -# over `*`. A provider the consumer brings is used instead, and no default is added. -# -# Criteria: the build reports the default; the program returns the default's value (1, Linux); -# with the consumer depending on the other provider it returns that one's (2) and no default is -# reported. -set -e -t=$(mktemp -d); trap 'rm -rf "$t"' EXIT -fail() { echo "FAIL: $1"; [ -n "${2:-}" ] && printf '%s\n' "$2" | tail -20; exit 1; } -mkdir -p "$t/greet-a/src" "$t/greet-b/src" "$t/lib/src" "$t/app/src" -for p in a b; do -cat > "$t/greet-$p/mcpp.toml" < "$t/greet-a/src/a.cpp" -echo 'extern "C" int greet() { return 2; }' > "$t/greet-b/src/b.cpp" -cat > "$t/lib/mcpp.toml" <<'EOF' -[package] -name = "lib" -version = "0.1.0" -requires = ["acme:greeter"] - -[targets.lib] -kind = "lib" - -[defaults."acme:greeter"] -linux = { path = "../greet-a" } -"*" = { path = "../greet-b" } -EOF -echo 'extern "C" int greet(); int lib_greet() { return greet(); }' > "$t/lib/src/lib.cpp" -cat > "$t/app/mcpp.toml" <<'EOF' -[package] -name = "app" -version = "0.1.0" - -[dependencies] -lib = { path = "../lib" } -EOF -echo 'int lib_greet(); int main() { return lib_greet(); }' > "$t/app/src/main.cpp" -cd "$t/app" - -out=$("$MCPP" build 2>&1) || fail "the build failed" "$out" -grep -q "Default .*greet-a for acme:greeter of lib (linux)" <<<"$out" || fail "the default is not reported" "$out" -exe=$(find target -path '*/bin/app' -type f | head -1) -set +e; "$exe"; rc=$?; set -e -[ "$rc" = 1 ] || fail "the program returned $rc, not the Linux default's 1" -echo " ok the target's default provides the capability, over \`*\`" - -printf 'greet-b = { path = "../greet-b" }\n' >> mcpp.toml -out=$("$MCPP" build 2>&1) || fail "the build with the consumer's provider failed" "$out" -grep -q "Default " <<<"$out" && fail "a default was added though the graph provides the capability" "$out" -set +e; "$exe"; rc=$?; set -e -[ "$rc" = 2 ] || fail "the program returned $rc, not the consumer's provider's 2" -echo " ok the consumer's provider is used, and no default is added" -echo "PASS: 1045 a required capability takes its default provider" diff --git a/tests/e2e/106_feature_gated_sources_toml.sh b/tests/e2e/106_feature_gated_sources_toml.sh deleted file mode 100755 index 57893ea18..000000000 --- a/tests/e2e/106_feature_gated_sources_toml.sh +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# G5: mcpp.toml [features] `sources` key — feature-gated source globs, at -# parity with index descriptors. Four quadrants: feature off/on × build/test -# (the 0.0.94 lesson: feature-source behavior MUST match on both paths). -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT - -cd "$TMP" -"$MCPP" new featsrc > /dev/null -cd featsrc - -cat > src/extra.cpp <<'EOF' -extern "C" int extra_answer() { return 42; } -EOF - -cat > src/main.cpp <<'EOF' -import std; -extern "C" int extra_answer(); -int main() { -#ifdef HAS_EXTRA - std::println("extra = {}", extra_answer()); -#else - std::println("extra = none"); -#endif - return 0; -} -EOF - -cat > mcpp.toml <<'EOF' -[package] -name = "featsrc" -version = "0.1.0" - -[features] -default = [] -extra = { sources = ["src/extra.cpp"], defines = ["HAS_EXTRA"] } -EOF -# The skeleton's test is mtest's; this manifest names no dev-dependency. -rm -f tests/test_smoke.cpp - -# Quadrant 1: feature OFF, build — extra.cpp must NOT compile (main has no -# reference so the link succeeds; the object must be absent). -"$MCPP" build > build_off.log 2>&1 || { cat build_off.log; echo "build (off) failed"; exit 1; } -find target -name 'extra.o' | grep -q . && { echo "extra.cpp compiled with feature off"; exit 1; } || true -out="$("$MCPP" run 2>&1 | tail -1)" -[[ "$out" == "extra = none" ]] || { echo "unexpected (off): $out"; exit 1; } - -# Quadrant 2: feature ON, build (`run` has no --features; exec the binary). -"$MCPP" build --features extra > build_on.log 2>&1 || { cat build_on.log; echo "build (on) failed"; exit 1; } -# Feature defines change the fingerprint → a second output dir; take the -# newest binary. -bin="$(ls -t $(find target -name 'featsrc' -type f) | head -1)" -[[ -n "$bin" ]] || { echo "binary not found"; exit 1; } -out="$("./$bin" | tail -1)" -[[ "$out" == "extra = 42" ]] || { echo "unexpected (on): $out"; exit 1; } - -# Quadrants 3+4: test path, feature off then on. The test binary references -# the gated symbol only when the feature is active. -mkdir -p tests -cat > tests/test_featsrc.cpp <<'EOF' -#ifdef HAS_EXTRA -extern "C" int extra_answer(); -int main() { return extra_answer() == 42 ? 0 : 1; } -#else -int main() { return 0; } -#endif -EOF -"$MCPP" test > test_off.log 2>&1 || { cat test_off.log; echo "mcpp test (off) failed"; exit 1; } -"$MCPP" test --features extra > test_on.log 2>&1 || { cat test_on.log; echo "mcpp test (on) failed"; exit 1; } - -echo "OK" diff --git a/tests/e2e/108_cfg_conditional_sources.sh b/tests/e2e/108_cfg_conditional_sources.sh deleted file mode 100755 index 0d8fe517a..000000000 --- a/tests/e2e/108_cfg_conditional_sources.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# G1b: [target.'cfg(...)'.build] sources — target-conditional source globs, -# evaluated against the RESOLVED target. Host-aware: asserts the host-arch -# gated file is compiled and the other-arch file is not. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT - -HOST_ARCH="$(uname -m)" -case "$HOST_ARCH" in - x86_64) THIS=x86_64; OTHER=aarch64 ;; - aarch64|arm64) THIS=aarch64; OTHER=x86_64 ;; - *) echo "SKIP: unmapped arch $HOST_ARCH"; exit 77 ;; -esac - -cd "$TMP" -"$MCPP" new condsrc > /dev/null -cd condsrc - -mkdir -p arch -cat > arch/this.cpp <<'EOF' -extern "C" int arch_answer() { return 7; } -EOF -cat > arch/other.cpp <<'EOF' -extern "C" int arch_answer() { return 8; } // would double-define if both compiled -EOF - -cat > src/main.cpp <<'EOF' -import std; -extern "C" int arch_answer(); -int main() { - std::println("arch = {}", arch_answer()); - return 0; -} -EOF - -cat > mcpp.toml < build.log 2>&1 || { cat build.log; echo "build failed"; exit 1; } -out="$("$MCPP" run 2>&1 | tail -1)" -[[ "$out" == "arch = 7" ]] || { echo "unexpected output: $out"; exit 1; } -# The other-arch file must not have been compiled. -find target -name 'other.o' | grep -q . && { echo "other-arch source compiled"; exit 1; } || true - -echo "OK" diff --git a/tests/e2e/119_dep_cfg_sources.sh b/tests/e2e/119_dep_cfg_sources.sh deleted file mode 100755 index 517c0b51f..000000000 --- a/tests/e2e/119_dep_cfg_sources.sh +++ /dev/null @@ -1,106 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 119_dep_cfg_sources.sh — #229: a PATH DEPENDENCY's `[target.'cfg(...)'.build]` -# conditional `sources` must be compiled, in BOTH `mcpp build` and `mcpp test`. -# -# Regression: `merge_conditional_build` (the L1b cfg-sources merge) was only -# ever invoked for the root package (prepare.cppm root call site) and for -# version/registry dependencies (loadVersionDep). Path/git dependencies load -# their manifest directly and skipped the merge entirely — a path dep's -# cfg-conditional `sources` were parsed into `conditionalConfigs` but never -# folded into `buildConfig.sources` / `modules.sources`, so the modgraph scan -# never saw the file and the final link failed with `undefined reference`. -# -# `mylib` is a path dependency whose `mylib.cppm` module forward-declares -# `impl_value()` (extern "C", to sidestep name mangling) but the DEFINITION -# lives in `src/impl.cpp`, which is reachable ONLY through the cfg-conditional -# `[target.'cfg(...)'.build] sources` — never through the unconditional -# `[build] sources` glob. Host-aware: both `cfg(unix)` and `cfg(windows)` -# blocks point at the same file, so exactly one always applies (mirrors -# 86_target_cfg_dependencies.sh's host-aware dual-predicate style) and the -# test runs identically on all 3 CI platforms. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -mkdir -p mylib/src -cat > mylib/mcpp.toml <<'EOF' -[package] -name = "mylib" -version = "0.1.0" - -[targets.mylib] -kind = "lib" - -[build] -sources = ["src/**/*.cppm"] - -# impl.cpp is reachable ONLY through these conditional blocks — never through -# the unconditional [build] sources glob above. Exactly one of unix/windows -# always matches the resolved host target. -[target.'cfg(unix)'.build] -sources = ["src/impl.cpp"] -[target.'cfg(windows)'.build] -sources = ["src/impl.cpp"] -EOF -cat > mylib/src/mylib.cppm <<'EOF' -export module mylib; - -// Defined in src/impl.cpp, which is pulled in ONLY via cfg-conditional -// [target.'cfg(...)'.build] sources — extern "C" so the module boundary -// doesn't affect the link name. -extern "C" int impl_value(); - -export int mylib_answer() { return impl_value(); } -EOF -cat > mylib/src/impl.cpp <<'EOF' -extern "C" int impl_value() { return 42; } -EOF - -mkdir -p app/src app/tests -cat > app/mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" - -[dependencies] -mylib = { path = "../mylib" } -EOF -cat > app/src/main.cpp <<'EOF' -import mylib; -import std; -int main() { - std::println("value = {}", mylib_answer()); - return mylib_answer() == 42 ? 0 : 1; -} -EOF -cat > app/tests/t.cpp <<'EOF' -import mylib; -int main() { - return mylib_answer() == 42 ? 0 : 1; -} -EOF - -cd app - -# (1) `mcpp build` must compile+link mylib's cfg-conditional source. -"$MCPP" build > b.log 2>&1 || { - cat b.log - echo "FAIL: build did not compile the path dep's cfg-conditional sources (undefined reference expected pre-fix)" - exit 1 -} -out="$("$MCPP" run 2>&1 | tail -1)" -[[ "$out" == "value = 42" ]] || { echo "FAIL: unexpected run output: $out"; exit 1; } - -# (2) `mcpp test` must ALSO compile+link it (the per-package funnel must fire -# in both modes, mirroring 100_feature_sources_test_mode.sh's build/test -# parity check). -"$MCPP" test > t.log 2>&1 || { - cat t.log - echo "FAIL: test mode did not compile the path dep's cfg-conditional sources" - exit 1 -} - -echo "OK" diff --git a/tests/e2e/126_default_features_opt_out.sh b/tests/e2e/126_default_features_opt_out.sh deleted file mode 100755 index 22a8e7278..000000000 --- a/tests/e2e/126_default_features_opt_out.sh +++ /dev/null @@ -1,140 +0,0 @@ -#!/usr/bin/env bash -# 126_default_features_opt_out.sh — consumer-side `default-features = false` -# (#242, Cargo parity). A dependency declares a DEFAULT feature set -# (`[features] default = ["turbo"]`) that carries a package-owned define. A -# consumer must be able to OPT OUT of that default set via -# `default-features = false`, so the dependency's default feature (and its -# define) does NOT activate. Explicitly requesting `features = [...]` still -# activates those, even alongside the opt-out. -# -# Root-cause funnel: the feature-activation closure (prepare.cppm -# feature_closure) no longer unconditionally seeds `[features].default` for a -# dependency — it seeds it only when the consumer's dep spec keeps default -# features (seedDefault). See the #242 commit. -# -# No `requires:` capability → runs on all three CI platforms. -set -e -# The project-root copy of the compile database is opt-in since -# 2026.10.26.1 ([build] compile_commands); this test reads it. -export MCPP_COMPILE_COMMANDS=compile_commands.json - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -# Dependency whose DEFAULT feature set enables `turbo`, which carries a -# package-owned define that propagates to consumers (header-only case). -mkdir -p widget/include/widget widget/src -cat > widget/mcpp.toml <<'EOF' -[package] -name = "widget" -version = "0.1.0" - -[features] -default = ["turbo"] -turbo = { defines = ["WIDGET_TURBO=1"] } - -[build] -include_dirs = ["include"] - -[targets.widget] -kind = "lib" -EOF -cat > widget/include/widget/widget.hpp <<'EOF' -#pragma once -inline int widget_mode() { -#ifdef WIDGET_TURBO - return 1; -#else - return 0; -#endif -} -EOF -cat > widget/src/widget.cppm <<'EOF' -export module widget; -export int widget_anchor() { return 0; } -EOF - -# ── Case A: default-features = false → the dep's default `turbo` is SUPPRESSED. -# The consumer TU must NOT see WIDGET_TURBO; if it did, the #error below trips -# and the build fails. -mkdir -p appoff/src -cat > appoff/mcpp.toml <<'EOF' -[package] -name = "appoff" -version = "0.1.0" - -[dependencies] -widget = { path = "../widget", default-features = false } -EOF -cat > appoff/src/main.cpp <<'EOF' -#include -#ifdef WIDGET_TURBO -#error "WIDGET_TURBO leaked despite default-features = false" -#endif -int main() { return widget_mode() == 0 ? 0 : 2; } -EOF - -( cd appoff - "$MCPP" build > b.log 2>&1 || { cat b.log; echo "FAIL(A): opt-out build failed"; exit 1; } - if grep -q 'WIDGET_TURBO' compile_commands.json; then - echo "FAIL(A): WIDGET_TURBO present despite default-features = false"; cat compile_commands.json; exit 1 - fi - BIN=$(find target -type f \( -name appoff -o -name appoff.exe \) | head -1) - [ -n "$BIN" ] || { echo "FAIL(A): built binary not found"; exit 1; } - "$BIN"; rc=$? - [ "$rc" -eq 0 ] || { echo "FAIL(A): binary observed turbo despite opt-out (exit $rc)"; exit 1; } -) - -# ── Case B: default features ON (omitted) → the dep's default `turbo` activates -# and its define reaches the consumer, exactly as before this change. -mkdir -p appon/src -cat > appon/mcpp.toml <<'EOF' -[package] -name = "appon" -version = "0.1.0" - -[dependencies] -widget = { path = "../widget" } -EOF -cat > appon/src/main.cpp <<'EOF' -#include -#ifndef WIDGET_TURBO -#error "WIDGET_TURBO missing though default features are on" -#endif -int main() { return widget_mode() == 1 ? 0 : 2; } -EOF - -( cd appon - "$MCPP" build > b.log 2>&1 || { cat b.log; echo "FAIL(B): default-on build failed"; exit 1; } - grep -q 'WIDGET_TURBO' compile_commands.json || { - echo "FAIL(B): WIDGET_TURBO missing with default features on"; cat compile_commands.json; exit 1; } -) - -# ── Case C: default-features = false BUT explicitly re-request `turbo` → -# the explicit request still activates it (opt-out suppresses only the default -# seed, not explicit `features = [...]`). -mkdir -p appre/src -cat > appre/mcpp.toml <<'EOF' -[package] -name = "appre" -version = "0.1.0" - -[dependencies] -widget = { path = "../widget", default-features = false, features = ["turbo"] } -EOF -cat > appre/src/main.cpp <<'EOF' -#include -#ifndef WIDGET_TURBO -#error "explicit features = [turbo] did not activate alongside default-features = false" -#endif -int main() { return widget_mode() == 1 ? 0 : 2; } -EOF - -( cd appre - "$MCPP" build > b.log 2>&1 || { cat b.log; echo "FAIL(C): opt-out + explicit build failed"; exit 1; } - grep -q 'WIDGET_TURBO' compile_commands.json || { - echo "FAIL(C): explicit feature did not activate alongside opt-out"; cat compile_commands.json; exit 1; } -) - -echo "OK" diff --git a/tests/e2e/127_transitive_default_features_optout.sh b/tests/e2e/127_transitive_default_features_optout.sh deleted file mode 100755 index 44421251e..000000000 --- a/tests/e2e/127_transitive_default_features_optout.sh +++ /dev/null @@ -1,117 +0,0 @@ -#!/usr/bin/env bash -# requires: elf gcc -# mcpp#242 (transitive edge, found in the 0.0.99 architecture review): a -# consumer's `default-features = false` must be honored even when the consumer -# is itself a DEPENDENCY (transitive edge), not only for the root's direct deps. -# -# Before the edge-graph convergence, feature *resolution* honored the per-edge -# opt-out (mergeActiveFeatureDeps) but feature *activation* re-derived the flag -# by scanning only the ROOT manifest's direct deps — so a transitive dep's -# opt-out was silently dropped: activation still seeded the dep's default -# feature (defining its macro / keeping its default-gated sources) that -# resolution had already skipped. Now both consume the authoritative -# consumer→dep edge graph, so they agree. -# -# Layout: root(bin) -> A(lib) -> B(lib, `default = ["heavy"]`, heavy defines -# B_HEAVY). A depends on B with `default-features = false`. B's heavy macro must -# therefore be OFF in the transitive build. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -# B: a library with a default feature `heavy` that defines B_HEAVY. -mkdir -p B/src -cat > B/mcpp.toml <<'EOF' -[package] -name = "B" -version = "0.1.0" -[modules] -sources = ["src/**/*.cpp"] -[features] -default = ["heavy"] -heavy = { defines = ["B_HEAVY=1"] } -[targets.B] -kind = "lib" -EOF -cat > B/src/b.cpp <<'EOF' -int b_heavy() { -#ifdef B_HEAVY - return 1; -#else - return 0; -#endif -} -EOF - -# A: depends on B, opting OUT of B's default features. -mkdir -p A/src -cat > A/mcpp.toml <<'EOF' -[package] -name = "A" -version = "0.1.0" -[modules] -sources = ["src/**/*.cpp"] -[dependencies] -B = { path = "../B", default-features = false } -[targets.A] -kind = "lib" -EOF -cat > A/src/a.cpp <<'EOF' -extern int b_heavy(); -int a_val() { return b_heavy(); } -EOF - -# root consumer -> A (which transitively pulls B). -mkdir -p app/src -cat > app/mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" -[modules] -sources = ["src/**/*.cpp"] -[dependencies] -A = { path = "../A" } -[targets.app] -kind = "bin" -main = "src/main.cpp" -EOF -cat > app/src/main.cpp <<'EOF' -import std; -extern int a_val(); -int main() { std::println("heavy={}", a_val()); return 0; } -EOF - -cd app -"$MCPP" build > build.log 2>&1 || { cat build.log; echo "FAIL: build failed"; exit 1; } -out="$("$MCPP" run 2>&1 | grep '^heavy=' | tail -1)" -[[ "$out" == "heavy=0" ]] || { - echo "FAIL: transitive default-features opt-out not honored (got '$out', want heavy=0)" - exit 1 -} - -# Control: flip A to KEEP B's defaults -> heavy must come back on. Clean first: -# changing a transitive dep's active feature set across an in-place rebuild is a -# separate fingerprint concern; this test isolates the activation logic with a -# fresh build. -cat > ../A/mcpp.toml <<'EOF' -[package] -name = "A" -version = "0.1.0" -[modules] -sources = ["src/**/*.cpp"] -[dependencies] -B = { path = "../B" } -[targets.A] -kind = "lib" -EOF -"$MCPP" clean > /dev/null 2>&1 || true -"$MCPP" build > build2.log 2>&1 || { cat build2.log; echo "FAIL: control build failed"; exit 1; } -out2="$("$MCPP" run 2>&1 | grep '^heavy=' | tail -1)" -[[ "$out2" == "heavy=1" ]] || { - echo "FAIL: control (defaults kept) expected heavy=1, got '$out2'" - exit 1 -} - -echo "OK" diff --git a/tests/e2e/128_feature_forwarding.sh b/tests/e2e/128_feature_forwarding.sh deleted file mode 100755 index 216964628..000000000 --- a/tests/e2e/128_feature_forwarding.sh +++ /dev/null @@ -1,142 +0,0 @@ -#!/usr/bin/env bash -# 128_feature_forwarding.sh — feature dep/feat forwarding (#243, Cargo parity). -# A package feature may open a feature OF a dependency: -# -# [features] -# withextra = ["leaf/extra"] # when `withextra` is active, request -# # `extra` from dependency `leaf` -# -# This is what unblocks a module package's optional module interface (the -# opencv.dnn case): a single feature both pulls a source set locally AND opens -# the heavy feature of the compat dependency, without forcing every consumer to -# pay for it. -# -# Graph: app -> mid -> leaf. `leaf` has a feature `extra` (NOT default) that -# carries a package-owned define LEAF_EXTRA. `mid` forwards `leaf/extra` behind -# its own `withextra` feature and pulls leaf with default-features = false (so -# leaf's default set never brings `extra` on its own). The forward must: -# * activate leaf's `extra` ONLY when the consumer turns on mid/withextra, -# * travel TRANSITIVELY (root app -> mid -> leaf), -# * hold on BOTH `mcpp build` and `mcpp test` (the 0.0.97 double-path -# invariant — activation must not diverge between the two). -# -# No `requires:` capability → runs on all three CI platforms. -set -e -# The project-root copy of the compile database is opt-in since -# 2026.10.26.1 ([build] compile_commands); this test reads it. -export MCPP_COMPILE_COMMANDS=compile_commands.json - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -# ── leaf: a lib whose non-default feature `extra` carries a define ─────────── -mkdir -p leaf/include/leaf leaf/src -cat > leaf/mcpp.toml <<'EOF' -[package] -name = "leaf" -version = "0.1.0" - -[features] -default = [] -extra = { defines = ["LEAF_EXTRA=1"] } - -[build] -include_dirs = ["include"] - -[targets.leaf] -kind = "lib" -EOF -cat > leaf/include/leaf/leaf.hpp <<'EOF' -#pragma once -inline int leaf_mode() { -#ifdef LEAF_EXTRA - return 1; -#else - return 0; -#endif -} -EOF -cat > leaf/src/leaf.cppm <<'EOF' -export module leaf; -export int leaf_anchor() { return 0; } -EOF - -# ── mid: depends on leaf (default-features = false), forwards leaf/extra ───── -mkdir -p mid/src -cat > mid/mcpp.toml <<'EOF' -[package] -name = "mid" -version = "0.1.0" - -[features] -default = [] -withextra = ["leaf/extra"] - -[dependencies] -leaf = { path = "../leaf", default-features = false } - -[targets.mid] -kind = "lib" -EOF -cat > mid/src/mid.cppm <<'EOF' -export module mid; -export int mid_anchor() { return 0; } -EOF - -# helper: does leaf's compile command carry the extra-feature define? -leaf_has_extra() { # $1 = compile_commands.json path - grep -q 'LEAF_EXTRA' "$1" -} - -# ── Case A: app turns on mid/withextra → forward opens leaf/extra ──────────── -mkdir -p appon/src -cat > appon/mcpp.toml <<'EOF' -[package] -name = "appon" -version = "0.1.0" - -[dependencies] -mid = { path = "../mid", features = ["withextra"] } -EOF -cat > appon/src/main.cpp <<'EOF' -int main() { return 0; } -EOF - -( cd appon - "$MCPP" build > b.log 2>&1 || { cat b.log; echo "FAIL(A): build failed"; exit 1; } - leaf_has_extra compile_commands.json || { - echo "FAIL(A): leaf/extra was NOT activated by the forward"; cat compile_commands.json; exit 1; } - # double-path: the same forward must hold under `mcpp test`. - "$MCPP" test > t.log 2>&1 || { cat t.log; echo "FAIL(A): test build failed"; exit 1; } - leaf_has_extra compile_commands.json || { - echo "FAIL(A/test): forward did not hold on the test path"; cat compile_commands.json; exit 1; } -) - -# ── Case B: app does NOT turn on withextra → leaf/extra stays off ──────────── -mkdir -p appoff/src -cat > appoff/mcpp.toml <<'EOF' -[package] -name = "appoff" -version = "0.1.0" - -[dependencies] -mid = { path = "../mid" } -EOF -cat > appoff/src/main.cpp <<'EOF' -int main() { return 0; } -EOF - -( cd appoff - "$MCPP" build > b.log 2>&1 || { cat b.log; echo "FAIL(B): build failed"; exit 1; } - if leaf_has_extra compile_commands.json; then - echo "FAIL(B): leaf/extra leaked though no consumer requested mid/withextra" - cat compile_commands.json; exit 1 - fi - "$MCPP" test > t.log 2>&1 || { cat t.log; echo "FAIL(B): test build failed"; exit 1; } - if leaf_has_extra compile_commands.json; then - echo "FAIL(B/test): leaf/extra leaked on the test path"; cat compile_commands.json; exit 1 - fi -) - -echo "OK" diff --git a/tests/e2e/138_feature_sources_gate_vs_provider.sh b/tests/e2e/138_feature_sources_gate_vs_provider.sh deleted file mode 100755 index e7bb24103..000000000 --- a/tests/e2e/138_feature_sources_gate_vs_provider.sh +++ /dev/null @@ -1,147 +0,0 @@ -#!/usr/bin/env bash -# requires: unix-shell -# A feature that GATES a source and a feature that PROVIDES one, under both -# `mcpp build` and `mcpp test`. -# -# THIS DEFECT SURVIVED FOR MONTHS BECAUSE NO TEST COVERED BOTH FAMILIES, AND -# THREE ATTEMPTED FIXES EACH BROKE THE ONE THEY DID NOT COVER. -# -# Two shapes of package reach the same code in `prepare_build`, and under -# `mcpp test` they want opposite things: -# -# the GATE family gtest lists `*/googletest/src/gtest_main.cc` in base -# `sources` AND under `features.main`. The package -# provides the file unconditionally; the feature is a -# switch over it. The dev-dependency track's per-test -# main detection has to SEE it to prune it per test, so -# an inactive gate must not make it disappear. -# -# the PROVIDER family riscv-virt-rt names `src/kal/**` under -# `features.openkal` and nowhere else. Those files are -# not part of the package without the feature — the -# headers they include arrive through that feature's -# `[feature-deps]` — so compiling them fails on a header -# that was never meant to be there. -# -# The engine gated the whole exclusion on "is this a test build", which is -# right for the first family and wrong for the second. Fixing it by removing -# the gate broke gtest; fixing it by removing only the glob string was a no-op, -# because a provider's glob is not in base `sources` at all and its files are -# matched by the inferred `src/**`. -# -# Both families are asserted here so that the next change to this code cannot -# satisfy one at the other's expense. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -# ── The PROVIDER family ───────────────────────────────────────────────────── -# -# `src/gated/**` is named only by the feature, and the file includes a header -# that does not exist. With the feature inactive both commands must ignore it; -# a command that compiles it fails on the missing header, which is exactly the -# symptom the real package produced. -mkdir -p provider/src/gated provider/tests -cat > provider/mcpp.toml <<'EOF' -[package] -name = "provider" -version = "0.1.0" - -# NO `[build] sources`, WHICH IS THE POINT. The base set is the inferred -# `src/**`, which matches `src/gated/only_with_feature.cpp` — so removing the -# feature's glob STRING from a list it was never in gates nothing. -[features] -default = [] -gated = { sources = ["src/gated/**"] } -EOF -cat > provider/src/lib.cpp <<'EOF' -int provider_value() { return 7; } -EOF -cat > provider/src/gated/only_with_feature.cpp <<'EOF' -// The header arrives with the feature's dependencies and does not exist -// without it. Compiling this file without the feature is the defect. -#include -int gated_value() { return 1; } -EOF -cat > provider/tests/basic.cpp <<'EOF' -extern int provider_value(); -int main() { return provider_value() == 7 ? 0 : 1; } -EOF - -cd provider -"$MCPP" build > build.log 2>&1 || { cat build.log; echo "provider: build compiled a feature-only source"; exit 1; } -"$MCPP" test > test.log 2>&1 || { cat test.log; echo "provider: TEST compiled a feature-only source — the defect this file exists for"; exit 1; } - -if find target -name '*only_with_feature*' | grep -q .; then - find target -name '*only_with_feature*' - echo "provider: an object was produced for a source the feature did not activate" - exit 1 -fi - -# With the feature ON it must be reached — otherwise the check above would pass -# for a build that simply ignores feature sources entirely. -if "$MCPP" build --features gated > gated.log 2>&1; then - cat gated.log - echo "provider: the feature did not bring its own source in" - exit 1 -fi -grep -q "a_header_the_feature_would_have_brought.h" gated.log || { - cat gated.log - echo "provider: activating the feature failed for some other reason"; exit 1; } -cd .. - -# ── The GATE family ───────────────────────────────────────────────────────── -# -# The same glob in base `sources` and under a feature. `mcpp build` excludes it; -# `mcpp test` keeps it visible, which is what the dev-dependency track's -# per-test main detection depends on. -mkdir -p gate/src gate/tests -cat > gate/mcpp.toml <<'EOF' -[package] -name = "gate" -version = "0.1.0" - -# `src/gated_main.cpp` IS IN BOTH LISTS, EXACTLY AS gtest'S DESCRIPTOR HAS IT. -# The package provides the file; the feature is a switch over it. -[build] -sources = ["src/lib.cpp", "src/gated_main.cpp"] - -[features] -default = [] -main = { sources = ["src/gated_main.cpp"] } -EOF -cat > gate/src/lib.cpp <<'EOF' -int gate_value() { return 11; } -EOF -cat > gate/src/gated_main.cpp <<'EOF' -int gated_main_marker() { return 42; } -EOF -cat > gate/tests/basic.cpp <<'EOF' -extern int gate_value(); -int main() { return gate_value() == 11 ? 0 : 1; } -EOF - -cd gate -"$MCPP" build > build.log 2>&1 || { cat build.log; echo "gate: build failed"; exit 1; } -if find target -name '*gated_main*' | grep -q .; then - find target -name '*gated_main*' - echo "gate: build compiled a gated source with the feature inactive" - exit 1 -fi - -rm -rf target -"$MCPP" test > test.log 2>&1 || { cat test.log; echo "gate: test failed"; exit 1; } -# THE ASSERTION IS THAT IT IS STILL THERE. A fix that excluded every -# inactive feature glob under `mcpp test` would remove it — and would then -# break gtest, whose per-test main detection needs to see the file in order to -# prune it. Measured once as `ld returned 1 exit status` across mcpp's own -# unit tests. -find target -name '*gated_main*' | grep -q . || { - echo "gate: a source the package provides unconditionally vanished under \`mcpp test\`" - echo " (this is the shape that broke gtest)" - exit 1; } -cd .. - -echo "PASS: a feature that gates a source and a feature that provides one behave correctly under both commands" diff --git a/tests/e2e/146_feature_flags.sh b/tests/e2e/146_feature_flags.sh deleted file mode 100755 index d4b6b26fa..000000000 --- a/tests/e2e/146_feature_flags.sh +++ /dev/null @@ -1,155 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# #253: [features]..flags — per-feature per-glob compile flags. -# Quadrants: feature off/on × build/test (0.0.94 dual-path invariant), plus: -# - feature OFF: the rule does not exist → NO dead-glob warning (the issue's -# opencv mlas noise), and the define is absent; -# - feature ON: the define reaches EXACTLY the glob-matched TU (not the -# package's other TUs — contrast feature `defines`, which are package-wide); -# - feature ON with a dead glob: warning fires, naming the owning feature; -# - flags do NOT propagate to a consumer (contrast feature `defines`, which -# interface-propagate) — asserted in the dep half below. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT - -cd "$TMP" -"$MCPP" new featflags > /dev/null -cd featflags - -mkdir -p src/simd -cat > src/simd/kernel.cpp <<'EOF' -#ifndef KERNEL_TAG -#error "feature flags did not reach the glob-matched TU" -#endif -extern "C" int kernel_tag() { return KERNEL_TAG; } -EOF - -cat > src/plain.cpp <<'EOF' -#ifdef KERNEL_TAG -#error "feature flags leaked outside their glob (must be per-TU, not package-wide)" -#endif -extern "C" int plain_ok() { return 1; } -EOF - -cat > src/main.cpp <<'EOF' -import std; -extern "C" int plain_ok(); -#ifdef MCPP_FEATURE_SIMD -extern "C" int kernel_tag(); -#endif -int main() { -#ifdef MCPP_FEATURE_SIMD - std::println("tag = {}", kernel_tag() + plain_ok()); -#else - std::println("tag = none"); -#endif - return 0; -} -EOF - -# Base sources deliberately do NOT cover src/simd/** (the opencv shape: a -# feature's sources live only under the feature) — so the gated TU is absent -# in every feature-off quadrant, including `mcpp test`, whose drop-skip only -# applies to gated globs that base sources also cover. -cat > mcpp.toml <<'EOF' -[package] -name = "featflags" -version = "0.1.0" - -[build] -sources = ["src/*.cpp"] - -[features] -default = [] -simd = { sources = ["src/simd/**"], flags = [ - { glob = "src/simd/**", defines = ["KERNEL_TAG=6"] } ] } -dead = { flags = [ { glob = "src/never/**", defines = ["NOPE"] } ] } -EOF -# The skeleton's test is mtest's; this manifest names no dev-dependency. -rm -f tests/test_smoke.cpp - -# Quadrant 1: feature OFF, build — rule doesn't exist, so no dead-glob warning. -"$MCPP" build > build_off.log 2>&1 || { cat build_off.log; echo "build (off) failed"; exit 1; } -grep -q "matched no source file" build_off.log && { - cat build_off.log; echo "feature-off build must not warn about feature flag globs"; exit 1; } || true -out="$("$MCPP" run 2>&1 | tail -1)" -[[ "$out" == "tag = none" ]] || { echo "unexpected (off): $out"; exit 1; } - -# Quadrant 2: feature ON, build — define lands on the matched TU only. -"$MCPP" build --features simd > build_on.log 2>&1 || { cat build_on.log; echo "build (on) failed"; exit 1; } -bin="$(ls -t $(find target -name 'featflags' -type f) | head -1)" -[[ -n "$bin" ]] || { echo "binary not found"; exit 1; } -out="$("./$bin" | tail -1)" -[[ "$out" == "tag = 7" ]] || { echo "unexpected (on): $out (want 6+1)"; exit 1; } - -# Active feature with a glob that matches nothing → warning naming the feature. -"$MCPP" build --features simd,dead > build_dead.log 2>&1 || { - cat build_dead.log; echo "build (dead) failed"; exit 1; } -grep -q "features.dead.flags glob 'src/never/\*\*' matched no source file" build_dead.log || { - cat build_dead.log; echo "missing feature-scoped dead-glob warning"; exit 1; } - -# Quadrants 3+4: test path — flags behavior must match build (dual-path). -mkdir -p tests -cat > tests/test_featflags.cpp <<'EOF' -#ifdef MCPP_FEATURE_SIMD -extern "C" int kernel_tag(); -int main() { return kernel_tag() == 6 ? 0 : 1; } -#else -int main() { return 0; } -#endif -EOF -"$MCPP" test > test_off.log 2>&1 || { cat test_off.log; echo "mcpp test (off) failed"; exit 1; } -grep -q "matched no source file" test_off.log && { - cat test_off.log; echo "feature-off test must not warn about feature flag globs"; exit 1; } || true -"$MCPP" test --features simd > test_on.log 2>&1 || { cat test_on.log; echo "mcpp test (on) failed"; exit 1; } - -# --- Dep half: feature flags are PRIVATE to the owning package. --- -# The dep's feature carries both an interface define (propagates) and a -# per-glob flag define (must NOT propagate to the consumer's TUs). -cd "$TMP" -mkdir -p widget/src app/src -cat > widget/mcpp.toml <<'EOF' -[package] -name = "widget" -version = "0.1.0" - -[features] -default = [] -turbo = { defines = ["WIDGET_IFACE=1"], flags = [ - { glob = "src/**", defines = ["WIDGET_PRIVATE=1"] } ] } - -[targets.widget] -kind = "lib" -EOF -cat > widget/src/widget.cppm <<'EOF' -export module widget; -#ifndef WIDGET_PRIVATE -#error "feature flags did not reach the dep's own matched TU" -#endif -export int widget_anchor() { return WIDGET_PRIVATE; } -EOF -cat > app/mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" - -[dependencies] -widget = { path = "../widget", features = ["turbo"] } -EOF -cat > app/src/main.cpp <<'EOF' -import widget; -#ifndef WIDGET_IFACE -#error "feature defines must interface-propagate to the consumer" -#endif -#ifdef WIDGET_PRIVATE -#error "feature flags must NOT propagate to the consumer" -#endif -int main() { return widget_anchor() == 1 ? 0 : 1; } -EOF -cd app -"$MCPP" build > build_dep.log 2>&1 || { cat build_dep.log; echo "dep build failed"; exit 1; } -"$MCPP" run > /dev/null 2>&1 || { echo "dep run failed"; exit 1; } - -echo "OK" diff --git a/tests/e2e/149_conditional_glob_flags.sh b/tests/e2e/149_conditional_glob_flags.sh deleted file mode 100755 index d931e1d55..000000000 --- a/tests/e2e/149_conditional_glob_flags.sh +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/env bash -# #258: per-glob `flags` inside `[target.'cfg()'.build]`. -# -# What this unblocks, in the reporter's words: `sources` is OS-conditional -# but `flags` was not, so one manifest covering three OSes had to present -# every OS with the other two OSes' flag entries — each matching zero sources -# by construction. The vendored-opencv port worked around it with 703 -# committed stub files whose only purpose was to give windows TUs OS-unique -# PATHS that a global flag table could key on, plus 32 globs pointing at -# them, plus ~23 structurally-guaranteed dead-glob warnings on every build. -# -# Three assertions, matching the three costs: -# 1. a matching-OS conditional flag entry reaches the TU; -# 2. it can OVERRIDE the unconditional base table (the removal case: -# unix defines HAVE_UNISTD_H, windows must not); -# 3. an off-OS entry produces NO dead-glob warning — the entry simply does -# not exist on this OS, the same way #253 made a feature-off entry not -# exist. No suppression mechanism, no `optional = true` escape hatch. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT - -cd "$TMP" -"$MCPP" new condflags > /dev/null -cd condflags - -case "$(uname -s)" in - Linux) THIS_OS=linux; OTHER_OS=windows ;; - Darwin) THIS_OS=macos; OTHER_OS=windows ;; - *) THIS_OS=windows; OTHER_OS=linux ;; -esac - -mkdir -p src/zlib - -# Probes the three-way outcome: the base define must be overridden by the -# conditional entry that lands after it, and the conditional-only define -# must be present. -cat > src/zlib/probe.cpp <<'EOF' -#ifndef BASE_DEFINE -#error "base [build].flags entry did not reach the TU" -#endif -#ifndef COND_DEFINE -#error "conditional [target.cfg(os)] flags entry did not reach the TU" -#endif -#ifdef REMOVED_DEFINE -#error "conditional entry failed to override the base table (removal case)" -#endif -extern "C" int probe_ok() { return 1; } -EOF - -cat > src/main.cpp <<'EOF' -import std; -extern "C" int probe_ok(); -int main() { std::println("probe = {}", probe_ok()); return 0; } -EOF - -# The base table defines REMOVED_DEFINE for everyone; the matching-OS -# conditional entry appends -U for it (last flag wins) and adds its own. -# The off-OS entry points at a glob that matches nothing on this host and -# must stay silent. -cat > mcpp.toml < build.log 2>&1 || { cat build.log; echo "conditional glob flags build failed"; exit 1; } - -out=$("$MCPP" run 2>&1 | tail -1) -[[ "$out" == "probe = 1" ]] || { echo "unexpected output: $out"; cat build.log; exit 1; } - -# Assertion 3 — the whole point of the "23 warnings" half of the report. -if grep -q "matched no source file" build.log; then - cat build.log - echo "FAIL: an off-OS conditional flags entry produced a dead-glob warning;" - echo " off-OS entries must not exist on this OS at all" - exit 1 -fi - -# Control: a genuinely dead glob in the UNCONDITIONAL table must still warn. -# The fix removes structural noise, not real signal. -cat >> mcpp.toml <<'EOF' - -[[build.flags]] -glob = "src/nonexistent_dir/**" -defines = ["NEVER=1"] -EOF -"$MCPP" build > build2.log 2>&1 || { cat build2.log; exit 1; } -grep -q "matched no source file" build2.log || { - cat build2.log - echo "FAIL: a real dead glob in [build].flags must still warn" - exit 1 -} - -echo "OK" diff --git a/tests/e2e/195_target_cfg_feature_deps.sh b/tests/e2e/195_target_cfg_feature_deps.sh deleted file mode 100755 index 9b3ad322b..000000000 --- a/tests/e2e/195_target_cfg_feature_deps.sh +++ /dev/null @@ -1,147 +0,0 @@ -#!/usr/bin/env bash -# 195_target_cfg_feature_deps.sh — #359 D3a: `[target..feature-deps.]`. -# -# The conditional channel carried `dependencies`, `dev-dependencies` and -# `build-dependencies` and silently lacked `feature-deps`. That is the shape -# ConditionalConfig's own comment records for #258: the conditional reader -# keeps its own subset of the keys and falls behind without anyone noticing. -# -# It became load-bearing once a library could re-export build-time provisions. -# A library that puts a host tool behind a feature has no other way to say "not -# on this platform", and an unconditional declaration turns an unsupported -# platform into a hard error raised from inside the LIBRARY's manifest — which -# its user cannot edit and cannot work around. -# -# Two properties: -# 1. a matching predicate's feature-deps are pulled in when the feature is on -# 2. a non-matching predicate's are not — but the FEATURE still exists, so -# requesting it on that platform is not an unknown-feature error -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -mkdir -p widget/src -cat > widget/mcpp.toml <<'EOF' -[package] -name = "widget" -version = "0.1.0" -[targets.widget] -kind = "lib" -EOF -cat > widget/src/widget.cppm <<'EOF' -export module widget; -export int widget_anchor() { return 0; } -EOF - -mkdir -p lib/src -cat > lib/mcpp.toml <<'EOF' -[package] -name = "mylib" -version = "0.1.0" - -[build] -sources = ["src/mylib.cpp"] - -# Exactly one of these matches on any of the three CI platforms, so the -# `codegen` feature always pulls widget in. -[target.'cfg(unix)'.feature-deps.codegen] -widget = { path = "../widget", visibility = "public" } -[target.'cfg(windows)'.feature-deps.codegen] -widget = { path = "../widget", visibility = "public" } - -# Never matches. If a non-matching section were merged, resolving this bogus -# path would fail the build. -[target.'cfg(arch = "no_such_arch")'.feature-deps.codegen] -ghost = { path = "../this_path_does_not_exist" } -EOF -printf 'int mylib_fn(){return 1;}\n' > lib/src/mylib.cpp - -# ── 1. feature on: the matching platform's feature-dep is resolved ────────── -mkdir -p app/src -cat > app/mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" - -[dependencies] -mylib = { path = "../lib", features = ["codegen"] } -EOF -cat > app/src/main.cpp <<'EOF' -import widget; // only reachable if the cfg-matched feature-dep resolved -int main() { return widget_anchor(); } -EOF -cd app -"$MCPP" build > b1.log 2>&1 || { - cat b1.log - echo "FAIL: a matching [target.*.feature-deps] was not merged" - exit 1 -} -if [ -f mcpp.lock ] && grep -q 'ghost' mcpp.lock; then - echo "FAIL: a non-matching predicate's feature-dep was pulled in"; exit 1 -fi - -# ── 2. feature off: nothing is pulled in ─────────────────────────────────── -cd "$TMP" -mkdir -p plain/src -cat > plain/mcpp.toml <<'EOF' -[package] -name = "plain" -version = "0.1.0" - -[dependencies] -mylib = { path = "../lib" } -EOF -printf 'int main(){}\n' > plain/src/main.cpp -cd plain -"$MCPP" build > b2.log 2>&1 || { cat b2.log; echo "FAIL: plain build failed"; exit 1; } -if [ -f mcpp.lock ] && grep -q 'widget' mcpp.lock; then - echo "FAIL: an inactive feature's conditional dep was resolved anyway"; exit 1 -fi - -# ── 3. the feature exists on EVERY platform ──────────────────────────────── -# Only what it pulls in is conditional. A consumer requesting it where no -# predicate matches must not be told the feature does not exist — that error -# would be the library's platform support leaking into its user's manifest. -cd "$TMP" -mkdir -p nomatch/src -cat > nomatch/mcpp.toml <<'EOF' -[package] -name = "nomatchlib" -version = "0.1.0" - -[build] -sources = ["src/nm.cpp"] - -[target.'cfg(arch = "no_such_arch")'.feature-deps.codegen] -ghost = { path = "../this_path_does_not_exist" } -EOF -printf 'int nm(){return 1;}\n' > nomatch/src/nm.cpp -mkdir -p nmapp/src -cat > nmapp/mcpp.toml <<'EOF' -[package] -name = "nmapp" -version = "0.1.0" - -[dependencies] -nomatchlib = { path = "../nomatch", features = ["codegen"] } -EOF -printf 'int main(){}\n' > nmapp/src/main.cpp -cd nmapp -# Asserted on the DIAGNOSTIC rather than with --strict: --strict promotes every -# degradation, including unrelated ones (clang on Windows reports "this -# toolchain and platform combination emits no GNU depfile"), so it would make -# this test fail for a reason it is not about. -"$MCPP" build > b3.log 2>&1 || { - cat b3.log - echo "FAIL: requesting a conditionally-populated feature failed where no predicate matches" - exit 1 -} -grep -q "does not declare requested feature" b3.log && { - cat b3.log - echo "FAIL: the feature was reported as undeclared where no predicate matches" - exit 1 -} - -echo "PASS: 195_target_cfg_feature_deps" diff --git a/tests/e2e/247_bare_triple_conditional_native.sh b/tests/e2e/247_bare_triple_conditional_native.sh deleted file mode 100755 index 938f3d095..000000000 --- a/tests/e2e/247_bare_triple_conditional_native.sh +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/env bash -# requires: -# (no capability: the probe greps the MACRO NAME, never the `-D` prefix — MSVC -# spells that `/D`, and the prefix was the only thing that tied this test to -# one compiler family. The predicate it checks is compiler-independent, so the -# test has to run everywhere the predicate does.) -# 247_bare_triple_conditional_native.sh — `[target.''.build]` applies to -# a NATIVE build, not only to one with an explicit `--target`. -# -# It did not, and the failure shape is the dangerous one: CI passes `--target` -# and is green, the developer's plain `mcpp build` silently drops the section, -# and whatever it carried (a `-L`, a define, a source) goes missing somewhere -# far from the manifest. `cfg(linux)` matched the same build all along, so the -# two spellings of one statement disagreed. -# -# The probe asserts BOTH spellings under BOTH invocations. Asserting only the -# native case would pass against a build that applies every section always. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -# The host triple, asked of mcpp rather than guessed. -# -# The first version pattern-matched `mcpp self env` for -# `--` and fell back to `x86_64-linux-gnu`. macOS's canonical -# triple is `aarch64-macos` — TWO segments, no env — so the match failed, the -# fallback was used, and the test then asserted that a *Linux* section applied -# to a macOS build. It reported "the bare triple was inert" against a product -# that was behaving correctly. -# -# `target//` is mcpp's own answer to the same question, so take it from -# there: build once with nothing conditional, read the directory name, then -# write the real manifest. -mkdir -p probe/src -echo 'int main() { return 0; }' > probe/src/main.cpp -cat > probe/mcpp.toml <<'EOF' -[package] -name = "probe" -version = "0.1.0" -[targets.probe] -kind = "bin" -main = "src/main.cpp" -EOF -( cd probe && "$MCPP" build > discover.log 2>&1 ) \ - || { cat probe/discover.log; echo "discovery build failed"; exit 1; } -host_triple="$(ls probe/target | head -1)" -[[ -n "$host_triple" ]] || { ls -R probe/target; echo "no target/ dir"; exit 1; } -echo "host triple: $host_triple" - -cat > probe/mcpp.toml <`: the prefix is dialect-specific (`/D` on MSVC), - # and what this test is about is whether the section applied at all. - local nj; nj="$(find target -name build.ninja -not -path '*/.build-mcpp/*' | head -1)" - grep -c -- "$1" "$nj" || true -} - -cd probe - -# ── native: no --target ──────────────────────────────────────────────── -rm -rf target -"$MCPP" build > native.log 2>&1 || { cat native.log; echo "native build failed"; exit 1; } -bare="$(count MCPP_BARE_TRIPLE)" -alias_="$(count MCPP_CFG_ALIAS)" -[[ "$alias_" -gt 0 ]] || { echo "cfg() did not apply on a native build"; exit 1; } -[[ "$bare" -gt 0 ]] || { - echo "FAIL: [target.'$host_triple'.build] was inert on a native build" - echo " (cfg() applied $alias_ times, the bare triple $bare)" - exit 1; } - -# ── explicit --target: unchanged ─────────────────────────────────────── -rm -rf target -"$MCPP" build --target "$host_triple" > explicit.log 2>&1 \ - || { cat explicit.log; echo "explicit build failed"; exit 1; } -[[ "$(count MCPP_BARE_TRIPLE)" -gt 0 ]] || { echo "bare triple inert with --target"; exit 1; } -[[ "$(count MCPP_CFG_ALIAS)" -gt 0 ]] || { echo "cfg() inert with --target"; exit 1; } - -echo "PASS: a bare-triple conditional applies to native and explicit builds alike" diff --git a/tests/e2e/308_dependency_required_features.sh b/tests/e2e/308_dependency_required_features.sh deleted file mode 100755 index 96e347544..000000000 --- a/tests/e2e/308_dependency_required_features.sh +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/env bash -# requires: elf -# 308_dependency_required_features.sh — a DEPENDENCY's target gate (issue #519). -# -# `[targets.] required_features` was filtered for the root package only. -# A dependency that wrote it got the OPPOSITE of what it asked for: the target -# was emitted for every consumer, active feature or not. -# -# That is not cosmetic when the gated target is `kind = "shared"`. A package -# with any shared target has ALL of its objects taken out of every consumer's -# link and put behind a shared library instead — so an "optional" target -# silently changed how the package was linked into everyone. -# -# The criterion is the ARTIFACT, not a log line: with the feature off there -# must be no shared object; with it on there must be one. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -mkdir -p dep/src app/src - -cat > dep/mcpp.toml <<'EOF' -[package] -name = "dep" -version = "0.1.0" -[build] -c_standard = "c11" -sources = ["src/*.c"] -[features] -plugin = [] -[targets.dep] -kind = "lib" -[targets.depshared] -kind = "shared" -required_features = ["plugin"] -EOF -cat > dep/src/dep.c <<'EOF' -int dep_value(void) { return 5; } -EOF - -cat > app/mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" -[build] -c_standard = "c11" -[targets.app] -kind = "bin" -main = "src/main.c" -[dependencies] -dep = { path = "../dep" } -EOF -cat > app/src/main.c <<'EOF' -#include -int dep_value(void); -int main(void) { printf("%d\n", dep_value()); return 0; } -EOF - -cd app - -# ── the gate is CLOSED: no shared object, and the program still works ─────── -"$MCPP" build > off.log 2>&1 || { cat off.log; exit 1; } -if find target -name 'libdepshared.so' | grep -q .; then - echo "FAIL: a feature-gated dependency target was built with the feature off" - find target -name 'libdepshared.so' - exit 1 -fi -bin="$(find target -name app -type f | head -1)" -[[ -n "$bin" ]] || { cat off.log; echo "FAIL: no executable"; exit 1; } -"$bin" | grep -qx 5 || { echo "FAIL: the gated-off build does not run"; exit 1; } - -# ── the gate is OPEN: the target appears ─────────────────────────────────── -cat > mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" -[build] -c_standard = "c11" -[targets.app] -kind = "bin" -main = "src/main.c" -[dependencies] -dep = { path = "../dep", features = ["plugin"] } -EOF - -"$MCPP" build > on.log 2>&1 || { cat on.log; exit 1; } -so="$(find target -name 'libdepshared.so' -type f | head -1)" -[[ -n "$so" ]] || { - echo "FAIL: requesting the feature did not build the gated target" - cat on.log; exit 1; } - -echo "ok: a dependency's required_features gate opens and closes" diff --git a/tests/e2e/328_layer_predicates_apply_and_unknown_ones_say_so.sh b/tests/e2e/328_layer_predicates_apply_and_unknown_ones_say_so.sh deleted file mode 100755 index 00fe257af..000000000 --- a/tests/e2e/328_layer_predicates_apply_and_unknown_ones_say_so.sh +++ /dev/null @@ -1,235 +0,0 @@ -#!/usr/bin/env bash -# 328_layer_predicates_apply_and_unknown_ones_say_so.sh — #540 / #494. -# -# docs/14 documents a package adapting to the C library it was built over: -# -# [target.'cfg(c-abi = "musl")'.build] -# std-module-flags = ["-D_GNU_SOURCE"] # wrong for picolibc -# -# and #494 moved those keys onto BuildInputs FOR this, its member comment -# saying membership "is what makes the cfg axis carry it". Nothing evaluated -# the predicate: `cfgpred::Ctx` was built from the target triple alone, so -# `match_kv` knew only os/arch/family/env and every layer section was dropped — -# in SILENCE, because a predicate that answers false and a predicate that was -# never understood produced the same nothing. Measured on 2026.8.30.2: a -# `cfg(c-abi = "glibc")` section contributed no define and emitted no warning. -# -# Three properties: -# (1) a layer predicate that matches APPLIES; -# (2) a layer predicate that does not match does NOT apply — otherwise (1) -# is satisfied by a pass that fires unconditionally; -# (3) a key outside the vocabulary is REPORTED, and is an error under -# --strict. -# -# requires: gcc unix-shell -set -e -# The project-root copy of the compile database is opt-in since -# 2026.10.26.1 ([build] compile_commands); this test reads it. -export MCPP_COMPILE_COMMANDS=compile_commands.json - -MCPP="${MCPP:-mcpp}" -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -# The resolved c-abi for this build, read from mcpp's own report rather than -# assumed. The report prints layers whose origin is not the compiler payload -# by default, so the zero-config case needs MCPP_VERBOSE to list all five. -mkdir -p probe/src -cat > probe/mcpp.toml <<'EOF' -[package] -name = "probe" -version = "0.1.0" -EOF -echo 'int main() { return 0; }' > probe/src/main.cpp -( cd probe && MCPP_VERBOSE=1 "$MCPP" build > report.log 2>&1 ) || { - cat probe/report.log; echo "FAIL: the probe build errored"; exit 1; } -CABI=$(sed -n 's/.*c-abi *\([A-Za-z0-9_+-]*\).*/\1/p' probe/report.log | head -1) -[ -n "$CABI" ] || { cat probe/report.log; echo "FAIL: could not read the resolved c-abi"; exit 1; } -echo " .. this host resolves c-abi = '$CABI'" - -# A NAME NO LAYER CAN TAKE, for the negative leg. Deriving it (rather than -# hardcoding "musl") keeps the leg meaningful on a musl host, where a hardcoded -# counter-example would be the TRUE case and the test would assert nothing. -NOTCABI="not-${CABI}" - -# ── (1) + (2) a matching layer predicate applies; a non-matching one does not ── -# -# THE TRIPLE HALF OF THE COMBINED PREDICATE IS any(unix, windows), NOT unix. -# The point of that leg is that a triple key and a layer key COMBINE, so its -# triple half has to be true everywhere this test runs. Written as bare unix it -# was true on Linux and macOS and false on Windows, where all(...) then -# correctly evaluated false and the fixture's #error fired. CI caught it; the -# feature was right and the test was not. -# -# AND THESE NOTES LIVE OUTSIDE THE HEREDOCS. The fixtures below interpolate -# $CABI, so their heredocs are unquoted — which makes backticks in a comment -# command substitution. The first version of this note sat inside one and the -# suite printed `syntax error: unexpected end of file` while still passing. -mkdir -p layers/src -cat > layers/mcpp.toml < layers/src/main.cpp <<'EOF' -#ifndef PROBE_MATCHED -#error "a matching cfg(c-abi = ...) section did not apply" -#endif -#ifdef PROBE_MUST_NOT_APPLY -#error "a NON-matching cfg(c-abi = ...) section applied" -#endif -#ifndef PROBE_COMBINED -#error "cfg(all(unix, c-abi = ...)) did not apply" -#endif -int main() { return 0; } -EOF -( cd layers && "$MCPP" build > b.log 2>&1 ) || { - cat layers/b.log; echo "FAIL: layer predicates did not behave"; exit 1; } -echo " ok a matching layer predicate applies, a non-matching one does not" - -# EXACTLY ONCE, NOT AT LEAST ONCE. The pass that evaluates layer predicates -# runs AFTER the triple-only merge, and `append()` is additive — so a predicate -# with a triple leg (`any(unix, c-abi = ...)`) would be matched by both passes -# and contribute twice. Counting the flag is the only way to see that; a -# preprocessor check cannot tell one -D from two. -mkdir -p once/src -cat > once/mcpp.toml < once/src/main.cpp -( cd once && "$MCPP" build > b.log 2>&1 ) || { - cat once/b.log; echo "FAIL: the mixed-predicate build errored"; exit 1; } -# Both spellings: a Windows self-host may drive MSVC, which takes `/D`. -n=$(grep -oE '[-/]DPROBE_ONCE=1' once/compile_commands.json | wc -l | tr -d ' ') -[ "$n" -eq 1 ] || { - echo "FAIL: a predicate with both a triple leg and a layer leg contributed $n times, expected 1" - grep -o '\-DPROBE_ONCE=1' once/compile_commands.json - exit 1; } -echo " ok a predicate naming both a triple key and a layer applies exactly once" - -# ── (2b) …and it reaches a DEPENDENCY, which is the motivating case ───────── -# -# EVERY LEG ABOVE IS SATISFIED BY A PASS THAT ONLY PATCHES packages[0]. -# docs/14 writes this feature for "a package supplying a layer [that] frequently -# supports several implementations of the layer beneath it" — a LIBRARY, reached -# as someone's dependency. The build.mcpp tail that shares this pass's window -# patches the root alone, correctly for its own purpose, so copying that shape -# would have left the one package this feature exists for unserved and every -# root-only assertion still green. -mkdir -p dep/app/src dep/lib/src -cat > dep/lib/mcpp.toml < dep/lib/src/layerlib.cppm <<'EOF' -export module layerlib; -#ifndef LIB_MATCHED -#error "a DEPENDENCY's matching cfg(c-abi = ...) section did not apply" -#endif -#ifdef LIB_MUST_NOT_APPLY -#error "a DEPENDENCY's non-matching cfg(c-abi = ...) section applied" -#endif -export int lib_answer() { return 42; } -EOF -cat > dep/app/mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" - -[dependencies] -layerlib = { path = "../lib" } -EOF -cat > dep/app/src/main.cpp <<'EOF' -import layerlib; -int main() { return lib_answer() == 42 ? 0 : 1; } -EOF -( cd dep/app && "$MCPP" build > b.log 2>&1 ) || { - cat dep/app/b.log; echo "FAIL: layer predicates did not reach a dependency"; exit 1; } -echo " ok a layer predicate reaches a dependency, and its negative leg holds" - -# ── (3) an unknown key is reported, and --strict makes it an error ────────── -mkdir -p unknown/src -cat > unknown/mcpp.toml <<'EOF' -[package] -name = "unknown" -version = "0.1.0" - -[target.'cfg(no-such-key = "x")'.build] -defines = ["NEVER"] -EOF -echo 'int main() { return 0; }' > unknown/src/main.cpp -( cd unknown && "$MCPP" build > w.log 2>&1 ) || { - cat unknown/w.log; echo "FAIL: an unknown cfg key must warn, not fail"; exit 1; } -grep -q "no-such-key" unknown/w.log || { - cat unknown/w.log; echo "FAIL: the unknown cfg key was not named"; exit 1; } -# The message must list the vocabulary it checked against — built FROM that -# list, so it cannot drift from the check the way three other key lists did. -grep -q "c-abi" unknown/w.log || { - cat unknown/w.log; echo "FAIL: the message does not list the supported keys"; exit 1; } -echo " ok an unknown cfg key is named, with the vocabulary it was checked against" - -set +e -( cd unknown && "$MCPP" build --strict > s.log 2>&1 ) -rc=$? -set -e -[ "$rc" -ne 0 ] || { cat unknown/s.log; echo "FAIL: --strict did not turn the warning into an error"; exit 1; } -echo " ok --strict makes it an error" - -# ── (4) a layer predicate cannot select dependencies, and says so ─────────── -# -# A layer is resolved FROM the dependency graph, so a dependency chosen by one -# would decide the answer it is asking for — docs/14 states the limit. The limit -# is not the interesting part; being told about it is. By the time the layer -# pass runs, dependency resolution is over, so such a section would otherwise be -# dropped in exactly the silence this whole test exists to end. -# -# AND THE BUILD INPUTS UNDER THE SAME PREDICATE MUST STILL APPLY. A warning -# that quietly disabled the rest of the section would trade one silent drop for -# another. -mkdir -p depcond/src -cat > depcond/mcpp.toml < depcond/src/main.cpp <<'EOF' -#ifndef INPUTS_STILL_APPLY -#error "the warning about dependencies also suppressed the build inputs" -#endif -int main() { return 0; } -EOF -( cd depcond && "$MCPP" build > d.log 2>&1 ) || { - cat depcond/d.log; echo "FAIL: a layer-conditioned dependency must warn, not fail"; exit 1; } -grep -q "conditions dependencies on a target-side layer" depcond/d.log || { - cat depcond/d.log; echo "FAIL: no diagnostic for a layer-conditioned dependency"; exit 1; } -echo " ok a layer-conditioned dependency is reported, and the build inputs still apply" - -echo "PASS: 328 layer predicates apply, and unknown ones say so" diff --git a/tests/e2e/601_exclusive_capability.sh b/tests/e2e/601_exclusive_capability.sh deleted file mode 100755 index c8bd800f5..000000000 --- a/tests/e2e/601_exclusive_capability.sh +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env bash -# An exclusive capability with two providers is refused at binding time. -# -# Two packages providing one capability is usually fine and sometimes the -# point: an OpenBLAS package and an MKL package both provide `blas`, and a -# build that links one program against each is legitimate. What is not fine is -# two implementations of ONE accelerator interface in one link: they define the -# same symbols, and the link resolves every call to whichever archive it -# reached first. -# -# The engine cannot tell those apart -- seeing the symbol overlap needs object -# files that do not exist when capabilities are bound -- so the package says it. -# This asserts both halves: the declared-exclusive pair is refused, and the pair -# that declares nothing still builds. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -mkpkg() { # $1 name $2 capability $3 "exclusive"|"" - mkdir -p "$1/src" - { - echo '[package]' - echo "name = \"$1\"" - echo 'version = "0.1.0"' - echo "provides = [\"$2\"]" - [ -n "$3" ] && echo "exclusive = [\"$2\"]" - echo '[language]' - echo 'standard = "c++23"' - echo '[targets.'"$1"']' - echo 'kind = "lib"' - } > "$1/mcpp.toml" - echo "int ${1//-/_}_anchor(void) { return 0; }" > "$1/src/$1.c" -} - -consumer() { # $@ dep names - rm -rf app; "$MCPP" new app > /dev/null; cd app - { - echo '[package]' - echo 'name = "app"' - echo 'version = "0.1.0"' - echo '[language]' - echo 'standard = "c++23"' - echo '[dependencies]' - for d in "$@"; do echo "$d = { path = \"../$d\" }"; done - } > mcpp.toml - rm -f src/*.cppm - echo 'int main(){return 0;}' > src/main.cpp -} - -# ── Half one: two exclusive providers of one capability are refused ───────── -mkpkg gpublas-a gpu-blas exclusive -mkpkg gpublas-b gpu-blas exclusive -consumer gpublas-a gpublas-b - -if "$MCPP" build > out.log 2>&1; then - cat out.log - echo "FAIL: two exclusive providers of 'gpu-blas' were accepted" - exit 1 -fi -grep -q "gpu-blas" out.log || { cat out.log; echo "FAIL: refusal does not name the capability"; exit 1; } -grep -q "gpublas-a" out.log || { cat out.log; echo "FAIL: refusal does not name the first provider"; exit 1; } -grep -q "gpublas-b" out.log || { cat out.log; echo "FAIL: refusal does not name the second provider"; exit 1; } -grep -qi "exclusive" out.log || { cat out.log; echo "FAIL: refusal does not say why"; exit 1; } -echo "PASS: exclusive pair refused, naming the capability and both providers" - -# ── Half two: the SAME graph without the claim still builds ──────────────── -# -# The control matters. Without it this test would also pass if mcpp refused -# every duplicate provider, which is the behaviour the `exclusive` key exists -# to avoid: it would break the documented OpenBLAS/MKL case. -cd "$TMP" -mkpkg blas-a blas -mkpkg blas-b blas -consumer blas-a blas-b - -"$MCPP" build > ok.log 2>&1 || { cat ok.log; echo "FAIL: two ordinary providers were refused"; exit 1; } -echo "PASS: two ordinary providers of one capability coexist" - -echo "PASS: exclusive capability" diff --git a/tests/e2e/615_feature_deps_shared_library_is_linked.sh b/tests/e2e/615_feature_deps_shared_library_is_linked.sh deleted file mode 100755 index 56a84aade..000000000 --- a/tests/e2e/615_feature_deps_shared_library_is_linked.sh +++ /dev/null @@ -1,114 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc unix-shell -# A shared library a LIBRARY package acquires through `[feature-deps]` reaches -# the link line of that package's test binaries. -# -# THE DEFECT THIS COVERS, AND WHY IT WAS QUIET. `[feature-deps]` is folded into -# the root's dependency map during resolution, so the dependency was fetched -# and COMPILED -- every log line said it was there. The plan read the root's -# edges from a snapshot taken BEFORE that fold, so the dependency's shared -# library was neither linked nor named as an input. `mcpp build` on a library -# package still succeeded, because an archive resolves no symbols; the failure -# appeared in whoever linked an executable, as undefined references to the -# dependency's own entry points. -# -# THE SHAPE IS THE CRITERION. The same project written as a BINARY passes on -# both the defective and the fixed engine -- measured -- so a fixture built -# around `mcpp run` would have been a test that could not fail. What surfaces -# the defect is a library root whose test binary does the linking, which is the -# shape every library package in this ecosystem has. -# -# AND THE CRITERION IS THE PROGRAM RUNNING. Grepping build.ninja for -# `-lgreeter` would pass on a build that named the flag and never found the -# library; reading back the string the library owns distinguishes them. -set -e - -MCPP="${MCPP:-mcpp}" -work="$(mktemp -d)" -trap 'rm -rf "$work"' EXIT - -# The provider: a shared library and nothing else. -mkdir -p "$work/greeter/src" -cat > "$work/greeter/mcpp.toml" <<'TOML' -[package] -name = "greeter" -version = "0.1.0" - -[build] -sources = ["src/greeter.cpp"] - -[targets.greeter] -kind = "shared" -TOML -cat > "$work/greeter/src/greeter.cpp" <<'CPP' -extern "C" const char* greeter_line() { return "greeter-from-the-shared-library"; } -CPP - -# The consumer: a library package for which the provider is optional. -mkdir -p "$work/app/src" "$work/app/tests" -cat > "$work/app/mcpp.toml" <<'TOML' -[package] -name = "app" -version = "0.1.0" - -[build] -sources = ["src/lib.cpp"] - -[targets.app] -kind = "lib" - -[features] -default = {} -loud = { defines = ["USE_GREETER"] } - -[feature-deps.loud] -greeter = { path = "../greeter" } -TOML -cat > "$work/app/src/lib.cpp" <<'CPP' -int app_anchor() { return 0; } -CPP -cat > "$work/app/tests/use.cpp" <<'CPP' -#include -#ifdef USE_GREETER -extern "C" const char* greeter_line(); -#endif -int main() { -#ifdef USE_GREETER - std::printf("%s\n", greeter_line()); -#else - std::printf("quiet\n"); -#endif - return 0; -} -CPP - -cd "$work/app" - -fails=0 -check() { - case "$2" in - *"$3"*) printf 'ok: %s\n' "$1" ;; - *) printf 'ASSERT-FAIL: %s -- expected %s in:\n%s\n' "$1" "$3" "$2" - fails=$((fails + 1)) ;; - esac -} - -loud="$("$MCPP" test use --features loud 2>&1 || true)" -rm -rf target -quiet="$("$MCPP" test use 2>&1 || true)" - -printf -- '--- with --features loud ---\n%s\n' "$loud" -printf -- '--- without ---\n%s\n' "$quiet" - -check "the feature's shared library links and runs" \ - "$loud" "greeter-from-the-shared-library" -# The control: without the feature the same project still builds and runs, so -# the reading above is about the feature rather than about the fixture. -check "and the same project builds without the feature" \ - "$quiet" "quiet" - -if [ "$fails" -ne 0 ]; then - printf 'FAIL: %s assertion(s) failed\n' "$fails" - exit 1 -fi -printf 'PASS: a [feature-deps] shared library reaches a test binary link line\n' diff --git a/tests/e2e/637_a_conditional_runtime_table_alone_is_applied.sh b/tests/e2e/637_a_conditional_runtime_table_alone_is_applied.sh deleted file mode 100755 index d822db166..000000000 --- a/tests/e2e/637_a_conditional_runtime_table_alone_is_applied.sh +++ /dev/null @@ -1,129 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# A PREDICATE CARRYING ONLY A RUNTIME TABLE IS STILL APPLIED. -# -# `[target..runtime]` is the dialect-neutral half of a link line: mcpp -# renders `libraries` as `-lm` or `m.lib` depending on the target, which is what -# lets one manifest name a system library for a `cl.exe` consumer and a GNU one -# alike. It has been implemented since 2026.8.29.1, and it worked -- unless it -# was the only thing written under its predicate. -# -# The gate deciding whether to keep a parsed conditional block was a hand-written -# disjunction over the fields its author knew, and `libraries` / -# `link_library_dirs` were added to the struct without being added to it. So the -# block was parsed, populated and dropped. -# -# THE TWO LEGS DIFFER IN ONE DIMENSION THAT HAS NOTHING TO DO WITH LIBRARIES, -# and that is what makes this evidence about the gate rather than about the -# predicate. The second project adds a `defines` entry -- a key the old gate did -# list -- and before the fix that single unrelated line decided whether the -# libraries were applied. -# -# It is also why no existing test caught it: the only fixture exercising the -# neutral form is a GENERATED distribution package, and those always carry -# `ldflags` as well, so the gate always passed. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -make_project() { # $1 = dir, $2 = extra manifest text - mkdir -p "$1/src" - cat > "$1/src/main.cpp" <<'EOF' -int main() { return 0; } -EOF - { cat <<'EOF' -[package] -name = "condrt" -version = "0.1.0" - -[target.linux.runtime] -libraries = ["m"] -EOF - printf '%s\n' "$2"; } > "$1/mcpp.toml" -} - -# Leg A: the runtime table is the ONLY thing under the predicate. -make_project alone "" -# Leg B: the control -- identical, plus one unrelated key the old gate listed. -make_project sibling '[target.linux.build] -defines = ["UNRELATED_TO_LIBRARIES"]' - -for leg in alone sibling; do - ( cd "$leg" && "${MCPP:-mcpp}" build > build.log 2>&1 ) || { - echo "FAIL [$leg]: build" - cat "$leg/build.log" - exit 1; } - graph=$(find "$leg/target" -name build.ninja | head -1) - [ -n "$graph" ] || { echo "FAIL [$leg]: no build graph"; exit 1; } - if ! grep -q -- '-lm\b' "$graph"; then - echo "FAIL [$leg]: [target.linux.runtime] libraries did not reach the link line" - if [ "$leg" = alone ]; then - echo " the block was parsed and then discarded because nothing" - echo " else was written under the same predicate" - fi - grep -n 'ldflags' "$graph" | head -3 - exit 1 - fi - echo "ok [$leg]: -lm present" -done - -# The reverse leg. A predicate that does NOT match this host must contribute -# nothing, or the two assertions above would also be satisfied by an mcpp that -# stopped evaluating predicates at all. -mkdir -p offhost/src -cat > offhost/src/main.cpp <<'EOF' -int main() { return 0; } -EOF -cat > offhost/mcpp.toml <<'EOF' -[package] -name = "condrt" -version = "0.1.0" - -[target.windows.runtime] -libraries = ["user32"] -EOF -( cd offhost && "${MCPP:-mcpp}" build > build.log 2>&1 ) || { - echo "FAIL [offhost]: build"; cat offhost/build.log; exit 1; } -graph=$(find offhost/target -name build.ninja | head -1) -grep -q -- '-luser32' "$graph" && { - echo "FAIL [offhost]: a windows predicate contributed to a linux link line" - exit 1; } -echo "ok [offhost]: a non-matching predicate contributes nothing" - -# An unsupported key in either runtime table is REPORTED, not dropped -- the rule -# [build] and [target.] have each followed since #418 and #249. -mkdir -p typo/src -cat > typo/src/main.cpp <<'EOF' -int main() { return 0; } -EOF -cat > typo/mcpp.toml <<'EOF' -[package] -name = "condrt" -version = "0.1.0" - -[runtime] -dlopen_lib = ["one"] - -[target.linux.runtime] -libraries = ["m"] -framework = ["Cocoa"] -EOF -( cd typo && "${MCPP:-mcpp}" build > build.log 2>&1 ) || { - echo "FAIL [typo]: build"; cat typo/build.log; exit 1; } -grep -q "unsupported key 'dlopen_lib'" typo/build.log || { - echo "FAIL [typo]: a plausible typo in [runtime] was accepted in silence" - cat typo/build.log - exit 1; } -grep -q "unsupported key 'framework'" typo/build.log || { - echo "FAIL [typo]: a plausible typo in [target..runtime] was accepted in silence" - cat typo/build.log - exit 1; } -# The correctly spelled sibling in the same table still took effect, so the -# sweep is not simply reporting everything. -graph=$(find typo/target -name build.ninja | head -1) -grep -q -- '-lm\b' "$graph" || { - echo "FAIL [typo]: the correctly spelled key stopped working" - exit 1; } -echo "ok [typo]: reported, and the neighbouring key still applies" diff --git a/tests/e2e/654_a_requirement_on_the_target_axis.sh b/tests/e2e/654_a_requirement_on_the_target_axis.sh deleted file mode 100755 index d7913b257..000000000 --- a/tests/e2e/654_a_requirement_on_the_target_axis.sh +++ /dev/null @@ -1,132 +0,0 @@ -#!/usr/bin/env bash -# requires: elf gcc -# 654_a_requirement_on_the_target_axis.sh -- design 2026-09-12 (a UI -# framework on Android, iOS and Web), section 2.6, A6: `requires_abi` can sit -# directly under `[target.]` and under -# `[target..feature-requires-abi]`, unioned into the package's -# requirement set only when the selector matches the resolved target. -# -# 647 already covers the UNCONDITIONAL forms (`[package] requires_abi`, and -# `[features.] requires_abi`). This script exercises the TARGET axis: -# 1. a dependency's `[target.'cfg(linux)'] requires_abi = { threads = true }` -# is refused on a Linux host while the root's abi table is absent, -# naming the dependency and the selector `cfg(linux)`, as written; -# 2. with the root's `[target.'cfg(linux)'.abi] threads = true` the build -# succeeds, and -pthread reaches the dependency's compile command; -# 3. the per-feature form (`[target..feature-requires-abi]`) is -# refused only when the feature that names it is active; -# 4. a selector that does not match the host (`cfg(windows)`) imposes -# nothing, in both directions -- present or absent, the Linux build is -# unaffected by a requirement scoped to a target it is not building. -set -e -# The project-root copy of the compile database is opt-in since -# 2026.10.26.1 ([build] compile_commands); this test reads it. -export MCPP_COMPILE_COMMANDS=compile_commands.json - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT - -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } - -mkdir -p "$TMP/tgtdep/src" -cd "$TMP/tgtdep" -printf 'export module tgtdep;\nexport int tgt_value() { return 7; }\n' > src/tgtdep.cppm -write_dep() { # $1 = extra [package]-level table text (requires_abi et al.) - cat > "$TMP/tgtdep/mcpp.toml" < src/main.cpp <<'CPP' -import tgtdep; -int main() { return tgt_value() == 7 ? 0 : 1; } -CPP -write_app() { # $1 = the dependency's inline table, $2 = trailing tables - cat > "$TMP/app/mcpp.toml" < control.log 2>&1 \ - || fail "a windows-only target-axis requirement failed a host build" control.log -plain_dir=$(dirname "$(newest_ninja)") -if pthread_in_dep_command; then - fail "control: -pthread is on the dependency's compile command with no root table" control.log -fi -echo "0. a non-matching selector imposes nothing OK" - -# ── 1. A matching selector's requirement is refused without the root table ─ -write_dep $'[target.\'cfg(linux)\']\nrequires_abi = { threads = true }\n' -write_app '{ path = "../tgtdep" }' '' -if "$MCPP" build > refuse-selector.log 2>&1; then - fail "a target-axis threads requirement was built without the root's table" refuse-selector.log -fi -grep -qF "\`tgtdep\` requires the artefact's ABI to have threads ([target.'cfg(linux)'])" refuse-selector.log \ - || fail "the refusal does not name the dependency and the selector" refuse-selector.log -echo "1. refused, naming the dependency and cfg(linux) OK" - -# ── 2. With the root's table, the build succeeds and threads reach the dep ── -write_app '{ path = "../tgtdep" }' $'[target.\'cfg(linux)\'.abi]\nthreads = true\n' -"$MCPP" build > threads.log 2>&1 || fail "the build with the root's table failed" threads.log -pthread_in_dep_command || fail "-pthread did not reach the dependency's compile command" threads.log -[ "$(dirname "$(newest_ninja)")" != "$plain_dir" ] \ - || fail "the build with threads reused the fingerprint directory of the plain build" threads.log -"$MCPP" run > run.log 2>&1 || fail "the program did not run" run.log -echo "2. the root's table satisfies the target-axis requirement OK" - -# ── 3. The per-feature target-axis form is refused only when active ──────── -rm -rf target -write_dep $'[target.\'cfg(linux)\'.feature-requires-abi]\nmt = { threads = true }\n' -write_app '{ path = "../tgtdep" }' '' -"$MCPP" build > feature-inactive.log 2>&1 \ - || fail "an inactive feature's target-axis requirement failed the build" feature-inactive.log -if pthread_in_dep_command; then - fail "an inactive feature's requirement put -pthread on the compile command" feature-inactive.log -fi -echo " 3a. inactive feature imposes nothing OK" - -write_app '{ path = "../tgtdep", features = ["mt"] }' '' -if "$MCPP" build > feature-active.log 2>&1; then - fail "an active feature's target-axis requirement was built without the root's table" feature-active.log -fi -grep -qF "\`tgtdep\` requires the artefact's ABI to have threads ([target.'cfg(linux)'])" feature-active.log \ - || fail "the refusal does not name the selector for the active feature's requirement" feature-active.log -echo " 3b. active feature is refused, naming cfg(linux) OK" - -write_app '{ path = "../tgtdep", features = ["mt"] }' $'[target.\'cfg(linux)\'.abi]\nthreads = true\n' -"$MCPP" build > feature-satisfied.log 2>&1 \ - || fail "the active feature's requirement was not satisfied by the root's table" feature-satisfied.log -pthread_in_dep_command || fail "-pthread did not reach the dependency's compile command" feature-satisfied.log -echo "3. the per-feature target-axis form OK" - -echo "PASS: 654" diff --git a/tests/e2e/677_a_conditional_dependency_replaces_the_unconditional_one.sh b/tests/e2e/677_a_conditional_dependency_replaces_the_unconditional_one.sh deleted file mode 100755 index 5b8d8e296..000000000 --- a/tests/e2e/677_a_conditional_dependency_replaces_the_unconditional_one.sh +++ /dev/null @@ -1,104 +0,0 @@ -#!/usr/bin/env bash -# requires: elf -# 677 -- a matching `[target..dependencies]` declaration REPLACES the -# unconditional declaration of the same identity on the rows the selector -# matches (#634, A1). Before, the merge kept the unconditional entry, so a -# conditional `linkage = "shared"` was dropped on its own row without a word. -# -# Legs: -# A. The conditional table matches this row: `libfw.so` is built and the -# program NEEDs it, and the resolution record names the conditional table. -# B. The conditional table does not match: the unconditional declaration -# stands (a static link), and the record names `[dependencies]`. -# C. The modifier-only spelling `demo.fw = { linkage = "shared" }` names no -# source. It still fails, and the warning names the conditional table and -# restates the dependency with the source the unconditional table wrote. -# D. A misspelled sub-table of `[target.]` is reported instead of -# doing nothing. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT - -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } - -# table_of -> the tables its requests came from -table_of() { - python3 - "$1" "$2" <<'PY' -import json, sys -doc = json.load(open(sys.argv[1])) -for p in doc["graph"]["packages"]: - if p["package"]["canonical"] == sys.argv[2]: - print(" ".join(r["table"] for r in p["requested_by"])) -PY -} - -mkdir -p "$TMP/fw/src" -cat > "$TMP/fw/mcpp.toml" <<'TOML' -[package] -namespace = "demo" -name = "fw" -version = "0.1.0" -[targets.fw] -kind = "lib" -TOML -printf 'export module fw;\nexport int fw_anchor() { return 41; }\n' > "$TMP/fw/src/fw.cppm" - -make_app() { # make_app - mkdir -p "$TMP/$1/src" - printf '[package]\nname = "%s"\nversion = "0.1.0"\n%s' "$1" "$2" > "$TMP/$1/mcpp.toml" - printf 'import fw;\nint main() { return fw_anchor() == 41 ? 0 : 1; }\n' > "$TMP/$1/src/main.cpp" -} - -# ── A ────────────────────────────────────────────────────────────────────── -make_app matching '[dependencies] -demo.fw = { path = "../fw" } -[target.'"'"'cfg(os = "linux")'"'"'.dependencies] -demo.fw = { path = "../fw", linkage = "shared" } -' -( cd "$TMP/matching" && "$MCPP" build > build.log 2>&1 ) || fail "A: build failed" "$TMP/matching/build.log" -so=$(find "$TMP/matching/target" -name libfw.so | head -1) -[ -n "$so" ] || fail "A: the conditional linkage did not produce libfw.so" "$TMP/matching/build.log" -exe=$(find "$TMP/matching/target" -type f -name matching -perm -u+x | head -1) -readelf -d "$exe" | grep -q 'NEEDED.*libfw.so' || fail "A: the program does not NEED libfw.so" -json=$(find "$TMP/matching/target" -name resolution.json | head -1) -t=$(table_of "$json" "demo.fw@0.1.0") -[ "$t" = "[target.'cfg(os = \"linux\")'.dependencies]" ] || fail "A: graph table is '$t'" "$json" - -# ── B ────────────────────────────────────────────────────────────────────── -make_app other '[dependencies] -demo.fw = { path = "../fw" } -[target.'"'"'cfg(os = "windows")'"'"'.dependencies] -demo.fw = { path = "../fw", linkage = "shared" } -' -( cd "$TMP/other" && "$MCPP" build > build.log 2>&1 ) || fail "B: build failed" "$TMP/other/build.log" -[ -z "$(find "$TMP/other/target" -name libfw.so | head -1)" ] || fail "B: a non-matching selector produced libfw.so" -json=$(find "$TMP/other/target" -name resolution.json | head -1) -t=$(table_of "$json" "demo.fw@0.1.0") -[ "$t" = "[dependencies]" ] || fail "B: graph table is '$t'" "$json" - -# ── C ────────────────────────────────────────────────────────────────────── -make_app modifier '[dependencies] -demo.fw = { path = "../fw" } -[target.'"'"'cfg(os = "linux")'"'"'.dependencies] -demo.fw = { linkage = "shared" } -' -if ( cd "$TMP/modifier" && "$MCPP" build > build.log 2>&1 ); then - fail "C: a table that names no source built" "$TMP/modifier/build.log" -fi -grep -qF "[target.'cfg(os = \"linux\")'.dependencies] demo.fw.linkage = \"shared\"" "$TMP/modifier/build.log" \ - || fail "C: the warning does not name the conditional table" "$TMP/modifier/build.log" -grep -qF 'demo.fw = { path = "../fw", linkage = "shared" }' "$TMP/modifier/build.log" \ - || fail "C: the warning does not restate the dependency with its source" "$TMP/modifier/build.log" - -# ── D ────────────────────────────────────────────────────────────────────── -make_app typo '[dependencies] -demo.fw = { path = "../fw" } -[target.'"'"'cfg(os = "linux")'"'"'.dependecies] -demo.fw = { path = "../fw", linkage = "shared" } -' -( cd "$TMP/typo" && "$MCPP" build > build.log 2>&1 ) || fail "D: build failed" "$TMP/typo/build.log" -grep -qF "[target.'cfg(os = \"linux\")'.dependecies] is not a section mcpp reads" "$TMP/typo/build.log" \ - || fail "D: the misspelled section was not reported" "$TMP/typo/build.log" - -echo "OK" diff --git a/tests/e2e/67_features_strict.sh b/tests/e2e/67_features_strict.sh deleted file mode 100755 index 6476c38fd..000000000 --- a/tests/e2e/67_features_strict.sh +++ /dev/null @@ -1,131 +0,0 @@ -#!/usr/bin/env bash -# 67_features_strict.sh — feature activation, backend= sugar, and the -# --strict schema gate (unknown feature / unknown platform). -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -# Library dep declaring backend-* features (the backend= sugar target). -mkdir -p widget/src -cat > widget/mcpp.toml <<'EOF' -[package] -name = "widget" -version = "0.1.0" - -[targets.widget] -kind = "lib" - -[features] -default = [] -backend-a = [] -backend-b = [] -EOF -cat > widget/src/widget.cppm <<'EOF' -export module widget; -export int widget_backend() { -#if defined(MCPP_FEATURE_BACKEND_A) - return 1; -#elif defined(MCPP_FEATURE_BACKEND_B) - return 2; -#else - return 0; -#endif -} -EOF - -mkdir -p app/src -cat > app/mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" -platforms = ["linux", "macos", "windows"] - -[features] -default = ["base"] -base = [] -extra = [] - -[dependencies] -widget = { path = "../widget", backend = "a" } -EOF -cat > app/src/main.cpp <<'EOF' -import std; -import widget; -int main() { -#ifdef MCPP_FEATURE_BASE - std::println("base"); -#endif -#ifdef MCPP_FEATURE_EXTRA - std::println("extra"); -#endif - std::println("backend={}", widget_backend()); - return 0; -} -EOF - -cd app - -# 1. backend= sugar selects widget's backend-a feature. -"$MCPP" build > b1.log 2>&1 || { cat b1.log; echo "build failed"; exit 1; } -bin=$(find target \( -name app -o -name app.exe \) -type f | head -1) -[[ -n "$bin" ]] || { echo "no app binary"; exit 1; } -out=$("$bin" | tail -2) -grep -q "backend=1" <<< "$out" || { echo "backend=a not activated: $out"; exit 1; } -grep -q "base" <<< "$out" || true # default features active - -# 2. --features extra activates a declared feature. -rm -rf target -"$MCPP" build --features extra > b2.log 2>&1 || { cat b2.log; echo "features build failed"; exit 1; } -bin=$(find target \( -name app -o -name app.exe \) -type f | head -1) -[[ -n "$bin" ]] || { echo "no app binary"; exit 1; } -"$bin" | grep -q "extra" || { echo "--features extra not active"; exit 1; } - -# 3. Unknown feature → warning by default, error under --strict. -rm -rf target -"$MCPP" build --features nosuch > b3.log 2>&1 || { cat b3.log; echo "warn path must not fail"; exit 1; } -grep -q "does not declare" b3.log || { cat b3.log; echo "missing unknown-feature warning"; exit 1; } -if "$MCPP" build --features nosuch --strict > b4.log 2>&1; then - cat b4.log; echo "--strict must fail on unknown feature"; exit 1 -fi -grep -q "does not declare" b4.log || { cat b4.log; echo "missing strict error text"; exit 1; } - -# 4. Unknown backend on a dep that declares backend-* features → warning. -sed 's/backend = "a"/backend = "zzz"/' mcpp.toml > mcpp.toml.tmp && mv mcpp.toml.tmp mcpp.toml -rm -rf target -"$MCPP" build > b5.log 2>&1 || { cat b5.log; echo "unknown-backend warn path must not fail"; exit 1; } -grep -q "does not declare requested feature 'backend-zzz'" b5.log \ - || { cat b5.log; echo "missing unknown-backend warning"; exit 1; } -sed 's/backend = "zzz"/backend = "a"/' mcpp.toml > mcpp.toml.tmp && mv mcpp.toml.tmp mcpp.toml - -# 5. Unknown platform → warning by default, error under --strict. -sed 's/platforms = \["linux", "macos", "windows"\]/platforms = ["linux", "amiga"]/' mcpp.toml > mcpp.toml.tmp && mv mcpp.toml.tmp mcpp.toml -rm -rf target -"$MCPP" build > b6.log 2>&1 || { cat b6.log; echo "platform warn path must not fail"; exit 1; } -grep -q "unknown platform 'amiga'" b6.log || { cat b6.log; echo "missing platform warning"; exit 1; } -if "$MCPP" build --strict > b7.log 2>&1; then - cat b7.log; echo "--strict must fail on unknown platform"; exit 1 -fi -grep -q "expected: linux | macos | windows | ios | android | emscripten" b7.log \ - || { cat b7.log; echo "the refusal must list the six platform names"; exit 1; } - -# 6. The vocabulary names every row the engine has (#622 A7): the three rows -# verified in 2026.9.11.x are claimable, and the word is the triple's own -# (`emscripten`), not `web`. Asserted on the warning's absence rather than -# on `--strict` succeeding: `--strict` also fails on degradations unrelated -# to this key (a toolchain that emits no GNU depfile, measured on the -# Windows shard), so a strict success would read a platform fact through a -# toolchain fact. -sed 's/platforms = \["linux", "amiga"\]/platforms = ["linux", "ios", "android", "emscripten"]/' mcpp.toml > mcpp.toml.tmp && mv mcpp.toml.tmp mcpp.toml -rm -rf target -"$MCPP" build > b8.log 2>&1 || { cat b8.log; echo "a manifest naming the six platforms must build"; exit 1; } -if grep -q "unknown platform" b8.log; then cat b8.log; echo "a known platform name was reported as unknown"; exit 1; fi -sed 's/platforms = \["linux", "ios", "android", "emscripten"\]/platforms = ["web"]/' mcpp.toml > mcpp.toml.tmp && mv mcpp.toml.tmp mcpp.toml -rm -rf target -if "$MCPP" build --strict > b9.log 2>&1; then - cat b9.log; echo "--strict must fail on 'web': the word is emscripten"; exit 1 -fi -grep -q "unknown platform 'web'" b9.log || { cat b9.log; echo "missing platform refusal for 'web'"; exit 1; } - -echo "OK" diff --git a/tests/e2e/710_a_forward_reaches_every_dependency_table.sh b/tests/e2e/710_a_forward_reaches_every_dependency_table.sh deleted file mode 100755 index 2d391a70f..000000000 --- a/tests/e2e/710_a_forward_reaches_every_dependency_table.sh +++ /dev/null @@ -1,167 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 710 -- a feature forward is validated against every dependency table of its -# manifest (#647 E4.1). -# -# `injectForwards` applies a `[features]` forward to `[build-dependencies]` -# edges, while the validator used to look the key up in `[dependencies]` and -# `[dev-dependencies]` only. A two-level forward along build-dependency edges -# therefore opened the leaf's feature (its tool was built) and printed -# "not declared" for each level in the same run, and `--strict` refused the -# build whose forward had worked. A key declared only for another row is also -# declared: on this row the forward reaches no edge. -# -# Readings: the two-level forward builds the tool under `--strict` with no -# forward warning; a forward to a dependency declared for another row builds -# under `--strict`; a forward to a key no table declares still warns, and -# `--strict` refuses it. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -export MCPP_HOME="$TMP/mcpphome" -mkdir -p "$MCPP_HOME" -if [ -d "$HOME/.mcpp/registry" ]; then - ln -s "$HOME/.mcpp/registry" "$MCPP_HOME/registry" -fi - -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } - -mkdir -p kt/src leaf/src rules/src fw/src app/src win/src -cat > kt/mcpp.toml <<'EOF' -[package] -name = "kt" -namespace = "spike" -version = "0.1.0" - -[targets.kt] -kind = "bin" -main = "src/main.cpp" -EOF -echo 'int main() { return 0; }' > kt/src/main.cpp - -cat > leaf/mcpp.toml <<'EOF' -[package] -name = "leaf" -namespace = "spike" -version = "0.1.0" - -[targets.leaf] -kind = "lib" - -[build] -sources = ["src/*.cpp"] - -[features] -kt = [] - -[feature-deps.kt] -spike.kt = { path = "../kt", tools = ["kt"] } -EOF -echo 'int leaf_answer() { return 1; }' > leaf/src/leaf.cpp - -cat > rules/mcpp.toml <<'EOF' -[package] -name = "rules" -namespace = "spike" -version = "0.1.0" - -[targets.rules] -kind = "lib" - -[build] -sources = ["src/*.cpp"] - -[build-dependencies] -spike.leaf = { path = "../leaf" } - -[features] -kotlin = ["spike.leaf/kt"] -EOF -echo 'int rules_answer() { return 1; }' > rules/src/rules.cpp - -cat > win/mcpp.toml <<'EOF' -[package] -name = "win" -namespace = "spike" -version = "0.1.0" - -[targets.win] -kind = "lib" - -[build] -sources = ["src/*.cpp"] - -[features] -x = [] -EOF -echo 'int win_answer() { return 1; }' > win/src/win.cpp - -cat > fw/mcpp.toml <<'EOF' -[package] -name = "fw" -namespace = "spike" -version = "0.1.0" - -[targets.fw] -kind = "lib" - -[build] -sources = ["src/*.cpp"] - -[build-dependencies] -spike.rules = { path = "../rules" } - -[target.'cfg(os = "windows")'.dependencies] -spike.win = { path = "../win" } - -[features] -kotlin = ["spike.rules/kotlin", "spike.win/x"] -EOF -echo 'int fw_answer() { return 42; }' > fw/src/fw.cpp - -cat > app/mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" - -[targets.app] -kind = "bin" -main = "src/main.cpp" - -[dependencies] -spike.fw = { path = "../fw", features = ["kotlin"] } -EOF -cat > app/src/main.cpp <<'EOF' -int fw_answer(); -int main() { return fw_answer() == 42 ? 0 : 1; } -EOF - -# ── 1. two levels along [build-dependencies], strict, no forward warning ── -cd "$TMP/app" -"$MCPP" build --strict > b1.log 2>&1 || fail "the two-level build-dependency forward was refused under --strict" b1.log -grep -q "forwards to dependency" b1.log \ - && fail "a forward along a build-dependency edge was reported as undeclared" b1.log -grep -q "Building host tool kt" b1.log \ - || fail "the forwarded feature did not reach the leaf (its tool was not built)" b1.log - -# ── 2. a forward to a dependency declared only for another row ─────────── -# `spike.win` is declared for Windows only (this script runs where gcc is the -# host compiler, which is never a Windows row). The forward names it, reaches -# no edge here, and must be accepted: the manifest is portable, not wrong. -grep -q "spike.win" b1.log && fail "a row-declared dependency's forward was reported" b1.log - -# ── 3. a key no table declares is still named, and --strict refuses it ─── -sed -i.bak 's|kotlin = \["spike.rules/kotlin", "spike.win/x"\]|kotlin = ["spike.rules/kotlin", "spike.nowhere/x"]|' "$TMP/fw/mcpp.toml" -rm -rf target -"$MCPP" build > b3.log 2>&1 || fail "an undeclared forward failed a non-strict build" b3.log -grep -q "forwards to dependency 'spike.nowhere'" b3.log \ - || fail "a forward to an undeclared key was not named" b3.log -if "$MCPP" build --strict > b4.log 2>&1; then - fail "--strict accepted a forward to a key no table declares" b4.log -fi -grep -q "spike.nowhere" b4.log || fail "the --strict refusal does not name the key" b4.log - -echo "PASS: 710 a forward is validated against every dependency table" diff --git a/tests/e2e/711_a_feature_deps_restatement_names_one_source.sh b/tests/e2e/711_a_feature_deps_restatement_names_one_source.sh deleted file mode 100755 index fb13b4b87..000000000 --- a/tests/e2e/711_a_feature_deps_restatement_names_one_source.sh +++ /dev/null @@ -1,164 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 711 -- a `[feature-deps.]` entry that adds `tools` to a dependency declared -# unconditionally restates that dependency's source, and a restatement naming -# another source is refused (#647 E4.2). A build program addresses the tool of a -# package written `namespace = "spike"`, `name = "installer"` by the qualified -# name as well as the bare one (#647 E4.3). -# -# The grammar requires the source in a dependency table, so a source-less entry -# is refused by the parser, whose message prescribes restating it. The merge -# took only the additive fields of a restatement: one naming another path was -# ignored without a word, under `--strict` too. And the tool variable was -# published under `package.name` alone, so `dep_bin("spike.installer", ...)` -# read nothing while `dep_dir("spike.installer")` answered. -# -# Readings: the source-less entry is refused and names the remedy; the -# restatement with the same source builds the tool only under the feature, and -# the build program sees both spellings; a restatement naming another path is -# refused under `--strict` naming both sources; and two spellings of one version -# constraint, differing only in whitespace, are one source, while a genuinely -# different constraint is still refused. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -export MCPP_HOME="$TMP/mcpphome" -mkdir -p "$MCPP_HOME" -if [ -d "$HOME/.mcpp/registry" ]; then - ln -s "$HOME/.mcpp/registry" "$MCPP_HOME/registry" -fi - -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } - -mkdir -p installer/src other/src app/src -for d in installer other; do -cat > $d/mcpp.toml <<'EOF' -[package] -name = "installer" -namespace = "spike" -version = "0.1.0" - -[targets.installer] -kind = "bin" -main = "src/main.cpp" -EOF -cat > $d/src/main.cpp <<'EOF' -#include -int main() { std::puts("installer ran"); return 0; } -EOF -done - -cat > app/mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" - -[targets.app] -kind = "bin" -main = "src/main.cpp" - -[dependencies] -spike.installer = { path = "../installer" } - -[features] -installer = [] - -[feature-deps.installer] -spike.installer = { tools = ["installer"] } -EOF -echo 'int main() { return 0; }' > app/src/main.cpp -cat > app/build.mcpp <<'EOF' -import std; -import mcpp; -int main() { - const char* a = mcpp::dep_bin("installer", "installer"); - const char* b = mcpp::dep_bin("spike.installer", "installer"); - mcpp::warning((std::string("DEPBIN short=[") + (a ? a : "") + "] qualified=[" - + (b ? b : "") + "]").c_str()); - return 0; -} -EOF - -cd "$TMP/app" - -# ── 1. a source-less entry is refused, and the refusal names the remedy ── -if "$MCPP" build --features installer > b1.log 2>&1; then - fail "a source-less [feature-deps] entry was accepted" b1.log -fi -grep -q "source" b1.log || fail "the refusal does not say to restate the source" b1.log - -# ── 2. the same source restated: the tool, both spellings, only under the feature -sed -i.bak 's|spike.installer = { tools = \["installer"\] }|spike.installer = { path = "../installer", tools = ["installer"] }|' mcpp.toml -"$MCPP" build --strict --features installer > b2.log 2>&1 \ - || fail "the restated feature-deps entry was refused under --strict" b2.log -grep -q 'DEPBIN short=\[[^]]' b2.log || fail "dep_bin(\"installer\") read nothing" b2.log -grep -q 'qualified=\[[^]]' b2.log \ - || fail "dep_bin(\"spike.installer\") read nothing for a namespace + name package" b2.log -rm -rf target -"$MCPP" build --strict > b3.log 2>&1 || fail "the build without the feature failed" b3.log -grep -q 'DEPBIN short=\[\] qualified=\[\]' b3.log \ - || fail "the tool was provided without its feature" b3.log - -# ── 3. a restatement naming another source is refused, naming both ─────── -sed -i.bak 's|spike.installer = { path = "../installer", tools|spike.installer = { path = "../other", tools|' mcpp.toml -rm -rf target -if "$MCPP" build --strict --features installer > b4.log 2>&1; then - fail "a restatement naming another path was accepted" b4.log -fi -grep -q '../other' b4.log && grep -q '../installer' b4.log \ - || fail "the refusal does not name both sources" b4.log - -# ── 4. two spellings of one constraint are one source ──────────────────── -# The judgement is on what the declarations mean. `">= 9.9.9"` and `">=9.9.9"` -# are one constraint, and a manifest spelling them differently built on -# 2026.9.15.2, so refusing it would be an upgrade cliff. The package is never -# resolved here: the merge runs before resolution, so the reading is whether the -# refusal appears at all, and the leg below with a genuinely different -# constraint is what shows the gate still closes. -mkdir -p "$TMP/spell/src" -echo 'int main() { return 0; }' > "$TMP/spell/src/main.cpp" -write_spell() { # write_spell -cat > "$TMP/spell/mcpp.toml" <= 9.9.9" -MCPP_OFFLINE=1 "$MCPP" build --features extra > s1.log 2>&1 || true -grep -q 'restates the dependency' s1.log \ - && fail "a restatement differing from the declaration only in whitespace was refused" s1.log - -write_spell ">=9.9.8" -rm -rf target -if MCPP_OFFLINE=1 "$MCPP" build --features extra > s2.log 2>&1; then - fail "a restatement naming another constraint was accepted" s2.log -fi -grep -q 'restates the dependency' s2.log \ - || fail "a restatement naming another constraint was not refused" s2.log - -echo "PASS: 711 a feature-deps restatement names one source; dep_bin answers both spellings" diff --git a/tests/e2e/714_features_names_a_dependency_feature.sh b/tests/e2e/714_features_names_a_dependency_feature.sh deleted file mode 100755 index b9bf1e750..000000000 --- a/tests/e2e/714_features_names_a_dependency_feature.sh +++ /dev/null @@ -1,121 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 714 -- `--features /` opens a feature of a dependency, as -# the same token does inside `[features]` (#649 E8). -# -# The command line accepted root features only. With no `[features]` table the -# token became the macro `-DMCPP_FEATURE_SPIKE_FW_INSTALLER` and opened nothing; -# with the table it was reported as an undeclared feature of the root. A plain -# name without a `[features]` table keeps its documented meaning, a macro. -# -# Readings: without a `[features]` table the dependency's tool is built and no -# macro is made of the token; with the table the build is clean under -# `--strict`; a token naming no dependency is refused under `--strict`; and -# `mcpp why deps --features` reports the graph of the feature build. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -export MCPP_HOME="$TMP/mcpphome" -mkdir -p "$MCPP_HOME" -if [ -d "$HOME/.mcpp/registry" ]; then - ln -s "$HOME/.mcpp/registry" "$MCPP_HOME/registry" -fi - -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } - -mkdir -p fw/src fw/tool/src app/src -cat > fw/mcpp.toml <<'EOF' -[package] -name = "fw" -namespace = "spike" -version = "0.1.0" - -[targets.fw] -kind = "lib" - -[build] -sources = ["src/*.cpp"] - -[features] -installer = [] - -[feature-deps.installer] -spike.fw-installer = { path = "tool", tools = ["fw-installer"], reexport = true } -EOF -echo 'int fw_answer() { return 42; }' > fw/src/fw.cpp -cat > fw/tool/mcpp.toml <<'EOF' -[package] -name = "fw-installer" -namespace = "spike" -version = "0.1.0" - -[targets.fw-installer] -kind = "bin" -main = "src/main.cpp" -EOF -echo 'int main() { return 0; }' > fw/tool/src/main.cpp - -cat > app/mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" - -[targets.app] -kind = "bin" -main = "src/main.cpp" - -[dependencies] -spike.fw = { path = "../fw" } -EOF -cat > app/src/main.cpp <<'EOF' -int fw_answer(); -int main() { return fw_answer() == 42 ? 0 : 1; } -EOF -cat > app/build.mcpp <<'EOF' -import std; -import mcpp; -int main() { - const char* a = mcpp::dep_bin("spike.fw-installer", "fw-installer"); - mcpp::warning((std::string("DEPBIN tool=[") + (a ? a : "") + "]").c_str()); - return 0; -} -EOF -cd "$TMP/app" - -# ── 1. no [features] table: the dependency's feature opens, no macro ───── -"$MCPP" build --features spike.fw/installer > b1.log 2>&1 || fail "the dependency feature build failed" b1.log -grep -q 'DEPBIN tool=\[[^]]' b1.log || fail "--features spike.fw/installer did not open the feature" b1.log -ninja_file=$(find target -name build.ninja -not -path '*/.build-mcpp/*' | head -1) -grep -q 'MCPP_FEATURE_SPIKE_FW_INSTALLER' "$ninja_file" \ - && fail "the dependency token became a macro of the root" "$ninja_file" - -# A plain undeclared name without a [features] table is documented as a macro. -rm -rf target -"$MCPP" build --strict --features nothing-here > b2.log 2>&1 \ - || fail "a plain name without a [features] table was refused" b2.log - -# ── 2. a token naming no dependency is refused under --strict ─────────── -if "$MCPP" build --strict --features nope/x > b3.log 2>&1; then - fail "--features nope/x was accepted under --strict" b3.log -fi -grep -q "nope" b3.log || fail "the refusal does not name the token" b3.log - -# ── 3. with a [features] table the token is not reported as undeclared ── -cat >> mcpp.toml <<'EOF' - -[features] -windows-installer = [] -EOF -rm -rf target -"$MCPP" build --strict --features spike.fw/installer > b4.log 2>&1 \ - || fail "the dependency token was refused beside a [features] table" b4.log -grep -q 'DEPBIN tool=\[[^]]' b4.log || fail "the feature did not open beside a [features] table" b4.log - -# ── 4. `why deps` reads the graph of the feature build ────────────────── -"$MCPP" why deps --features spike.fw/installer > w1.log 2>&1 || fail "why deps --features failed" w1.log -grep -q 'spike.fw-installer' w1.log || fail "why deps does not show the feature's dependency" w1.log - -echo "PASS: 714 --features opens a dependency's feature" diff --git a/tests/e2e/71_required_features_gate.sh b/tests/e2e/71_required_features_gate.sh deleted file mode 100755 index 78b554553..000000000 --- a/tests/e2e/71_required_features_gate.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# Per-target required_features gate (issue #131): a target is emitted only when -# all its required features are active. `--features X` builds only the matching -# target; the gated-out target's entry is never compiled (no main() clash). -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" -mkdir -p proj/src proj/bin -cd proj - -# Shared code (globbed). Entry mains live outside src/ so a gated-out target's -# main is not globbed into the shared pool. -cat > src/common.cpp <<'EOF' -extern "C" int shared_answer(void) { return 0; } -EOF -cat > bin/server.cpp <<'EOF' -extern "C" int shared_answer(void); -int main() { return shared_answer(); } -EOF -cat > bin/client.cpp <<'EOF' -extern "C" int shared_answer(void); -int main() { return shared_answer(); } -EOF - -cat > mcpp.toml <<'EOF' -[package] -name = "proj" -version = "0.1.0" - -[build] -sources = ["src/**/*.cpp"] - -[features] -server = [] -client = [] - -[targets.server] -kind = "bin" -main = "bin/server.cpp" -required_features = ["server"] - -[targets.client] -kind = "bin" -main = "bin/client.cpp" -required_features = ["client"] -EOF - -binpath() { find target -path '*/bin/'"$1" 2>/dev/null | head -1; } - -# --features server → only the server target is emitted. -"$MCPP" build --features server > s.log 2>&1 || { cat s.log; echo "server build failed"; exit 1; } -[[ -n "$(binpath server)" ]] || { echo "server binary not built under --features server"; exit 1; } -[[ -z "$(binpath client)" ]] || { echo "client binary should be gated out under --features server"; exit 1; } - -# --features client → only the client target is emitted. -"$MCPP" build --features client > c.log 2>&1 || { cat c.log; echo "client build failed"; exit 1; } -[[ -n "$(binpath client)" ]] || { echo "client binary not built under --features client"; exit 1; } - -echo "OK" diff --git a/tests/e2e/739_a_private_feature_dep_does_not_reach_the_consumer.sh b/tests/e2e/739_a_private_feature_dep_does_not_reach_the_consumer.sh deleted file mode 100755 index 3d692dc1c..000000000 --- a/tests/e2e/739_a_private_feature_dep_does_not_reach_the_consumer.sh +++ /dev/null @@ -1,126 +0,0 @@ -#!/usr/bin/env bash -# 739 -- a platform SDK pulled under [feature-deps.] with -# `visibility = "private"` reaches only the declaring package's own -# translation units, never a consumer's (mcpp#662, M5). -# -# WHY THIS TEST EXISTS. #662's plan required VERIFYING, not assuming, that a -# platform-bound package can keep a platform dependency to itself before -# documenting the pattern (docs/06, docs/24). Reading `computeUsageRequirements` -# in prepare.cppm shows a `DependencyVisibility` axis already exists and that -# `Private` folds a dependency's usage into `consumer.privateBuild` only, never -# into `consumer.publicUsage` — so it should already hold for ANY dependency -# table, `[feature-deps.]` included, since both route through the same -# per-entry parser. This test is the measurement that confirms it, and it -# fails on any future change that lets a private edge's headers leak. -# -# THE SHAPE, deliberately proving both directions on the SAME two packages so -# a change to the harness itself cannot make this pass vacuously: -# -# sdk (a path package) -- a header defining SDK_PRESENT -# lib_platform (a path pkg) -- [feature-deps.plat] depends on `sdk` -# app (this test's root) -- depends on lib_platform with features=["plat"] -# -# A. `sdk` declared PRIVATE -> lib_platform's own TU sees it (must build); -# app's TU including "sdk.h" must NOT build. -# B. `sdk` declared PUBLIC -> app's TU including "sdk.h" DOES build -- -# establishing that the harness would have -# caught A being wrong, not merely different. -set -e - -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -fail() { echo "FAIL: $1"; [ -n "${2:-}" ] && cat "$2"; exit 1; } - -mkdir -p "$TMP/sdk/src" "$TMP/sdk/include" -cat > "$TMP/sdk/mcpp.toml" <<'EOF' -[package] -name = "sdk" -version = "0.1.0" - -[targets.sdk] -kind = "lib" -EOF -cat > "$TMP/sdk/include/sdk.h" <<'EOF' -#pragma once -#define SDK_PRESENT 1 -inline int sdk_value(void) { return 42; } -EOF -cat > "$TMP/sdk/src/empty.cpp" <<'EOF' -// no compiled sources of its own beyond the header -EOF - -mkdir -p "$TMP/lib_platform/src" -write_lib_platform_manifest() { # $1 = visibility - cat > "$TMP/lib_platform/mcpp.toml" < "$TMP/lib_platform/src/lib_platform.cpp" <<'EOF' -#include "sdk.h" -#ifndef SDK_PRESENT -#error "lib_platform's own translation unit must see sdk's headers" -#endif -int lib_platform_uses_sdk(void) { return sdk_value(); } -EOF - -mkdir -p "$TMP/app/src" -cat > "$TMP/app/mcpp.toml" <<'EOF' -[package] -name = "app" -version = "0.1.0" - -[dependencies] -lib_platform = { path = "../lib_platform", features = ["plat"] } -EOF -cat > "$TMP/app/src/main.cpp" <<'EOF' -#include "sdk.h" -int main() { return 0; } -EOF - -# A. private — lib_platform's own build must still succeed (it needs sdk.h), -# and app's must fail to find sdk.h at all: not merely "SDK_PRESENT -# undefined" (which private defines/cflags would also produce) but the -# header itself absent from app's search path. -write_lib_platform_manifest private -cd "$TMP/app" -if out="$("$MCPP" build --cache off 2>&1)"; then - printf '%s\n' "$out" > "$TMP/a-build.log" - fail "A: app built despite depending (via a private feature-dep) on a package whose headers it must not see" "$TMP/a-build.log" -fi -printf '%s\n' "$out" > "$TMP/a-build.log" -case "$out" in - *"sdk.h"*"No such file"*|*"sdk.h"*"not found"*|*"sdk.h"*"cannot open"*) - echo " ok A: app cannot find sdk.h (private feature-dep did not cross the boundary)" ;; - *) - fail "A: app's build failed, but not on a missing sdk.h — cannot tell privacy from an unrelated break" "$TMP/a-build.log" ;; -esac - -# lib_platform's OWN build (its private dependency reaches its own TUs). -# `--features plat`: `plat` is not a default feature, and building -# lib_platform AS THE ROOT package activates only `default` unless told -# otherwise — the same rule app's own dependency edge states explicitly. -cd "$TMP/lib_platform" -"$MCPP" build --features plat --cache off > "$TMP/lp-build.log" 2>&1 \ - || fail "A: lib_platform (the declaring package) could not build against its own private feature-dep" "$TMP/lp-build.log" -echo " ok A: lib_platform's own translation unit does see sdk's headers" - -# B. public — the same app now builds, proving the assertion above would -# have failed had the engine actually broadcast a private edge. -write_lib_platform_manifest public -cd "$TMP/app" -"$MCPP" build --cache off > "$TMP/b-build.log" 2>&1 \ - || fail "B: app failed to build once sdk is a PUBLIC feature-dep of lib_platform — the harness cannot distinguish private from broken" "$TMP/b-build.log" -echo " ok B: with visibility=public the same app DOES see sdk.h (the harness is not vacuous)" - -echo "PASS: 739 a private feature-dep does not reach the consumer" diff --git a/tests/e2e/80_feature_defines.sh b/tests/e2e/80_feature_defines.sh deleted file mode 100755 index b78964e42..000000000 --- a/tests/e2e/80_feature_defines.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env bash -# 80_feature_defines.sh — a [features] entry may carry `defines` (package-owned -# preprocessor macros). When the feature is active, each define lands on the -# package's compile flags (visible in compile_commands.json); when inactive, it -# does not. This is Feature System v2 Stage 1 — see -# .agents/docs/2026-06-29-feature-capability-model-design.md. -# -# No `requires:` capability → runs on all three CI platforms. -set -e -# The project-root copy of the compile database is opt-in since -# 2026.10.26.1 ([build] compile_commands); this test reads it. -export MCPP_COMPILE_COMMANDS=compile_commands.json - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -mkdir -p app/src -cat > app/mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" - -[features] -default = [] -accel = { defines = ["APP_ACCEL=1"] } -EOF -cat > app/src/main.cpp <<'EOF' -int main() { -#ifdef APP_ACCEL - return 0; -#else - return 1; -#endif -} -EOF - -cd app -cdb=compile_commands.json - -# 1. Feature inactive → the define must NOT appear in the compile database. -"$MCPP" build > b1.log 2>&1 || { cat b1.log; echo "baseline build failed"; exit 1; } -if grep -q 'APP_ACCEL' "$cdb"; then - echo "FAIL: APP_ACCEL present in $cdb without the feature active"; cat "$cdb"; exit 1 -fi - -# 2. Feature active → the define MUST appear in the compile database. -rm -rf target "$cdb" -"$MCPP" build --features accel > b2.log 2>&1 || { cat b2.log; echo "feature build failed"; exit 1; } -grep -q 'APP_ACCEL' "$cdb" || { echo "FAIL: APP_ACCEL missing from $cdb with --features accel"; cat "$cdb"; exit 1; } - -# 3. The automatic MCPP_FEATURE_ define still coexists with custom defines. -grep -q 'MCPP_FEATURE_ACCEL' "$cdb" || { echo "FAIL: MCPP_FEATURE_ACCEL missing from $cdb"; cat "$cdb"; exit 1; } - -echo "OK" diff --git a/tests/e2e/813_conditional_dialect_cxxflags_root_only.sh b/tests/e2e/813_conditional_dialect_cxxflags_root_only.sh deleted file mode 100755 index ea9c2dc22..000000000 --- a/tests/e2e/813_conditional_dialect_cxxflags_root_only.sh +++ /dev/null @@ -1,227 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 813_conditional_dialect_cxxflags_root_only.sh -- #717, design 2026-09-27 §6: -# `[target..build] dialect_cxxflags` is a graph-wide switch under a -# target condition. Before the fix (measured on 2026.9.27.1), the key inside -# `[target.linux.build]` was reported as "unsupported key 'dialect_cxxflags' -# (ignored)" and reached no command at all. -# -# Four properties, each with its own scenario below: -# -# 1. a matching row reaches the std BMI prebuild, the module scan and every -# translation unit, in the order [build] (root, unconditional) then the -# matching [target..build] (§6.2: "entries are appended"); -# 2. a selector that does not match the resolved target contributes nothing; -# 3. switching a manifest between a matching and a non-matching row rebuilds -# the std BMI, and switching back reuses the earlier std-module cache -# entry rather than rebuilding it a second time (A, then B, then A); -# 4. a DEPENDENCY's own `dialect_cxxflags` (conditional or not) reaches no -# command, and toggling it does not change the OUTPUT DIRECTORY -# fingerprint of the graph it belongs to (the graph-wide keys must be -# excluded from a package's own per-package fingerprint contribution, -# design §6.2 finding 7). -set -e -# The project-root copy of the compile database is opt-in since -# 2026.10.26.1 ([build] compile_commands); this test reads it. -export MCPP_COMPILE_COMMANDS=compile_commands.json - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT - -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } - -export MCPP_HOME="$TMP/mcpp-home" -source "$(dirname "$0")/_inherit_toolchain.sh" - -std_module_json_for() { # $1 = a substring the std_flag field must contain - grep -rl -- "$1" "$MCPP_HOME/build-cache/v1/std" 2>/dev/null | head -1 -} - -echo "== 813/1: a matching row reaches the std BMI, the scan and every TU, after the root's own [build] ==" - -mkdir -p "$TMP/one/src" -cd "$TMP/one" -cat > mcpp.toml <<'TOML' -[package] -name = "app717a" -version = "0.1.0" -standard = "c++23" - -[build] -dialect_cxxflags = ["-DBASE717"] - -[targets.app717a] -kind = "bin" -main = "src/main.cpp" - -[target.linux.build] -dialect_cxxflags = ["-DX717A"] -TOML -printf 'import std;\nint main() { std::println("ok"); return 0; }\n' > src/main.cpp - -"$MCPP" build --toolchain gcc@16.1.0 > build1.log 2>&1 || fail "the build with a matching [target.linux.build] row failed" build1.log -ninja1=$(find target -name build.ninja -not -path '*/.build-mcpp/*' | head -1) -[ -n "$ninja1" ] || fail "no build.ninja produced" build1.log - -cxxflags_line=$(grep -E '^cxxflags' "$ninja1" | head -1) -echo "$cxxflags_line" | grep -qF -- '-DX717A' \ - || fail "the row's flag is not in build.ninja's global cxxflags (reaches neither scan nor TUs)" "$ninja1" -echo "$cxxflags_line" | grep -qF -- '-DBASE717' \ - || fail "the root's own unconditional flag is missing from cxxflags" "$ninja1" -# Order: the root's own [build] precedes the matching conditional row. -base_pos=$(echo "$cxxflags_line" | grep -bo -- '-DBASE717' | head -1 | cut -d: -f1) -cond_pos=$(echo "$cxxflags_line" | grep -bo -- '-DX717A' | head -1 | cut -d: -f1) -[ "$base_pos" -lt "$cond_pos" ] \ - || fail "the conditional row's flag does not follow the root's own [build] flag" "$ninja1" -echo " ok: build.ninja's cxxflags carries -DBASE717 then -DX717A (reaches the scan and every TU)" - -grep -qF -- '-DX717A' compile_commands.json \ - || fail "-DX717A is missing from compile_commands.json" compile_commands.json -echo " ok: the flag reaches compile_commands.json" - -stdA=$(std_module_json_for '-DX717A') -[ -n "$stdA" ] || fail "no std-module.json records a std_flag containing -DX717A" build1.log -grep -qF -- '-DBASE717' "$stdA" \ - || fail "the std module's cached prebuild command is missing the root's unconditional flag" "$stdA" -echo " ok: the std BMI prebuild's recorded command carries both flags ($stdA)" - -echo "== 813/2: a selector that does not match the resolved target contributes nothing ==" - -mkdir -p "$TMP/two/src" -cd "$TMP/two" -cat > mcpp.toml <<'TOML' -[package] -name = "app717b" -version = "0.1.0" -standard = "c++23" - -[build] -dialect_cxxflags = ["-DBASE717"] - -[targets.app717b] -kind = "bin" -main = "src/main.cpp" - -[target.windows.build] -dialect_cxxflags = ["-DX717A"] -TOML -printf 'import std;\nint main() { std::println("ok"); return 0; }\n' > src/main.cpp - -"$MCPP" build --toolchain gcc@16.1.0 > build2.log 2>&1 || fail "the build with a non-matching [target.windows.build] row failed" build2.log -ninja2=$(find target -name build.ninja -not -path '*/.build-mcpp/*' | head -1) -grep -qF -- '-DX717A' "$ninja2" compile_commands.json \ - && fail "-DX717A from a non-matching [target.windows.build] row reached a Linux build" "$ninja2" -grep -E '^cxxflags' "$ninja2" | grep -qF -- '-DBASE717' \ - || fail "the root's own unconditional flag is missing" "$ninja2" -echo " ok: the non-matching row's flag reaches neither build.ninja nor compile_commands.json" - -echo "== 813/3: switching between a matching and a non-matching row rebuilds the std BMI (A, then B, then A) ==" - -mkdir -p "$TMP/cyc/src" -cd "$TMP/cyc" -write_cyc() { # $1 = the selector ("linux" or "windows") - cat > mcpp.toml < src/main.cpp - -write_cyc linux -rm -rf target compile_commands.json -"$MCPP" build --toolchain gcc@16.1.0 > cyc-a1.log 2>&1 || fail "build A (matching) failed" cyc-a1.log -grep -qF -- '-DCYC717' "$(find target -name build.ninja -not -path '*/.build-mcpp/*' | head -1)" \ - || fail "build A did not carry -DCYC717" cyc-a1.log -stdA1=$(std_module_json_for '-DCYC717') -[ -n "$stdA1" ] || fail "no std-module.json for build A" cyc-a1.log -echo " ok: build A (linux, matching) carries -DCYC717 ($stdA1)" - -# `target/` (and the project's own compile_commands.json) is removed between -# steps so that only ONE build.ninja / compile_commands.json ever exists at a -# time -- this scenario's subject is the SHARED std BMI cache under -# $MCPP_HOME/build-cache (content-addressed, independent of the project's own -# output directory), not the project's own directory reuse, which 813/4 below -# checks on its own terms. -write_cyc windows -rm -rf target compile_commands.json -"$MCPP" build --toolchain gcc@16.1.0 > cyc-b.log 2>&1 || fail "build B (non-matching) failed" cyc-b.log -grep -qF -- '-DCYC717' "$(find target -name build.ninja -not -path '*/.build-mcpp/*' | head -1)" compile_commands.json \ - && fail "build B (windows, non-matching on a Linux host) still carries -DCYC717" cyc-b.log -echo " ok: build B (windows, non-matching) carries no -DCYC717 -- a different std BMI" - -write_cyc linux -rm -rf target compile_commands.json -"$MCPP" build --toolchain gcc@16.1.0 > cyc-a2.log 2>&1 || fail "build A2 (matching again) failed" cyc-a2.log -grep -qF -- '-DCYC717' "$(find target -name build.ninja -not -path '*/.build-mcpp/*' | head -1)" \ - || fail "build A2 did not carry -DCYC717" cyc-a2.log -stdA2=$(std_module_json_for '-DCYC717') -[ -n "$stdA2" ] || fail "no std-module.json for build A2" cyc-a2.log -[ "$stdA2" = "$stdA1" ] \ - || fail "switching back to the matching row did not reuse build A's std BMI cache entry" "$stdA1" "$stdA2" -echo " ok: switching back to linux reuses build A's std BMI cache entry exactly (A, then B, then A)" - -echo "== 813/4: a dependency's own dialect_cxxflags reaches no command and does not move the graph's fingerprint ==" - -mkdir -p "$TMP/dep717/src" "$TMP/four/src" -cat > "$TMP/dep717/mcpp.toml" <<'TOML' -[package] -name = "dep717" -version = "0.1.0" -standard = "c++23" - -[targets.dep717] -kind = "lib" - -[build] -dialect_cxxflags = ["-DDEPFLAG_V1"] -TOML -printf 'export module dep717;\nexport int value() { return 42; }\n' > "$TMP/dep717/src/dep717.cppm" - -cd "$TMP/four" -cat > mcpp.toml <<'TOML' -[package] -name = "app717d" -version = "0.1.0" -standard = "c++23" - -[dependencies] -dep717 = { path = "../dep717" } - -[targets.app717d] -kind = "bin" -main = "src/main.cpp" -TOML -printf 'import dep717;\nint main() { return value() == 42 ? 0 : 1; }\n' > src/main.cpp - -"$MCPP" build --toolchain gcc@16.1.0 > build4a.log 2>&1 || fail "the first build (dependency carries -DDEPFLAG_V1) failed" build4a.log -ninjas4a=$(find target -name build.ninja -not -path '*/.build-mcpp/*') -[ "$(echo "$ninjas4a" | wc -l)" -eq 1 ] || fail "more than one build.ninja after the first build" build4a.log -grep -qF -- '-DDEPFLAG_V1' "$ninjas4a" compile_commands.json \ - && fail "the dependency's own dialect_cxxflags reached a command" "$ninjas4a" -echo " ok: the dependency's -DDEPFLAG_V1 reaches neither build.ninja nor compile_commands.json" - -sed -i 's/-DDEPFLAG_V1/-DDEPFLAG_V2/' "$TMP/dep717/mcpp.toml" -"$MCPP" build --toolchain gcc@16.1.0 > build4b.log 2>&1 || fail "the second build (dependency's flag changed) failed" build4b.log -ninjas4b=$(find target -name build.ninja -not -path '*/.build-mcpp/*') -if [ "$(echo "$ninjas4b" | wc -l)" -ne 1 ]; then - echo "build.ninja files after the second build:"; echo "$ninjas4b" - fail "changing the dependency's own dialect_cxxflags left a second output directory behind (the fingerprint moved)" build4b.log -fi -if [ "$ninjas4b" != "$ninjas4a" ]; then - echo "first build.ninja: $ninjas4a"; echo "second build.ninja: $ninjas4b" - fail "changing the dependency's own dialect_cxxflags moved the graph's output directory" build4b.log -fi -grep -qF -- '-DDEPFLAG_V2' "$ninjas4b" compile_commands.json \ - && fail "the dependency's changed dialect_cxxflags reached a command" "$ninjas4b" -echo " ok: the same output directory is reused; the dependency's own key never entered the graph's fingerprint" - -echo "PASS: 813" diff --git a/tests/e2e/81_capability_binding.sh b/tests/e2e/81_capability_binding.sh deleted file mode 100755 index ea963b0ad..000000000 --- a/tests/e2e/81_capability_binding.sh +++ /dev/null @@ -1,106 +0,0 @@ -#!/usr/bin/env bash -# 81_capability_binding.sh — Feature System v2 Stage 3: capabilities. -# A package/feature may `requires = ["cap"]` an abstract capability; providers -# declare `provides = ["cap"]`. The resolver binds exactly ONE provider from the -# dependency graph: -# 0 providers → hard error "no package provides" -# 1 provider → bind (success) -# ≥2 unpinned → hard error listing candidates -# pinned [capabilities] → the pin wins -# See .agents/docs/2026-06-29-feature-capability-model-design.md. -# -# No `requires:` capability → runs on all three CI platforms. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -# Two interchangeable BLAS providers. -for impl in alpha beta; do - mkdir -p "blas_$impl/src" - cat > "blas_$impl/mcpp.toml" < "blas_$impl/src/blas_$impl.cppm" -done - -mkdir -p app/src -echo 'int main() { return 0; }' > app/src/main.cpp -cd app - -write_manifest() { # $1 = dependency lines - cat > mcpp.toml < c1.log 2>&1 || { cat c1.log; echo "FAIL case1: single provider should bind"; exit 1; } - -# Case 2: capability required but NO provider in the graph → hard error. -write_manifest '# no provider' -rm -rf target -if "$MCPP" build --features use_blas > c2.log 2>&1; then - cat c2.log; echo "FAIL case2: missing provider must error"; exit 1 -fi -grep -qi "no package provides .*blas\|capability 'blas'" c2.log || { cat c2.log; echo "FAIL case2: wrong/no error text"; exit 1; } - -# Case 3: two providers, unpinned → ambiguity error listing candidates. -write_manifest 'blas_alpha = { path = "../blas_alpha" } -blas_beta = { path = "../blas_beta" }' -rm -rf target -if "$MCPP" build --features use_blas > c3.log 2>&1; then - cat c3.log; echo "FAIL case3: ambiguous providers must error"; exit 1 -fi -grep -qi "blas_alpha" c3.log && grep -qi "blas_beta" c3.log || { cat c3.log; echo "FAIL case3: error must list candidates"; exit 1; } - -# Case 4: same two providers, pinned via [capabilities] → the pin wins, builds. -cat > mcpp.toml < c4.log 2>&1 || { cat c4.log; echo "FAIL case4: pin should resolve ambiguity"; exit 1; } - -# Case 5: feature inactive → no requirement, no provider needed, builds clean. -write_manifest '# no provider' -rm -rf target -"$MCPP" build > c5.log 2>&1 || { cat c5.log; echo "FAIL case5: inactive feature must not require capability"; exit 1; } - -# Case 6: two providers, --cap pin on the CLI resolves the ambiguity. -write_manifest 'blas_alpha = { path = "../blas_alpha" } -blas_beta = { path = "../blas_beta" }' -rm -rf target -"$MCPP" build --features use_blas --cap blas=blas_alpha > c6.log 2>&1 || { cat c6.log; echo "FAIL case6: --cap pin should resolve ambiguity"; exit 1; } - -echo "OK" diff --git a/tests/e2e/82_feature_optional_deps.sh b/tests/e2e/82_feature_optional_deps.sh deleted file mode 100755 index 3bb2812b3..000000000 --- a/tests/e2e/82_feature_optional_deps.sh +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env bash -# 82_feature_optional_deps.sh — Feature System v2 Stage 2a: a dependency declared -# under a feature (`[feature-deps.]`) is pulled ONLY when that feature is -# active; otherwise it is never fetched/resolved. When active it is resolved and -# built like any normal dependency. See -# .agents/docs/2026-06-29-feature-optional-dependencies-s2-design.md. -# -# No `requires:` capability → runs on all three CI platforms. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -# A small path library the feature optionally pulls in. -mkdir -p widget/src -cat > widget/mcpp.toml <<'EOF' -[package] -name = "widget" -version = "0.1.0" - -[targets.widget] -kind = "lib" -EOF -cat > widget/src/widget.cppm <<'EOF' -export module widget; -export int widget_answer() { return 42; } -EOF - -mkdir -p app/src -echo 'int main() { return 0; }' > app/src/main.cpp -cat > app/mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" - -[features] -default = [] -extra = [] - -# `widget` is declared ONLY under the feature, so it is optional: resolved only -# when --features extra is active. -[feature-deps.extra] -widget = { path = "../widget" } -EOF - -cd app - -# Both directions match EITHER verb. A dependency that is in the graph announces -# itself as `Compiling` or, when the global build cache supplies its objects, as -# `Cached` — so "Compiling" alone is too narrow for the positive AND too weak for -# the negative: a wrongly-pulled dependency whose objects happened to be cached -# would print `Cached widget` and slip past a `Compiling`-only check. -pulled() { grep -qE 'Compiling widget ' "$1"; } - -# 1. Feature inactive → widget is NOT pulled (never resolved, never built). -"$MCPP" build -v > b1.log 2>&1 || { cat b1.log; echo "FAIL: baseline build failed"; exit 1; } -if pulled b1.log; then - cat b1.log; echo "FAIL: widget must NOT be pulled when feature inactive"; exit 1 -fi - -# 2. Feature active → widget IS pulled and built like a normal dependency. -rm -rf target -"$MCPP" build -v --features extra > b2.log 2>&1 || { cat b2.log; echo "FAIL: feature build failed (widget not pulled?)"; exit 1; } -pulled b2.log || { cat b2.log; echo "FAIL: widget was not pulled when feature active"; exit 1; } - -echo "OK" diff --git a/tests/e2e/83_feature_defines_propagate.sh b/tests/e2e/83_feature_defines_propagate.sh deleted file mode 100755 index dc5d0da54..000000000 --- a/tests/e2e/83_feature_defines_propagate.sh +++ /dev/null @@ -1,91 +0,0 @@ -#!/usr/bin/env bash -# 83_feature_defines_propagate.sh — Feature System v2: a dependency's active- -# feature `defines` are INTERFACE requirements. When a consumer enables a feature -# on a (header-only) dependency, that feature's `defines` must reach the -# CONSUMER's own translation units — not only the dependency's own compile. This -# is the header-only-library case (e.g. Eigen's `use_blas` → EIGEN_USE_BLAS, -# which only changes behavior in the TU that includes Eigen's headers). The -# define propagates along Public/Interface dependency edges, mirroring -# include_dirs. See .agents/docs/2026-06-29-feature-capability-model-design.md. -# -# No `requires:` capability → runs on all three CI platforms. -set -e -# The project-root copy of the compile database is opt-in since -# 2026.10.26.1 ([build] compile_commands); this test reads it. -export MCPP_COMPILE_COMMANDS=compile_commands.json - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -# Header-only dependency: a feature `turbo` carries a package-owned define. -mkdir -p widget/include/widget widget/src -cat > widget/mcpp.toml <<'EOF' -[package] -name = "widget" -version = "0.1.0" - -[features] -default = [] -turbo = { defines = ["WIDGET_TURBO=1"] } - -[build] -include_dirs = ["include"] - -[targets.widget] -kind = "lib" -EOF -cat > widget/include/widget/widget.hpp <<'EOF' -#pragma once -// The macro's value is only meaningful in the TU that includes this header — -// exactly the header-only library situation. -inline int widget_mode() { -#ifdef WIDGET_TURBO - return 1; -#else - return 0; -#endif -} -EOF -cat > widget/src/widget.cppm <<'EOF' -export module widget; -export int widget_anchor() { return 0; } -EOF - -mkdir -p app/src -cat > app/mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" - -[dependencies] -widget = { path = "../widget", features = ["turbo"] } -EOF -# The consumer's TU asserts the dependency's feature define reached it. If the -# define does NOT propagate, this fails to compile (#error), failing the build. -cat > app/src/main.cpp <<'EOF' -#include -#ifndef WIDGET_TURBO -#error "WIDGET_TURBO did not propagate from widget[turbo] to the consumer" -#endif -int main() { return widget_mode() == 1 ? 0 : 2; } -EOF - -cd app - -# Build: widget[turbo]'s define must reach app/src/main.cpp. A missing -# propagation makes main.cpp hit the #error and the build fails. -"$MCPP" build > b.log 2>&1 || { cat b.log; echo "FAIL: feature define did not propagate to consumer"; exit 1; } - -# Double-check the compile database carries the define on the consumer TU. -grep -q 'WIDGET_TURBO' compile_commands.json || { - echo "FAIL: WIDGET_TURBO missing from consumer compile_commands.json"; cat compile_commands.json; exit 1; } - -# And the produced binary observes turbo mode at runtime. (Binary name is -# platform-dependent — `app` on POSIX, `app.exe` on Windows.) -BIN=$(find target -type f \( -name app -o -name app.exe \) | head -1) -[ -n "$BIN" ] || { echo "FAIL: built binary not found under target/"; exit 1; } -"$BIN"; rc=$? -[ "$rc" -eq 0 ] || { echo "FAIL: binary did not observe WIDGET_TURBO (exit $rc)"; exit 1; } - -echo "OK" diff --git a/tests/e2e/85_target_cfg_build_flags.sh b/tests/e2e/85_target_cfg_build_flags.sh deleted file mode 100755 index 0a749f1db..000000000 --- a/tests/e2e/85_target_cfg_build_flags.sh +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env bash -# 85_target_cfg_build_flags.sh — L1 platform-conditional config: a normal mcpp.toml -# scopes [build] flags to a target predicate via `[target.'cfg(...)'.build]`. The -# predicate is evaluated against the RESOLVED target (here the host, a native -# build) — NOT textually — so exactly the host's os/arch predicates apply. This -# test is HOST-AWARE: it asserts the correct subset applies on whichever of -# linux/macos/windows + x86_64/aarch64 the runner is, so it validates the -# evaluator on all three CI platforms. See -# .agents/docs/2026-06-29-manifest-environment-and-platform-design.md (L1). -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -mkdir -p app/src -cat > app/mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" - -[target.'cfg(linux)'.build] -cxxflags = ["-DCOND_LINUX=1"] -[target.'cfg(macos)'.build] -cxxflags = ["-DCOND_MACOS=1"] -[target.'cfg(windows)'.build] -cxxflags = ["-DCOND_WIN=1"] -[target.'cfg(unix)'.build] -cxxflags = ["-DCOND_UNIX=1"] -[target.'cfg(arch = "x86_64")'.build] -cxxflags = ["-DCOND_X64=1"] -[target.'cfg(arch = "aarch64")'.build] -cxxflags = ["-DCOND_ARM64=1"] -# Boolean combinator: any(linux, macos) == unix-family. -[target.'cfg(any(linux, macos))'.build] -cxxflags = ["-DCOND_UNIXLIKE=1"] -EOF -cat > app/src/main.cpp <<'EOF' -// Exactly one OS predicate must apply (the host's), regardless of platform. -#if (defined(COND_LINUX) + defined(COND_MACOS) + defined(COND_WIN)) != 1 -#error "exactly one of cfg(linux)/cfg(macos)/cfg(windows) must apply on any host" -#endif -// cfg(unix) applies iff not windows. -#if defined(COND_WIN) && defined(COND_UNIX) -#error "cfg(unix) wrongly applied on a windows host" -#endif -#if !defined(COND_WIN) && !defined(COND_UNIX) -#error "cfg(unix) should apply on a non-windows host" -#endif -// any(linux, macos) must agree with unix on the CI platforms. -#if defined(COND_UNIXLIKE) != defined(COND_UNIX) -#error "cfg(any(linux,macos)) disagreed with cfg(unix)" -#endif -// Exactly one arch predicate must apply on the CI runners (x86_64 or aarch64). -#if (defined(COND_X64) + defined(COND_ARM64)) != 1 -#error "exactly one of cfg(arch=x86_64)/cfg(arch=aarch64) must apply" -#endif -// Mutual exclusion: the non-host OS predicate must NOT leak in. -int main() { return 0; } -EOF - -cd app -# The #error guards ARE the assertion: a clean build proves the right conditional -# cxxflags reached the TU and the wrong ones did not. -"$MCPP" build > b.log 2>&1 || { cat b.log; echo "FAIL: conditional cfg flags mis-applied for this host"; exit 1; } - -echo "OK" diff --git a/tests/e2e/86_target_cfg_dependencies.sh b/tests/e2e/86_target_cfg_dependencies.sh deleted file mode 100755 index 8ac4bfde5..000000000 --- a/tests/e2e/86_target_cfg_dependencies.sh +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env bash -# 86_target_cfg_dependencies.sh — L1b platform-conditional DEPENDENCIES: a normal -# mcpp.toml can scope a dependency to a target predicate via -# `[target.'cfg(...)'.dependencies]`, evaluated against the resolved target (here -# the host). A matching predicate's dep resolves like any dep; a non-matching -# predicate's dep is never fetched/resolved. Host-aware so it runs on all three -# CI platforms. See .agents/docs/2026-06-29-manifest-environment-and-platform-design.md (L1b). -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -# A real path dependency exposing one symbol. -mkdir -p widget/src -cat > widget/mcpp.toml <<'EOF' -[package] -name = "widget" -version = "0.1.0" -[targets.widget] -kind = "lib" -EOF -cat > widget/src/widget.cppm <<'EOF' -export module widget; -export int widget_anchor() { return 0; } -EOF - -mkdir -p app/src -cat > app/mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" - -# widget is pulled under whichever family matches the host — so exactly one of -# these applies and widget is ALWAYS resolved (on any of the 3 platforms). -[target.'cfg(unix)'.dependencies] -widget = { path = "../widget" } -[target.'cfg(windows)'.dependencies] -widget = { path = "../widget" } - -# A never-matching predicate (no such arch) points at a non-existent path. If the -# conditional merge wrongly pulled it, resolution of the bogus path would FAIL the -# build — so a clean build proves non-matching deps are excluded. -[target.'cfg(arch = "no_such_arch")'.dependencies] -ghost = { path = "../this_path_does_not_exist" } -EOF -cat > app/src/main.cpp <<'EOF' -import widget; // fails to build unless the cfg-matched widget dep was resolved -int main() { return widget_anchor(); } -EOF - -cd app -"$MCPP" build > b.log 2>&1 || { cat b.log; echo "FAIL: conditional dep not resolved, or non-matching dep wrongly pulled"; exit 1; } - -# Sanity: the matched dependency really was wired (widget appears in the lockfile). -if [ -f mcpp.lock ]; then - grep -q 'widget' mcpp.lock || { echo "FAIL: widget missing from lockfile"; cat mcpp.lock; exit 1; } -fi -# The never-matching ghost must NOT be present. -if [ -f mcpp.lock ] && grep -q 'ghost' mcpp.lock; then - echo "FAIL: non-matching cfg dependency 'ghost' leaked into the resolve"; exit 1; fi - -echo "OK" diff --git a/tests/e2e/895_conditional_configuration_rows_count_as_they_evaluate.sh b/tests/e2e/895_conditional_configuration_rows_count_as_they_evaluate.sh deleted file mode 100755 index 6d69a8f75..000000000 --- a/tests/e2e/895_conditional_configuration_rows_count_as_they_evaluate.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 895 -- a member's conditional configuration rows count as they evaluate for -# the build (W4, design 2026-10-10 §3.5 item 5; #786). -# -# R1 A member's own `[target..build] dialect_cxxflags` reaches the -# build when the member is planned under the workspace's virtual root. -# 2026.10.8.1 dropped it: the plan's root carried none of the member's -# rows (`DIAL=0` with `-p app`, `DIAL=2` for the same package alone). -# R2 Two members that differ only in a row for another target are one -# configuration: `--workspace` plans one graph. -# R3 Two members whose rows evaluate differently for this target are two. -set -e - -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -export MCPP_HOME="$TMP/mcpp-home" -source "$(dirname "$0")/_inherit_toolchain.sh" -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } - -HOST='cfg(any(os = "linux", os = "macos", os = "windows"))' -member() { # $1 = name, $2 = extra manifest text - mkdir -p "$1/src" - printf '[package]\nname = "%s"\nversion = "0.1.0"\n[targets.%s]\nkind = "bin"\nmain = "src/main.cpp"\n%s\n' \ - "$1" "$1" "$2" > "$1/mcpp.toml" - printf '#include \n#ifndef DIAL\n#define DIAL 0\n#endif\nint main() { std::printf("DIAL=%%d\\n", DIAL); }\n' > "$1/src/main.cpp" -} -run_of() { local b; b=$(find target -path "*/bin/$1/$1" -type f -o -path "*/bin/$1" -type f -o -path "*/bin/$1.exe" -type f | head -1); "$b" | tr -d '\r'; } - -# ── R1 ──────────────────────────────────────────────────────────────────── -mkdir -p "$TMP/r1" && cd "$TMP/r1" -printf '[workspace]\nmembers = ["app"]\n' > mcpp.toml -member app "[target.'$HOST'.build] -dialect_cxxflags = [\"-DDIAL=2\"]" -"$MCPP" build -p app > b.log 2>&1 || fail "build" b.log -got=$(run_of app) -[ "$got" = "DIAL=2" ] || fail "the member's own conditional dialect_cxxflags did not reach the build: $got" b.log -echo "ok R1" - -# ── R2, R3 ──────────────────────────────────────────────────────────────── -mkdir -p "$TMP/r2" && cd "$TMP/r2" -printf '[workspace]\nmembers = ["a", "b"]\n' > mcpp.toml -member a "[target.'cfg(os = \"none\")'.build] -dialect_cxxflags = [\"-DNEVER=1\"]" -member b "" -"$MCPP" build --workspace > ws.log 2>&1 || fail "--workspace" ws.log -n=$(find target -name build.ninja -not -path '*/.build-mcpp/*' | wc -l | tr -d ' ') -[ "$n" = 1 ] || fail "a row for another target split the members into $n configurations" ws.log - -mkdir -p "$TMP/r3" && cd "$TMP/r3" -printf '[workspace]\nmembers = ["a", "b"]\n' > mcpp.toml -member a "[target.'$HOST'.build] -dialect_cxxflags = [\"-DDIAL=3\"]" -member b "" -"$MCPP" build --workspace > ws.log 2>&1 || fail "--workspace" ws.log -n=$(find target -name build.ninja -not -path '*/.build-mcpp/*' | wc -l | tr -d ' ') -[ "$n" = 2 ] || fail "members whose rows evaluate differently share $n configuration(s)" ws.log -[ "$(run_of a)" = "DIAL=3" ] || fail "member a lost its dialect: $(run_of a)" ws.log -[ "$(run_of b)" = "DIAL=0" ] || fail "member b received a's dialect: $(run_of b)" ws.log -echo "ok R2 R3" - -echo "PASS: 895_conditional_configuration_rows_count_as_they_evaluate" diff --git a/tests/e2e/MIGRATION.tsv b/tests/e2e/MIGRATION.tsv index eb8c67a48..043dc1da3 100644 --- a/tests/e2e/MIGRATION.tsv +++ b/tests/e2e/MIGRATION.tsv @@ -72,11 +72,11 @@ script subsystem class destination note 64_shared_soname_runtime_alias.sh runtime merge e2e/runtime 65_toolchain_runtime_dirs_for_run.sh run merge e2e/run 66_runtime_provides.sh runtime merge e2e/runtime -67_features_strict.sh features merge e2e/features +67_features_strict.sh features merge e2e/features::activation/67 features strict migrated (batch 5); script removed 68_profile_passthrough.sh build merge e2e/build 69_package_templates.sh cli merge e2e/cli 70_per_target_flags.sh build merge e2e/build -71_required_features_gate.sh features merge e2e/features +71_required_features_gate.sh features merge e2e/features::activation/71 required features gate migrated (batch 5); script removed 72_test_profile_features.sh test merge e2e/test::graph/72 test profile features migrated (batch 1); script removed 73_issue131_per_target_cxxflag.sh build merge e2e/build 74_run_no_loader_env_leak.sh run merge e2e/run @@ -86,14 +86,14 @@ script subsystem class destination note 77_cdb_preserves_test_entries.sh cdb merge e2e/cdb 78_test_main_combinations.sh test merge e2e/test::graph/78 test main combinations migrated (batch 1); script removed 79_gtest_regular_dep_feature_main.sh deps merge e2e/deps::resolve/79 gtest regular dep feature main migrated (batch 3); script removed -80_feature_defines.sh features merge e2e/features -81_capability_binding.sh features merge e2e/features -82_feature_optional_deps.sh features merge e2e/features -83_feature_defines_propagate.sh features merge e2e/features +80_feature_defines.sh features merge e2e/features::activation/80 feature defines migrated (batch 5); script removed +81_capability_binding.sh features merge e2e/features::activation/81 capability binding migrated (batch 5); script removed +82_feature_optional_deps.sh features merge e2e/features::activation/82 feature optional deps migrated (batch 5); script removed +83_feature_defines_propagate.sh features merge e2e/features::activation/83 feature defines propagate migrated (batch 5); script removed 84_runtime_dll_deploy.sh windows merge e2e/windows -85_target_cfg_build_flags.sh features merge e2e/features +85_target_cfg_build_flags.sh features merge e2e/features::conditional/85 target cfg build flags migrated (batch 5); script removed 86_llvm_hermetic_link.sh toolchain merge e2e/toolchain -86_target_cfg_dependencies.sh features merge e2e/features +86_target_cfg_dependencies.sh features merge e2e/features::conditional/86 target cfg dependencies migrated (batch 5); script removed 87_build_default_profile.sh build merge e2e/build 88_xlings_environment.sh self merge e2e/self 89_build_mcpp.sh build-program merge e2e/build-program::protocol/89 build mcpp migrated (batch 2); script removed @@ -108,15 +108,15 @@ script subsystem class destination note 98_reflection_import_std.sh toolchain merge e2e/toolchain 99_msvc_native_build.sh windows merge e2e/windows 100_cppfly_reflection.sh toolchain merge e2e/toolchain -100_feature_sources_test_mode.sh features merge e2e/features +100_feature_sources_test_mode.sh features merge e2e/features::sources/100 feature sources test mode migrated (batch 5); script removed 101_cppfly_llvm_soft.sh toolchain merge e2e/toolchain 102_mingw_cross_wine.sh windows keep keep emulator: runs the cross-built PE under wine 103_target_vocabulary.sh target merge e2e/target 104_asm_sources_gas.sh build merge e2e/build 105_asm_sources_nasm.sh build merge e2e/build -106_feature_gated_sources_toml.sh features merge e2e/features +106_feature_gated_sources_toml.sh features merge e2e/features::sources/106 feature gated sources toml migrated (batch 5); script removed 107_generated_files_toml.sh actions merge e2e/actions::generated/107 generated files toml migrated (batch 4); script removed -108_cfg_conditional_sources.sh features merge e2e/features +108_cfg_conditional_sources.sh features merge e2e/features::conditional/108 cfg conditional sources migrated (batch 5); script removed 109_per_glob_flags.sh build merge e2e/build 110_build_mcpp_env_contract.sh build-program merge e2e/build-program::protocol/110 build mcpp env contract migrated (batch 2); script removed 111_dep_build_mcpp.sh build-program merge e2e/build-program::deps/111 dep build mcpp migrated (batch 2); script removed @@ -127,16 +127,16 @@ script subsystem class destination note 116_include_family_and_define_spaces.sh build merge e2e/build 117_object_path_collision.sh build merge e2e/build 118_purview_include_rebuild.sh modules merge e2e/modules -119_dep_cfg_sources.sh features merge e2e/features +119_dep_cfg_sources.sh features merge e2e/features::conditional/119 dep cfg sources migrated (batch 5); script removed 120_ws_root_indices.sh index merge e2e/index 121_default_ns_redirect.sh index merge e2e/index 122_run_member.sh workspace merge e2e/workspace 123_same_named_main_across_dep.sh build merge e2e/build 124_gen_source_out_of_root_object_path.sh actions merge e2e/actions::generated/124 gen source out of root object path migrated (batch 4); script removed 125_build_mcpp_dep_dir.sh build-program merge e2e/build-program::deps/125 build mcpp dep dir migrated (batch 2); script removed -126_default_features_opt_out.sh features merge e2e/features -127_transitive_default_features_optout.sh features merge e2e/features -128_feature_forwarding.sh features merge e2e/features +126_default_features_opt_out.sh features merge e2e/features::defaults/126 default features opt out migrated (batch 5); script removed +127_transitive_default_features_optout.sh features merge e2e/features::defaults/127 transitive default features optout migrated (batch 5); script removed +128_feature_forwarding.sh features merge e2e/features::defaults/128 feature forwarding migrated (batch 5); script removed 130_freestanding_riscv_build_and_run.sh baremetal keep keep emulator: boots the image in qemu-system-riscv64 131_freestanding_bsp_supplies_everything.sh baremetal keep keep emulator: boots the image in qemu-system-riscv64 132_freestanding_test_and_artifacts.sh baremetal keep keep emulator: runs the freestanding tests in qemu-system-riscv64 @@ -145,17 +145,17 @@ script subsystem class destination note 135_diagnostic_promise_resolves.sh toolchain merge e2e/toolchain 136_aarch64_zero_libc_target.sh baremetal merge e2e/baremetal 137_x86_64_zero_libc_target.sh baremetal merge e2e/baremetal -138_feature_sources_gate_vs_provider.sh features merge e2e/features +138_feature_sources_gate_vs_provider.sh features merge e2e/features::sources/138 feature sources gate vs provider migrated (batch 5); script removed 139_build_program_advisory.sh build-program merge e2e/build-program::protocol/139 build program advisory migrated (batch 2); script removed 141_include_dirs_after.sh build merge e2e/build 142_generated_files_incremental.sh actions merge e2e/actions::generated/142 generated files incremental migrated (batch 4); script removed 143_build_mcpp_source_directive.sh build-program merge e2e/build-program::protocol/143 build mcpp source directive migrated (batch 2); script removed 144_build_mcpp_include_dir.sh build-program merge e2e/build-program::protocol/144 build mcpp include dir migrated (batch 2); script removed 145_root_build_mcpp_dep_dirs.sh build-program merge e2e/build-program::deps/145 root build mcpp dep dirs migrated (batch 2); script removed -146_feature_flags.sh features merge e2e/features +146_feature_flags.sh features merge e2e/features::sources/146 feature flags migrated (batch 5); script removed 147_per_os_features_xpkg.sh index merge e2e/index 148_wide_include_list.sh build merge e2e/build -149_conditional_glob_flags.sh features merge e2e/features +149_conditional_glob_flags.sh features merge e2e/features::conditional/149 conditional glob flags migrated (batch 5); script removed 150_clang_module_operator_template.sh toolchain keep keep a state canary of the bundled Clang (expected crash), not an assertion about mcpp 151_index_url_artifact_migration.sh self merge e2e/self 152_test_subdir_names.sh test merge e2e/test::selection/152 test subdir names migrated (batch 1); script removed @@ -203,7 +203,7 @@ script subsystem class destination note 192_install_path_namespace.sh index merge e2e/index 193_provision_reexport.sh build-program merge e2e/build-program::hosttools/193 provision reexport migrated (batch 2); script removed 194_build_mcpp_glob_input.sh build-program merge e2e/build-program::protocol/194 build mcpp glob input migrated (batch 2); script removed -195_target_cfg_feature_deps.sh features merge e2e/features +195_target_cfg_feature_deps.sh features merge e2e/features::conditional/195 target cfg feature deps migrated (batch 5); script removed 196_version_identity_and_lock.sh deps merge e2e/deps::lock/196 version identity and lock migrated (batch 3); script removed 197_windows_resources.sh windows merge e2e/windows 198_windows_resources_cross.sh windows merge e2e/windows @@ -248,7 +248,7 @@ script subsystem class destination note 244_pack_library_gate.sh pack merge e2e/pack 245_pack_library_fat_target_selection.sh pack merge e2e/pack 246_explicit_empty_sources.sh build merge e2e/build -247_bare_triple_conditional_native.sh features merge e2e/features +247_bare_triple_conditional_native.sh features merge e2e/features::conditional/247 bare triple conditional native migrated (batch 5); script removed 248_pack_library_fat_pe_leg.sh windows merge e2e/windows 249_pack_workspace_root_unchanged.sh pack merge e2e/pack 250_pack_names_the_target.sh pack merge e2e/pack @@ -299,7 +299,7 @@ script subsystem class destination note 305_built_from_is_not_published.sh openkal merge e2e/openkal 306_dependency_linkage_axis.sh runtime merge e2e/runtime 307_symbol_provision.sh runtime merge e2e/runtime -308_dependency_required_features.sh features merge e2e/features +308_dependency_required_features.sh features merge e2e/features::activation/308 dependency required features migrated (batch 5); script removed 309_host_module_identity.sh build-program merge e2e/build-program::hosttools/309 host module identity migrated (batch 2); script removed 310_build_dependencies.sh build-program merge e2e/build-program::deps/310 build dependencies migrated (batch 2); script removed 311_rule_build_dependencies.sh build-program merge e2e/build-program::deps/311 rule build dependencies migrated (batch 2); script removed @@ -316,7 +316,7 @@ script subsystem class destination note 325_system_toolchain_refused.sh toolchain merge e2e/toolchain 326_dependency_standard_floor.sh deps merge e2e/deps::floors/326 dependency standard floor migrated (batch 3); script removed 327_declared_xlings_deps_report_their_own_failure.sh index merge e2e/index -328_layer_predicates_apply_and_unknown_ones_say_so.sh features merge e2e/features +328_layer_predicates_apply_and_unknown_ones_say_so.sh features merge e2e/features::layers/328 layer predicates apply and unknown ones say so migrated (batch 5); script removed 329_exit_codes_match_the_contract.sh cli merge e2e/cli 330_runner_hosted_targets.sh run merge e2e/run 331_xlings_workspace_one_table.sh index merge e2e/index @@ -330,7 +330,7 @@ script subsystem class destination note 339_run_reports_the_program_status.sh run merge e2e/run 340_no_orphan_survives_a_killed_mcpp.sh cli merge e2e/cli 600_accel_variant_selection.sh build-program merge e2e/build-program::accel/600 accel variant selection migrated (batch 2); script removed -601_exclusive_capability.sh features merge e2e/features +601_exclusive_capability.sh features merge e2e/features::activation/601 exclusive capability migrated (batch 5); script removed 603_version_floor.sh deps merge e2e/deps::floors/603 version floor migrated (batch 3); script removed 604_offline_skips_sandbox_bootstrap.sh self merge e2e/self 605_probe_channel_and_accel_reach_the_build_program.sh build-program merge e2e/build-program::accel/605 probe channel and accel reach the build program migrated (batch 2); script removed @@ -344,7 +344,7 @@ script subsystem class destination note 612_build_program_file_input_fast_path.sh build-program merge e2e/build-program::protocol/612 build program file input fast path migrated (batch 2); script removed 613_sycl_sources_are_device_sources.sh actions merge e2e/actions::devices/613 sycl sources are device sources migrated (batch 4); script removed 614_feature_xlings_reaches_xpkg_dir.sh index merge e2e/index -615_feature_deps_shared_library_is_linked.sh features merge e2e/features +615_feature_deps_shared_library_is_linked.sh features merge e2e/features::activation/615 feature deps shared library is linked migrated (batch 5); script removed 616_examples_and_their_index_agree.sh repo sink unit/test_repo_consistency answers a parse/selection rule without a build: examples vs their index 617_build_program_knows_the_cxx_stdlib.sh build-program merge e2e/build-program::protocol/617 build program knows the cxx stdlib migrated (batch 2); script removed 618_dependency_feature_tools_reach_its_build_program.sh build-program merge e2e/build-program::deps/618 dependency feature tools reach its build program migrated (batch 2); script removed @@ -366,7 +366,7 @@ script subsystem class destination note 634_a_private_module_fragment_is_not_a_partition.sh modules merge e2e/modules 635_a_byte_order_mark_does_not_hide_a_module.sh modules merge e2e/modules 636_a_module_extension_file_need_not_provide_a_module.sh modules merge e2e/modules -637_a_conditional_runtime_table_alone_is_applied.sh features merge e2e/features +637_a_conditional_runtime_table_alone_is_applied.sh features merge e2e/features::layers/637 a conditional runtime table alone is applied migrated (batch 5); script removed 638_pack_format_dispatch.sh pack merge e2e/pack 639_the_scanner_does_not_read_inside_a_comment.sh modules merge e2e/modules 640_a_capability_pin_explains_its_own_row.sh target merge e2e/target @@ -384,7 +384,7 @@ script subsystem class destination note 652_an_application_is_a_kind.sh pack merge e2e/pack 652b_an_application_on_android_is_a_shared_library.sh android merge e2e/android 653_exceptions_is_the_second_abi_member.sh target merge e2e/target -654_a_requirement_on_the_target_axis.sh features merge e2e/features +654_a_requirement_on_the_target_axis.sh features merge e2e/features::layers/654 a requirement on the target axis migrated (batch 5); script removed 656_run_hands_the_distributable_to_the_runner.sh run merge e2e/run 657_a_build_program_under_the_web_row_uses_the_host_compiler.sh web merge e2e/web 658_a_program_runs_on_an_attached_android_device.sh android keep keep a physical Android device over adb @@ -405,7 +405,7 @@ script subsystem class destination note 673_test_discover_names_where_tests_are.sh test merge e2e/test::selection/673 test discover names where tests are migrated (batch 1); script removed 674_a_format_is_run_by_the_runner_named_after_it.sh run merge e2e/run 675_android_test_programs_carry_their_cxx_runtime.sh android merge e2e/android -677_a_conditional_dependency_replaces_the_unconditional_one.sh features merge e2e/features +677_a_conditional_dependency_replaces_the_unconditional_one.sh features merge e2e/features::conditional/677 a conditional dependency replaces the unconditional one migrated (batch 5); script removed 678_a_row_states_a_library_form.sh target merge e2e/target 679_a_path_dependency_takes_its_manifests_identity.sh deps merge e2e/deps::identity/679 a path dependency takes its manifests identity migrated (batch 3); script removed 680_a_dependency_floor_on_the_android_api_level.sh android merge e2e/android @@ -429,11 +429,11 @@ script subsystem class destination note 703_the_llvm_row_on_the_msvc_abi_records_its_static_crt.sh windows merge e2e/windows 704_cxx_identity_across_macho_images.sh macos keep keep a measurement leg (prints a table, asserts nothing) 705_cxx_identity_across_pe_images.sh windows keep keep a measurement leg (prints a table, asserts nothing) -710_a_forward_reaches_every_dependency_table.sh features merge e2e/features -711_a_feature_deps_restatement_names_one_source.sh features merge e2e/features +710_a_forward_reaches_every_dependency_table.sh features merge e2e/features::defaults/710 a forward reaches every dependency table migrated (batch 5); script removed +711_a_feature_deps_restatement_names_one_source.sh features merge e2e/features::activation/711 a feature deps restatement names one source migrated (batch 5); script removed 712_a_tool_package_contributes_nothing_to_its_consumer.sh build-program merge e2e/build-program::hosttools/712 a tool package contributes nothing to its consumer migrated (batch 2); script removed 713_a_git_dependency_selects_a_repository_member.sh deps merge e2e/deps::git/713 a git dependency selects a repository member migrated (batch 3); script removed -714_features_names_a_dependency_feature.sh features merge e2e/features +714_features_names_a_dependency_feature.sh features merge e2e/features::activation/714 features names a dependency feature migrated (batch 5); script removed 720_the_root_build_program_reads_the_resolved_graph.sh build-program merge e2e/build-program::deps/720 the root build program reads the resolved graph migrated (batch 2); script removed 721_an_android_row_links_on_a_macos_host.sh macos merge e2e/macos 722_pack_strips_what_the_graph_built.sh pack merge e2e/pack @@ -447,7 +447,7 @@ script subsystem class destination note 736_compile_flag_words_reach_the_compiler_and_the_databases.sh cdb merge e2e/cdb 737_the_declared_runtime_payload_is_installed.sh index merge e2e/index 738_a_graph_supplied_target_closes_the_hosts_own_search.sh target merge e2e/target -739_a_private_feature_dep_does_not_reach_the_consumer.sh features merge e2e/features +739_a_private_feature_dep_does_not_reach_the_consumer.sh features merge e2e/features::activation/739 a private feature dep does not reach the consumer migrated (batch 5); script removed 740_gcc_over_a_graph_c_library_is_degraded_not_refused.sh toolchain merge e2e/toolchain 741_c_abi_environment_declared_by_the_c_library.sh target merge e2e/target 742_gcc_accepted_when_the_realisation_is_empty.sh toolchain merge e2e/toolchain @@ -499,7 +499,7 @@ script subsystem class destination note 810_two_packages_deploy_the_same_bytes_to_one_name.sh runtime merge e2e/runtime 811_place_dlls_never_overwrites_a_declared_deploy.sh windows merge e2e/windows 812_an_index_floor_is_a_closing_tip.sh index merge e2e/index -813_conditional_dialect_cxxflags_root_only.sh features merge e2e/features +813_conditional_dialect_cxxflags_root_only.sh features merge e2e/features::conditional/813 conditional dialect cxxflags root only migrated (batch 5); script removed 814_the_llvm_row_defaults_to_the_dynamic_crt.sh windows merge e2e/windows 815_the_database_describes_what_a_rule_generates.sh cdb merge e2e/cdb 816_a_git_dependency_reports_its_fetch.sh deps merge e2e/deps::git/816 a git dependency reports its fetch migrated (batch 3); script removed @@ -580,7 +580,7 @@ script subsystem class destination note 892_coroutines_on_the_32_bit_msvc_abi_are_explained.sh windows merge e2e/windows 893_the_workspace_prefix_carries_the_meaning.sh workspace merge e2e/workspace 894_the_position_reading_is_kept_and_reported.sh build merge e2e/build -895_conditional_configuration_rows_count_as_they_evaluate.sh features merge e2e/features +895_conditional_configuration_rows_count_as_they_evaluate.sh features merge e2e/features::conditional/895 conditional configuration rows count as they evaluate migrated (batch 5); script removed 896_an_action_output_has_one_writer.sh actions merge e2e/actions::outputs/896 an action output has one writer migrated (batch 4); script removed 897_each_configuration_has_its_own_generated_output.sh actions merge e2e/actions::outputs/897 each configuration has its own generated output migrated (batch 4); script removed 898_module_units_in_cpp_files.sh modules merge e2e/modules @@ -677,19 +677,19 @@ script subsystem class destination note 1004_an_external_toolchain_finds_mcpps_ninja.sh toolchain merge e2e/toolchain 1005_clean_stale_says_what_it_kept.sh cache merge e2e/cache 1006_a_response_file_and_what_it_names_are_link_inputs.sh build merge e2e/build -1007_a_requirement_a_layer_decides_is_checked.sh features merge e2e/features +1007_a_requirement_a_layer_decides_is_checked.sh features merge e2e/features::layers/1007 a requirement a layer decides is checked migrated (batch 5); script removed 1008_a_provided_format_is_told_the_compression.sh pack merge e2e/pack 1009_cache_gc_asks_the_runner.sh openkal merge e2e/openkal 1010_the_std_module_takes_the_platforms_flags.sh toolchain merge e2e/toolchain 1011_accelerator_requests_are_complete_and_canonical.sh build-program merge e2e/build-program::accel/1011 accelerator requests are complete and canonical migrated (batch 2); script removed -1012_capabilities_bind_in_each_consumer_context.sh features merge e2e/features +1012_capabilities_bind_in_each_consumer_context.sh features merge e2e/features::activation/1012 capabilities bind in each consumer context migrated (batch 5); script removed 1013_optional_action_progress_preserves_outputs.sh actions merge e2e/actions::progress/1013 optional action progress preserves outputs migrated (batch 4); script removed 1014_host_only_layers_do_not_select_the_target_abi.sh target merge e2e/target 1015_action_progress_paths_are_utf8_on_every_host.sh actions merge e2e/actions::progress/1015 action progress paths are utf8 on every host migrated (batch 4); script removed 1016_a_killed_build_does_not_race_the_next_one.sh cli merge e2e/cli 1017_a_system_msvc_builds_the_build_program_of_a_cross_target.sh windows merge e2e/windows 1018_a_package_and_a_namespaced_package_of_one_name_both_link.sh deps merge e2e/deps::graph/1018 a package and a namespaced package of one name both link migrated (batch 3); script removed -1019_a_later_edge_walks_the_feature_deps_it_activates.sh features merge e2e/features +1019_a_later_edge_walks_the_feature_deps_it_activates.sh features merge e2e/features::activation/1019 a later edge walks the feature deps it activates migrated (batch 5); script removed 1020_a_platform_version_group_chooses_the_version.sh openkal merge e2e/openkal 1021_a_platform_series_is_locked_and_skips_newer_engines.sh openkal merge e2e/openkal 1022_a_taken_over_directory_keeps_no_compiler_record_of_another.sh cache merge e2e/cache @@ -715,7 +715,7 @@ script subsystem class destination note 1042_each_flag_names_its_layer_and_a_request_its_selection.sh target merge e2e/target 1043_several_programs_and_a_dotted_name_are_packed_as_stated.sh pack merge e2e/pack 1044_clang_pairs_with_the_mingw_payload.sh windows keep keep emulator: runs the clang/mingw program under wine -1045_a_required_capability_takes_its_default_provider.sh features merge e2e/features +1045_a_required_capability_takes_its_default_provider.sh features merge e2e/features::defaults/1045 a required capability takes its default provider migrated (batch 5); script removed 1046_a_patch_made_with_git_diff_from_the_package_root_is_applied.sh deps merge e2e/deps::edges/1046 a patch made with git diff from the package root is applied migrated (batch 3); script removed 1047_an_artifact_compiles_its_shared_sources_with_its_own_flags.sh build merge e2e/build 1048_a_git_reference_follows_the_default_branch.sh deps merge e2e/deps::git/1048 a git reference follows the default branch migrated (batch 3); script removed diff --git a/tests/e2e/features/activation.cpp b/tests/e2e/features/activation.cpp new file mode 100644 index 000000000..d953437d4 --- /dev/null +++ b/tests/e2e/features/activation.cpp @@ -0,0 +1,750 @@ +// Feature activation: `backend=` sugar and the --strict schema gate, required_features, +// feature defines and their propagation, capabilities, feature-deps, a dependency's +// required_features, exclusive capabilities, a feature-deps shared +// library, restated feature-deps, a dependency's feature on the command line, a private +// feature-dep, capabilities bound in each consumer context, a later edge walking the +// feature-deps it activates (formerly tests/e2e/67, 71, 80, 81, 82, 83, 308, 601, 615, 711, 714, +// 739, 1012, 1019). +import std; +import mcpplibs.mtest; +import mcpp.test.support; + +using namespace mcpplibs::mtest; +namespace s = mcpp::test::support; +namespace fs = std::filesystem; + +namespace { + +bool failed(const e2e::outcome& r) { return r.started && r.status != 0; } + +const s::options cdb{.env = {{"MCPP_COMPILE_COMMANDS", "compile_commands.json"}}}; + +// `find target \( -name app -o -name app.exe \) -type f | head -1`, run. +e2e::outcome run_app(const s::project& p) { + const auto bin = p.artifact(s::exe("app")); + expect(fatal(bin.has_value())) << "no app binary"; + return s::run(*bin); +} + +E2E activation{"activation", [] { + "67 features strict"_test = [] { + // Feature activation, `backend=` sugar, and the --strict schema gate: an unknown feature + // or platform warns, and --strict refuses; the six platform names are known. + const auto tmp = temp_dir(); + s::project{tmp / "widget", {{"mcpp.toml", "[package]\nname = \"widget\"\nversion = \"0.1.0\"\n\n[targets.widget]\nkind = \"lib\"\n\n[features]\ndefault = []\nbackend-a = []\nbackend-b = []\n"}, + {"src/widget.cppm", "export module widget;\nexport int widget_backend() {\n#if defined(MCPP_FEATURE_BACKEND_A)\n return 1;\n#elif defined(MCPP_FEATURE_BACKEND_B)\n return 2;\n#else\n return 0;\n#endif\n}\n"}}}; + s::project app{tmp / "app", {{"src/main.cpp", "import std;\nimport widget;\nint main() {\n#ifdef MCPP_FEATURE_BASE\n std::println(\"base\");\n#endif\n#ifdef MCPP_FEATURE_EXTRA\n std::println(\"extra\");\n#endif\n std::println(\"backend={}\", widget_backend());\n return 0;\n}\n"}}}; + auto manifest = [&](std::string_view platforms, std::string_view backend) { + app.write("mcpp.toml", std::format("[package]\nname = \"app\"\nversion = \"0.1.0\"\nplatforms = [{}]\n\n[features]\ndefault = [\"base\"]\nbase = []\nextra = []\n\n[dependencies]\nwidget = {{ path = \"../widget\", backend = \"{}\" }}\n", platforms, backend)); + }; + const std::string three = "\"linux\", \"macos\", \"windows\""; + manifest(three, "a"); + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "build failed"; + expect(e2e::contains(run_app(app).out, "backend=1")) << "backend=a not activated"; + app.remove("target"); + expect(fatal(e2e::succeeded(app.mcpp({"build", "--features", "extra"})))) << "features build failed"; + expect(e2e::contains(run_app(app).out, "extra")) << "--features extra not active"; + app.remove("target"); + auto b3 = app.mcpp({"build", "--features", "nosuch"}); + expect(fatal(e2e::succeeded(b3))) << "warn path must not fail"; + expect(e2e::contains(s::text(b3), "does not declare")) << "missing unknown-feature warning"; + auto b4 = app.mcpp({"build", "--features", "nosuch", "--strict"}); + expect(fatal(failed(b4))) << "--strict must fail on unknown feature"; + expect(e2e::contains(s::text(b4), "does not declare")) << "missing strict error text"; + manifest(three, "zzz"); + app.remove("target"); + auto b5 = app.mcpp({"build"}); + expect(fatal(e2e::succeeded(b5))) << "unknown-backend warn path must not fail"; + expect(e2e::contains(s::text(b5), "does not declare requested feature 'backend-zzz'")) << "missing unknown-backend warning"; + manifest("\"linux\", \"amiga\"", "a"); + app.remove("target"); + auto b6 = app.mcpp({"build"}); + expect(fatal(e2e::succeeded(b6))) << "platform warn path must not fail"; + expect(e2e::contains(s::text(b6), "unknown platform 'amiga'")) << "missing platform warning"; + auto b7 = app.mcpp({"build", "--strict"}); + expect(fatal(failed(b7))) << "--strict must fail on unknown platform"; + expect(e2e::contains(s::text(b7), "expected: linux | macos | windows | ios | android | emscripten")) << "the refusal must list the six platform names"; + manifest("\"linux\", \"ios\", \"android\", \"emscripten\"", "a"); + app.remove("target"); + auto b8 = app.mcpp({"build"}); + expect(fatal(e2e::succeeded(b8))) << "a manifest naming the six platforms must build"; + expect(s::lacks(s::text(b8), "unknown platform")) << "a known platform name was reported as unknown"; + manifest("\"web\"", "a"); + app.remove("target"); + auto b9 = app.mcpp({"build", "--strict"}); + expect(fatal(failed(b9))) << "--strict must fail on 'web': the word is emscripten"; + expect(e2e::contains(s::text(b9), "unknown platform 'web'")) << "missing platform refusal for 'web'"; + }; + + "71 required features gate"_test[tag{"requires:gcc"}] = [] { + // A target is emitted only when all its required features are active. + s::needs({"gcc"}); + s::project p{s::files{{"src/common.cpp", "extern \"C\" int shared_answer(void) { return 0; }\n"}, + {"bin/server.cpp", "extern \"C\" int shared_answer(void);\nint main() { return shared_answer(); }\n"}, + {"bin/client.cpp", "extern \"C\" int shared_answer(void);\nint main() { return shared_answer(); }\n"}, + {"mcpp.toml", "[package]\nname = \"proj\"\nversion = \"0.1.0\"\n\n[build]\nsources = [\"src/**/*.cpp\"]\n\n[features]\nserver = []\nclient = []\n\n[targets.server]\nkind = \"bin\"\nmain = \"bin/server.cpp\"\nrequired_features = [\"server\"]\n\n[targets.client]\nkind = \"bin\"\nmain = \"bin/client.cpp\"\nrequired_features = [\"client\"]\n"}}}; + auto built = [&](std::string_view name) { return !s::glob(p.at("target"), std::format("*/*/bin/{}", name)).empty(); }; + expect(fatal(e2e::succeeded(p.mcpp({"build", "--features", "server"})))) << "server build failed"; + expect(built("server")) << "server binary not built under --features server"; + expect(!built("client")) << "client binary should be gated out under --features server"; + expect(fatal(e2e::succeeded(p.mcpp({"build", "--features", "client"})))) << "client build failed"; + expect(built("client")) << "client binary not built under --features client"; + }; + + "80 feature defines"_test = [] { + // A feature's `defines` land on the package's compile flags when it is active only. + s::project app{s::files{{"mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[features]\ndefault = []\naccel = { defines = [\"APP_ACCEL=1\"] }\n"}, + {"src/main.cpp", "int main() {\n#ifdef APP_ACCEL\n return 0;\n#else\n return 1;\n#endif\n}\n"}}}; + expect(fatal(e2e::succeeded(app.mcpp({"build"}, cdb)))) << "baseline build failed"; + expect(fatal(s::lacks(app.read("compile_commands.json"), "APP_ACCEL"))) << "APP_ACCEL present without the feature active"; + app.remove("target"); + app.remove("compile_commands.json"); + expect(fatal(e2e::succeeded(app.mcpp({"build", "--features", "accel"}, cdb)))) << "feature build failed"; + expect(e2e::contains(app.read("compile_commands.json"), "APP_ACCEL")) << "APP_ACCEL missing with --features accel"; + expect(e2e::contains(app.read("compile_commands.json"), "MCPP_FEATURE_ACCEL")) << "MCPP_FEATURE_ACCEL missing"; + }; + + "81 capability binding"_test = [] { + // A feature's `requires` binds exactly one provider of the graph: none is an error, one + // binds, two unpinned are an error listing both; [capabilities] and --cap pin one; an + // inactive feature requires nothing. + const auto tmp = temp_dir(); + for (std::string_view impl : {"alpha", "beta"}) { + s::project blas{tmp / std::format("blas_{}", impl), {}}; + blas.write("mcpp.toml", std::format("[package]\nname = \"blas_{0}\"\nversion = \"0.1.0\"\nprovides = [\"blas\"]\n\n[targets.blas_{0}]\nkind = \"lib\"\n", impl)); + blas.write(std::format("src/blas_{}.cppm", impl), std::format("export module blas_{0};\nexport int blas_{0}() {{ return 0; }}\n", impl)); + } + s::project app{tmp / "app", {{"src/main.cpp", "int main() { return 0; }\n"}}}; + auto manifest = [&](std::string_view deps) { + app.write("mcpp.toml", std::format("[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[features]\ndefault = []\nuse_blas = {{ requires = [\"blas\"] }}\n\n[dependencies]\n{}\n", deps)); + app.remove("target"); + }; + const std::string both = "blas_alpha = { path = \"../blas_alpha\" }\nblas_beta = { path = \"../blas_beta\" }"; + manifest("blas_alpha = { path = \"../blas_alpha\" }"); + expect(e2e::succeeded(app.mcpp({"build", "--features", "use_blas"}))) << "case1: single provider should bind"; + manifest("# no provider"); + auto c2 = app.mcpp({"build", "--features", "use_blas"}); + expect(fatal(failed(c2))) << "case2: missing provider must error"; + expect(s::has_line_nocase(s::text(c2), "no package provides .*blas|capability 'blas'")) << "case2: wrong/no error text"; + manifest(both); + auto c3 = app.mcpp({"build", "--features", "use_blas"}); + expect(fatal(failed(c3))) << "case3: ambiguous providers must error"; + expect(s::contains_nocase(s::text(c3), "blas_alpha") && s::contains_nocase(s::text(c3), "blas_beta")) << "case3: error must list candidates"; + app.write("mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[features]\ndefault = []\nuse_blas = { requires = [\"blas\"] }\n\n[capabilities]\nblas = \"blas_beta\"\n\n[dependencies]\nblas_alpha = { path = \"../blas_alpha\" }\nblas_beta = { path = \"../blas_beta\" }\n"); + app.remove("target"); + expect(e2e::succeeded(app.mcpp({"build", "--features", "use_blas"}))) << "case4: pin should resolve ambiguity"; + manifest("# no provider"); + expect(e2e::succeeded(app.mcpp({"build"}))) << "case5: inactive feature must not require capability"; + manifest(both); + expect(e2e::succeeded(app.mcpp({"build", "--features", "use_blas", "--cap", "blas=blas_alpha"}))) << "case6: --cap pin should resolve ambiguity"; + }; + + "82 feature optional deps"_test = [] { + // A dependency declared under a feature is pulled only when the feature is active. + const auto tmp = temp_dir(); + s::project{tmp / "widget", {{"mcpp.toml", "[package]\nname = \"widget\"\nversion = \"0.1.0\"\n\n[targets.widget]\nkind = \"lib\"\n"}, + {"src/widget.cppm", "export module widget;\nexport int widget_answer() { return 42; }\n"}}}; + s::project app{tmp / "app", {{"src/main.cpp", "int main() { return 0; }\n"}, + {"mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[features]\ndefault = []\nextra = []\n\n# `widget` is declared ONLY under the feature, so it is optional: resolved only\n# when --features extra is active.\n[feature-deps.extra]\nwidget = { path = \"../widget\" }\n"}}}; + auto b1 = app.mcpp({"build", "-v"}); + expect(fatal(e2e::succeeded(b1))) << "baseline build failed"; + expect(s::no_line(s::text(b1), "Compiling widget ")) << "widget must NOT be pulled when feature inactive"; + app.remove("target"); + auto b2 = app.mcpp({"build", "-v", "--features", "extra"}); + expect(fatal(e2e::succeeded(b2))) << "feature build failed (widget not pulled?)"; + expect(s::has_line(s::text(b2), "Compiling widget ")) << "widget was not pulled when feature active"; + }; + + "83 feature defines propagate"_test = [] { + // A dependency's active-feature `defines` reach the consumer's own translation units. + const auto tmp = temp_dir(); + s::project{tmp / "widget", {{"mcpp.toml", "[package]\nname = \"widget\"\nversion = \"0.1.0\"\n\n[features]\ndefault = []\nturbo = { defines = [\"WIDGET_TURBO=1\"] }\n\n[build]\ninclude_dirs = [\"include\"]\n\n[targets.widget]\nkind = \"lib\"\n"}, + {"include/widget/widget.hpp", "#pragma once\n// The macro's value is only meaningful in the TU that includes this header —\n// exactly the header-only library situation.\ninline int widget_mode() {\n#ifdef WIDGET_TURBO\n return 1;\n#else\n return 0;\n#endif\n}\n"}, + {"src/widget.cppm", "export module widget;\nexport int widget_anchor() { return 0; }\n"}}}; + s::project app{tmp / "app", {{"mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies]\nwidget = { path = \"../widget\", features = [\"turbo\"] }\n"}, + {"src/main.cpp", "#include \n#ifndef WIDGET_TURBO\n#error \"WIDGET_TURBO did not propagate from widget[turbo] to the consumer\"\n#endif\nint main() { return widget_mode() == 1 ? 0 : 2; }\n"}}}; + expect(fatal(e2e::succeeded(app.mcpp({"build"}, cdb)))) << "feature define did not propagate to consumer"; + expect(e2e::contains(app.read("compile_commands.json"), "WIDGET_TURBO")) << "WIDGET_TURBO missing from consumer compile_commands.json"; + expect(e2e::succeeded(run_app(app))) << "binary did not observe WIDGET_TURBO"; + }; + "308 dependency required features"_test[tag{"requires:elf"}] = [] { + // A dependency's `[targets.] required_features` gates its target for every consumer: + // closed without the feature (the program still runs), open with it. + s::needs({"elf"}); + const auto tmp = temp_dir(); + s::project{tmp / "dep", {{"mcpp.toml", R"cpp([package] +name = "dep" +version = "0.1.0" +[build] +c_standard = "c11" +sources = ["src/*.c"] +[features] +plugin = [] +[targets.dep] +kind = "lib" +[targets.depshared] +kind = "shared" +required_features = ["plugin"] +)cpp"}, {"src/dep.c", R"cpp(int dep_value(void) { return 5; } +)cpp"}}}; + s::project app{tmp / "app", {{"mcpp.toml", R"cpp([package] +name = "app" +version = "0.1.0" +[build] +c_standard = "c11" +[targets.app] +kind = "bin" +main = "src/main.c" +[dependencies] +dep = { path = "../dep" } +)cpp"}, {"src/main.c", R"cpp(#include +int dep_value(void); +int main(void) { printf("%d\n", dep_value()); return 0; } +)cpp"}}}; + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "the feature-off build failed"; + expect(fatal(s::find_files(app.at("target"), R"(libdepshared\.so)").empty())) << "a feature-gated dependency target was built with the feature off"; + const auto bin = s::find_file(app.at("target"), "app"); + expect(fatal(bin.has_value())) << "no executable"; + expect(fatal(s::has_line(s::run(*bin).out, "^5$"))) << "the gated-off build does not run"; + app.write("mcpp.toml", R"cpp([package] +name = "app" +version = "0.1.0" +[build] +c_standard = "c11" +[targets.app] +kind = "bin" +main = "src/main.c" +[dependencies] +dep = { path = "../dep", features = ["plugin"] } +)cpp"); + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "the feature-on build failed"; + expect(s::find_file(app.at("target"), "libdepshared.so").has_value()) << "requesting the feature did not build the gated target"; + }; + + "601 exclusive capability"_test = [] { + // Two exclusive providers of one capability are refused at binding, naming it, both and + // why; two ordinary providers coexist. + const auto parent = temp_dir() / "new"; + fs::create_directories(parent); + auto mkpkg = [&](std::string_view name, std::string_view cap, bool exclusive) { + s::project p{parent / name, {}}; + p.write("mcpp.toml", std::format("[package]\nname = \"{0}\"\nversion = \"0.1.0\"\nprovides = [\"{1}\"]\n{2}[language]\nstandard = \"c++23\"\n[targets.{0}]\nkind = \"lib\"\n", name, cap, exclusive ? std::format("exclusive = [\"{}\"]\n", cap) : std::string{})); + std::string ident{name}; + std::ranges::replace(ident, '-', '_'); + p.write(std::format("src/{}.c", name), std::format("int {}_anchor(void) {{ return 0; }}\n", ident)); + }; + auto consumer = [&](std::vector deps) { + fs::remove_all(parent / "app"); + auto app = s::project::scaffold("app"); + std::string m = "[package]\nname = \"app\"\nversion = \"0.1.0\"\n[language]\nstandard = \"c++23\"\n[dependencies]\n"; + for (auto d : deps) m += std::format("{0} = {{ path = \"../{0}\" }}\n", d); + app.write("mcpp.toml", m); + for (const auto& f : s::glob(app.at("src"), "*.cppm")) fs::remove(f); + app.write("src/main.cpp", "int main(){return 0;}\n"); + return app; + }; + mkpkg("gpublas-a", "gpu-blas", true); + mkpkg("gpublas-b", "gpu-blas", true); + auto out = consumer({"gpublas-a", "gpublas-b"}).mcpp({"build"}); + expect(fatal(failed(out))) << "two exclusive providers of 'gpu-blas' were accepted"; + expect(e2e::contains(s::text(out), "gpu-blas")) << "refusal does not name the capability"; + expect(e2e::contains(s::text(out), "gpublas-a")) << "refusal does not name the first provider"; + expect(e2e::contains(s::text(out), "gpublas-b")) << "refusal does not name the second provider"; + expect(s::contains_nocase(s::text(out), "exclusive")) << "refusal does not say why"; + mkpkg("blas-a", "blas", false); + mkpkg("blas-b", "blas", false); + expect(e2e::succeeded(consumer({"blas-a", "blas-b"}).mcpp({"build"}))) << "two ordinary providers were refused"; + }; + "615 feature deps shared library is linked"_test[tag{"requires:gcc"}][tag{"requires:unix-shell"}] = [] { + // A shared library a library package acquires through [feature-deps] reaches the link + // line of its test binaries; the project builds without the feature too. + s::needs({"gcc", "unix-shell"}); + const auto tmp = temp_dir(); + s::project{tmp / "greeter", {{"mcpp.toml", R"cpp([package] +name = "greeter" +version = "0.1.0" + +[build] +sources = ["src/greeter.cpp"] + +[targets.greeter] +kind = "shared" +)cpp"}, {"src/greeter.cpp", R"cpp(extern "C" const char* greeter_line() { return "greeter-from-the-shared-library"; } +)cpp"}}}; + s::project app{tmp / "app", {{"mcpp.toml", R"cpp([package] +name = "app" +version = "0.1.0" + +[build] +sources = ["src/lib.cpp"] + +[targets.app] +kind = "lib" + +[features] +default = {} +loud = { defines = ["USE_GREETER"] } + +[feature-deps.loud] +greeter = { path = "../greeter" } +)cpp"}, {"src/lib.cpp", R"cpp(int app_anchor() { return 0; } +)cpp"}, {"tests/use.cpp", R"cpp(#include +#ifdef USE_GREETER +extern "C" const char* greeter_line(); +#endif +int main() { +#ifdef USE_GREETER + std::printf("%s\n", greeter_line()); +#else + std::printf("quiet\n"); +#endif + return 0; +} +)cpp"}}}; + const auto loud = s::text(app.mcpp({"test", "use", "--features", "loud"})); + app.remove("target"); + const auto quiet = s::text(app.mcpp({"test", "use"})); + expect(e2e::contains(loud, "greeter-from-the-shared-library")) << "the feature's shared library links and runs"; + expect(e2e::contains(quiet, "quiet")) << "and the same project builds without the feature"; + }; + "711 a feature deps restatement names one source"_test[tag{"requires:gcc"}] = [] { + // A [feature-deps.] entry adding `tools` to a dependency declared unconditionally + // restates its source; another source (or constraint) is refused naming both; a build + // program addresses a namespaced package's tool by both spellings, only with the feature. + s::needs({"gcc"}); + const auto tmp = temp_dir(); + const s::options linked{.where = s::home::linked}; + for (std::string_view d : {"installer", "other"}) + s::project{tmp / d, {{"mcpp.toml", R"cpp([package] +name = "installer" +namespace = "spike" +version = "0.1.0" + +[targets.installer] +kind = "bin" +main = "src/main.cpp" +)cpp"}, {"src/main.cpp", R"cpp(#include +int main() { std::puts("installer ran"); return 0; } +)cpp"}}}; + s::project app{tmp / "app", {{"mcpp.toml", R"cpp([package] +name = "app" +version = "0.1.0" + +[targets.app] +kind = "bin" +main = "src/main.cpp" + +[dependencies] +spike.installer = { path = "../installer" } + +[features] +installer = [] + +[feature-deps.installer] +spike.installer = { tools = ["installer"] } +)cpp"}, {"src/main.cpp", "int main() { return 0; }\n"}, {"build.mcpp", R"cpp(import std; +import mcpp; +int main() { + const char* a = mcpp::dep_bin("installer", "installer"); + const char* b = mcpp::dep_bin("spike.installer", "installer"); + mcpp::warning((std::string("DEPBIN short=[") + (a ? a : "") + "] qualified=[" + + (b ? b : "") + "]").c_str()); + return 0; +} +)cpp"}}}; + auto replace = [&](std::string_view from, std::string_view to) { + auto m = app.read("mcpp.toml"); + expect(fatal(m.find(from) != std::string::npos)) << "the fixture line is missing: " << from; + m.replace(m.find(from), from.size(), to); + app.write("mcpp.toml", m); + }; + auto b1 = app.mcpp({"build", "--features", "installer"}, linked); + expect(fatal(failed(b1))) << "a source-less [feature-deps] entry was accepted"; + expect(fatal(e2e::contains(s::text(b1), "source"))) << "the refusal does not say to restate the source"; + replace("spike.installer = { tools = [\"installer\"] }", "spike.installer = { path = \"../installer\", tools = [\"installer\"] }"); + auto b2 = app.mcpp({"build", "--strict", "--features", "installer"}, linked); + expect(fatal(e2e::succeeded(b2))) << "the restated feature-deps entry was refused under --strict"; + expect(fatal(s::has_line(s::text(b2), R"(DEPBIN short=\[[^\]])"))) << "dep_bin(\"installer\") read nothing"; + expect(fatal(s::has_line(s::text(b2), R"(qualified=\[[^\]])"))) << "dep_bin(\"spike.installer\") read nothing for a namespace + name package"; + app.remove("target"); + auto b3 = app.mcpp({"build", "--strict"}, linked); + expect(fatal(e2e::succeeded(b3))) << "the build without the feature failed"; + expect(fatal(e2e::contains(s::text(b3), "DEPBIN short=[] qualified=[]"))) << "the tool was provided without its feature"; + replace("spike.installer = { path = \"../installer\", tools", "spike.installer = { path = \"../other\", tools"); + app.remove("target"); + auto b4 = app.mcpp({"build", "--strict", "--features", "installer"}, linked); + expect(fatal(failed(b4))) << "a restatement naming another path was accepted"; + expect(fatal(e2e::contains(s::text(b4), "../other") && e2e::contains(s::text(b4), "../installer"))) << "the refusal does not name both sources"; + s::project spell{tmp / "spell", {{"src/main.cpp", "int main() { return 0; }\n"}}}; + const s::options offline{.where = s::home::linked, .env = {{"MCPP_OFFLINE", "1"}}}; + auto write_spell = [&](std::string_view restated) { spell.write("mcpp.toml", std::format(R"cpp([package] +name = "spellprobe" +version = "0.1.0" +description = "two spellings of one constraint" +license = "Apache-2.0" +authors = ["mcpp"] + +[language] +standard = "c++23" + +[features] +extra = [] + +[dependencies] +mcpplibs.nonexistent-spell-probe = ">=9.9.9" + +[feature-deps.extra] +mcpplibs.nonexistent-spell-probe = {{ version = "{0}", reexport = true }} + +[targets.spellprobe] +kind = "bin" +main = "src/main.cpp" +)cpp", restated)); }; + write_spell(">= 9.9.9"); + auto s1 = spell.mcpp({"build", "--features", "extra"}, offline); + expect(fatal(s::lacks(s::text(s1), "restates the dependency"))) << "a restatement differing from the declaration only in whitespace was refused"; + write_spell(">=9.9.8"); + spell.remove("target"); + auto s2 = spell.mcpp({"build", "--features", "extra"}, offline); + expect(fatal(failed(s2))) << "a restatement naming another constraint was accepted"; + expect(e2e::contains(s::text(s2), "restates the dependency")) << "a restatement naming another constraint was not refused"; + }; + + "714 features names a dependency feature"_test[tag{"requires:gcc"}] = [] { + // `--features /` opens a feature of a dependency (no root macro), a + // plain name without a [features] table is accepted, an unknown dependency is refused + // under --strict; beside a [features] table too, and in `mcpp why deps`. + s::needs({"gcc"}); + const auto tmp = temp_dir(); + const s::options linked{.where = s::home::linked}; + s::project{tmp / "fw", {{"mcpp.toml", R"cpp([package] +name = "fw" +namespace = "spike" +version = "0.1.0" + +[targets.fw] +kind = "lib" + +[build] +sources = ["src/*.cpp"] + +[features] +installer = [] + +[feature-deps.installer] +spike.fw-installer = { path = "tool", tools = ["fw-installer"], reexport = true } +)cpp"}, {"src/fw.cpp", "int fw_answer() { return 42; }\n"}, {"tool/mcpp.toml", R"cpp([package] +name = "fw-installer" +namespace = "spike" +version = "0.1.0" + +[targets.fw-installer] +kind = "bin" +main = "src/main.cpp" +)cpp"}, {"tool/src/main.cpp", "int main() { return 0; }\n"}}}; + s::project app{tmp / "app", {{"mcpp.toml", R"cpp([package] +name = "app" +version = "0.1.0" + +[targets.app] +kind = "bin" +main = "src/main.cpp" + +[dependencies] +spike.fw = { path = "../fw" } +)cpp"}, {"src/main.cpp", R"cpp(int fw_answer(); +int main() { return fw_answer() == 42 ? 0 : 1; } +)cpp"}, {"build.mcpp", R"cpp(import std; +import mcpp; +int main() { + const char* a = mcpp::dep_bin("spike.fw-installer", "fw-installer"); + mcpp::warning((std::string("DEPBIN tool=[") + (a ? a : "") + "]").c_str()); + return 0; +} +)cpp"}}}; + auto b1 = app.mcpp({"build", "--features", "spike.fw/installer"}, linked); + expect(fatal(e2e::succeeded(b1))) << "the dependency feature build failed"; + expect(fatal(s::has_line(s::text(b1), R"(DEPBIN tool=\[[^\]])"))) << "--features spike.fw/installer did not open the feature"; + std::optional nj; + for (const auto& f : s::find_files(app.at("target"), R"(build\.ninja)")) + if (!nj && f.generic_string().find("/.build-mcpp/") == std::string::npos) nj = f; + expect(fatal(nj.has_value())) << "no build.ninja"; + expect(fatal(s::lacks(s::read_file(*nj), "MCPP_FEATURE_SPIKE_FW_INSTALLER"))) << "the dependency token became a macro of the root"; + app.remove("target"); + expect(fatal(e2e::succeeded(app.mcpp({"build", "--strict", "--features", "nothing-here"}, linked)))) << "a plain name without a [features] table was refused"; + auto b3 = app.mcpp({"build", "--strict", "--features", "nope/x"}, linked); + expect(fatal(failed(b3))) << "--features nope/x was accepted under --strict"; + expect(fatal(e2e::contains(s::text(b3), "nope"))) << "the refusal does not name the token"; + app.write("mcpp.toml", app.read("mcpp.toml") + R"cpp( +[features] +windows-installer = [] +)cpp"); + app.remove("target"); + auto b4 = app.mcpp({"build", "--strict", "--features", "spike.fw/installer"}, linked); + expect(fatal(e2e::succeeded(b4))) << "the dependency token was refused beside a [features] table"; + expect(fatal(s::has_line(s::text(b4), R"(DEPBIN tool=\[[^\]])"))) << "the feature did not open beside a [features] table"; + auto w1 = app.mcpp({"why", "deps", "--features", "spike.fw/installer"}, linked); + expect(fatal(e2e::succeeded(w1))) << "why deps --features failed"; + expect(e2e::contains(s::text(w1), "spike.fw-installer")) << "why deps does not show the feature's dependency"; + }; + + "739 a private feature dep does not reach the consumer"_test = [] { + // A platform SDK under [feature-deps.] with visibility = "private" reaches only the + // declaring package's own units: the consumer fails on the missing header, the declaring + // package builds, and with "public" the consumer builds (the harness is not vacuous). + const auto tmp = temp_dir(); + s::project{tmp / "sdk", {{"mcpp.toml", R"cpp([package] +name = "sdk" +version = "0.1.0" + +[targets.sdk] +kind = "lib" +)cpp"}, {"include/sdk.h", R"cpp(#pragma once +#define SDK_PRESENT 1 +inline int sdk_value(void) { return 42; } +)cpp"}, {"src/empty.cpp", R"cpp(// no compiled sources of its own beyond the header +)cpp"}}}; + s::project lib{tmp / "lib_platform", {{"src/lib_platform.cpp", R"cpp(#include "sdk.h" +#ifndef SDK_PRESENT +#error "lib_platform's own translation unit must see sdk's headers" +#endif +int lib_platform_uses_sdk(void) { return sdk_value(); } +)cpp"}}}; + auto manifest = [&](std::string_view visibility) { lib.write("mcpp.toml", std::format(R"cpp([package] +name = "lib_platform" +version = "0.1.0" + +[targets.lib_platform] +kind = "lib" + +[features] +plat = {{}} + +[feature-deps.plat] +sdk = {{ path = "../sdk", visibility = "{0}" }} +)cpp", visibility)); }; + s::project app{tmp / "app", {{"mcpp.toml", R"cpp([package] +name = "app" +version = "0.1.0" + +[dependencies] +lib_platform = { path = "../lib_platform", features = ["plat"] } +)cpp"}, {"src/main.cpp", R"cpp(#include "sdk.h" +int main() { return 0; } +)cpp"}}}; + manifest("private"); + auto a1 = app.mcpp({"build", "--cache", "off"}); + expect(fatal(failed(a1))) << "A: app built despite depending (via a private feature-dep) on a package whose headers it must not see"; + const auto a1_text = s::text(a1); + const auto at_sdk = a1_text.find("sdk.h"); + const bool missing = at_sdk != std::string::npos + && std::ranges::any_of(std::array{"No such file", "not found", "cannot open"}, + [&](const char* m) { return a1_text.find(m, at_sdk) != std::string::npos; }); + expect(fatal(missing)) << "A: app's build failed, but not on a missing sdk.h — cannot tell privacy from an unrelated break"; + expect(fatal(e2e::succeeded(lib.mcpp({"build", "--features", "plat", "--cache", "off"})))) << "A: lib_platform (the declaring package) could not build against its own private feature-dep"; + manifest("public"); + expect(e2e::succeeded(app.mcpp({"build", "--cache", "off"}))) << "B: app failed to build once sdk is a PUBLIC feature-dep of lib_platform"; + }; + "1012 capabilities bind in each consumer context"_test[tag{"requires:gcc"}][tag{"requires:unix-shell"}][tag{"requires:python3"}] = [] { + // Independent products may select different providers; a library requirement still + // accepts a provider supplied by its final application's sibling edge, and each refusal + // names the consumer whose closure it is about. + s::needs({"gcc", "unix-shell", "python3"}); + const s::options o{.where = s::home::inherited}; + s::project ws{s::files{{"mcpp.toml", R"cpp([workspace] +members = ["app-a", "app-b", "helper", "provider-a", "provider-b"] +)cpp"}}}; + for (std::string_view suffix : {"a", "b"}) { + ws.write(std::format("provider-{}/mcpp.toml", suffix), std::format(R"cpp([package] +name = "provider-{0}" +version = "0.1.0" +provides = ["probe-interface"] +exclusive = ["probe-interface"] +[targets.provider-{0}] +kind = "lib" +)cpp", suffix)); + ws.write(std::format("provider-{}/src/anchor.c", suffix), std::format("int provider_{}(void){{return 0;}}\n", suffix)); + ws.write(std::format("app-{}/mcpp.toml", suffix), std::format(R"cpp([package] +name = "app-{0}" +version = "0.1.0" +[dependencies] +provider-{0} = {{ path = "../provider-{0}" }} +helper = {{ path = "../helper" }} +[targets.app-{0}] +kind = "bin" +main = "src/main.cpp" +)cpp", suffix)); + ws.write(std::format("app-{}/src/main.cpp", suffix), "int main(){return 0;}\n"); + } + ws.write("helper/mcpp.toml", R"cpp([package] +name = "helper" +version = "0.1.0" +[features] +default = ["need-provider"] +need-provider = { requires = ["probe-interface"] } +[targets.helper] +kind = "lib" +)cpp"); + ws.write("helper/src/anchor.c", "int helper_anchor(void){return 0;}\n"); + // sed's line edits: the lines starting with `prefix` dropped, or `line` after the first. + auto drop = [&](std::string_view file, std::string_view prefix) { + std::istringstream in{ws.read(file)}; + std::string out; + for (std::string l; std::getline(in, l);) + if (!l.starts_with(prefix)) out += l + "\n"; + ws.write(file, out); + }; + auto replace = [&](std::string_view file, std::string_view from, std::string_view to) { + auto m = ws.read(file); + const auto at = m.find(from); + expect(fatal(at != std::string::npos)) << file << " lacks " << from; + m.replace(at, from.size(), to); + ws.write(file, m); + }; + auto build = [&](std::vector args, std::string_view in = {}) { + return in.empty() ? ws.mcpp(std::move(args), o) : ws.mcpp_in(in, std::move(args), o); + }; + + auto r = build({"build", "--workspace"}); + expect(fatal(e2e::succeeded(r))) << "independent exclusive providers collided\n" << s::text(r); + expect(fatal(e2e::succeeded(build({"run", "-p", "app-a"})))) << "app-a did not run"; + expect(fatal(e2e::succeeded(build({"run", "-p", "app-b"})))) << "app-b did not run"; + r = build({"build", "-p", "helper"}); + expect(fatal(failed(r))) << "standalone library borrowed an unselected consumer"; + expect(fatal(e2e::contains(s::text(r), "consumer 'helper'"))) << "standalone library lost its context\n" << s::text(r); + ws.write("helper/tests/context.cpp", "int main(){return 0;}\n"); + r = build({"test", "--workspace"}); + expect(fatal(failed(r))) << "library test borrowed an application provider"; + expect(fatal(e2e::contains(s::text(r), "consumer 'helper'"))) << "library test lost its own context\n" << s::text(r); + ws.remove("helper/tests"); + for (std::string_view suffix : {"a", "b"}) + ws.write(std::format("app-{}/tests/context.cpp", suffix), "int main(){return 0;}\n"); + replace("provider-a/mcpp.toml", "kind = \"lib\"", "kind = \"shared\""); + r = build({"test", "--workspace"}); + expect(fatal(e2e::succeeded(r))) << "test/shared contexts crossed independent products\n" << s::text(r); + expect(fatal(e2e::succeeded(build({"run", "-p", "app-a"})))) << "shared provider consumer did not run"; + + const auto saved = ws.read("app-a/mcpp.toml"); + drop("app-a/mcpp.toml", "provider-a ="); + r = build({"build", "--workspace"}); + expect(fatal(failed(r))) << "another product supplied a missing provider"; + expect(fatal(e2e::contains(s::text(r), "no package provides capability 'probe-interface'"))) << "wrong missing-provider diagnosis\n" << s::text(r); + expect(fatal(e2e::contains(s::text(r), "consumer 'app-a'"))) << "missing consumer context\n" << s::text(r); + ws.write("app-a/mcpp.toml", ws.read("app-a/mcpp.toml") + R"cpp([build-dependencies] +provider-a = { path = "../provider-a" } +)cpp"); + ws.write("app-a/build.mcpp", "int main(){return 0;}\n"); + r = build({"build", "--workspace"}); + expect(fatal(failed(r))) << "a host-only provider satisfied a target requirement"; + expect(fatal(e2e::contains(s::text(r), "no package provides capability 'probe-interface'"))) << "wrong host/target scope diagnosis\n" << s::text(r); + ws.remove("app-a/build.mcpp"); + + ws.write("app-a/mcpp.toml", saved + R"cpp([capabilities] +probe-interface = "provider-a" +)cpp"); + replace("app-a/mcpp.toml", "\nhelper = { path = \"../helper\" }\n", "\nhelper = { path = \"../helper\" }\nprovider-b = { path = \"../provider-b\" }\n"); + r = build({"build", "--workspace"}); + expect(fatal(failed(r))) << "pin hid an exclusive collision"; + expect(fatal(s::contains_nocase(s::text(r), "exclusive"))) << "wrong exclusive diagnosis\n" << s::text(r); + expect(fatal(e2e::contains(s::text(r), "consumer 'app-a'"))) << "exclusive diagnosis omitted consumer\n" << s::text(r); + for (std::string_view suffix : {"a", "b"}) drop(std::format("provider-{}/mcpp.toml", suffix), "exclusive ="); + r = build({"build", "--workspace"}); + expect(fatal(e2e::succeeded(r))) << "member pin did not resolve local ambiguity\n" << s::text(r); + r = build({"build", "--workspace", "--cap", "probe-interface=provider-b"}); + expect(fatal(e2e::succeeded(r))) << "CLI pin did not override the member pin\n" << s::text(r); + r = build({"build", "--workspace", "--cap", "probe-interface=provider-a"}); + expect(fatal(failed(r))) << "CLI pin escaped the second consumer closure"; + expect(fatal(e2e::contains(s::text(r), "consumer 'app-b'"))) << "invalid pin omitted its consumer\n" << s::text(r); + + for (std::string_view ns : {"first", "second"}) { + ws.write(std::format("{}/mcpp.toml", ns), std::format(R"cpp([package] +name = "implementation" +namespace = "{0}" +version = "0.1.0" +provides = ["namespaced-interface"] +[targets.implementation] +kind = "lib" +)cpp", ns)); + ws.write(std::format("{}/src/anchor.c", ns), std::format("int {}_anchor(void){{return 0;}}\n", ns)); + } + ws.write("namespaced/mcpp.toml", R"cpp([package] +name = "namespaced-consumer" +version = "0.1.0" +[features] +default = ["need"] +need = { requires = ["namespaced-interface"] } +[dependencies.first] +implementation = { path = "../first" } +[dependencies.second] +implementation = { path = "../second" } +[capabilities] +namespaced-interface = "implementation" +)cpp"); + ws.write("namespaced/src/main.cpp", "int main(){return 0;}\n"); + r = build({"build"}, "namespaced"); + expect(fatal(failed(r))) << "ambiguous short provider identity was accepted"; + expect(fatal(e2e::contains(s::text(r), "first:implementation"))) << "missing first qualified candidate\n" << s::text(r); + expect(fatal(e2e::contains(s::text(r), "second:implementation"))) << "missing second qualified candidate\n" << s::text(r); + replace("namespaced/mcpp.toml", "= \"implementation\"\n", "= \"first:implementation\"\n"); + r = build({"build"}, "namespaced"); + expect(e2e::succeeded(r)) << "qualified provider pin did not bind\n" << s::text(r); + }; + + "1019 a later edge walks the feature deps it activates"_test = [] { + // `s` asks `v` for `x`, whose [feature-deps.x] asks `a` for `f2` (#897): with `v` + // selected beside `s`, in either order, `x`'s feature-deps reach the graph and `a` is + // built with `f2`. + s::project ws{s::files{{"mcpp.toml", "[workspace]\nmembers = [\"a\", \"v\", \"s\"]\n"}, + {"a/mcpp.toml", R"cpp([package] +name = "a" +version = "0.1.0" +[targets.a] +kind = "lib" +[build] +sources = [] +[features] +f1 = { sources = ["src/f1/*.cppm"] } +f2 = { sources = ["src/f2/*.cppm"] } +)cpp"}, + {"a/src/f1/f1.cppm", "export module a.f1;\nexport int f1() { return 1; }\n"}, + {"a/src/f2/f2.cppm", "export module a.f2;\nexport int f2() { return 2; }\n"}, + {"v/mcpp.toml", R"cpp([package] +name = "v" +version = "0.1.0" +[targets.v] +kind = "lib" +[build] +sources = ["src/base/*.cppm"] +[dependencies] +a = { path = "../a", features = ["f1"] } +[features] +x = { sources = ["src/x/*.cppm"] } +[feature-deps.x] +a = { path = "../a", features = ["f2"] } +)cpp"}, + {"v/src/base/v.cppm", "export module v;\nimport a.f1;\nexport int vv() { return f1(); }\n"}, + {"v/src/x/x.cppm", "export module v.x;\nimport a.f2;\nexport int vx() { return f2(); }\n"}, + {"s/mcpp.toml", R"cpp([package] +name = "s" +version = "0.1.0" +[targets.s] +kind = "bin" +main = "src/main.cpp" +[dependencies] +v = { path = "../v", features = ["x"] } +)cpp"}, + {"s/src/main.cpp", "import std;\nimport v;\nimport v.x;\nint main() { std::println(\"sum {}\", vv() + vx()); }\n"}}}; + for (const std::vector& order : {std::vector{"-p", "s", "-p", "v"}, std::vector{"-p", "v", "-p", "s"}}) { + ws.remove("target"); + std::vector args{"build"}; + args.insert(args.end(), order.begin(), order.end()); + auto r = ws.mcpp(args); + expect(fatal(e2e::succeeded(r))) << "mcpp build " << order[1] << " then " << order[3] << "\n" << s::text(r); + } + auto r = ws.mcpp({"run", "-p", "s"}); + expect(fatal(e2e::succeeded(r))) << "the program\n" << s::text(r); + expect(e2e::contains(s::text(r), "sum 3")) << "the program's output\n" << s::text(r); + }; +}}; + +} // namespace diff --git a/tests/e2e/features/conditional.cpp b/tests/e2e/features/conditional.cpp new file mode 100644 index 000000000..c595bf11d --- /dev/null +++ b/tests/e2e/features/conditional.cpp @@ -0,0 +1,586 @@ +// Target-conditional configuration evaluated against the resolved target: build flags and +// dependencies, sources, a dependency's sources, glob flags, +// feature-deps, a bare triple on a native build, a conditional dependency +// replacing the unconditional one, a root-only conditional dialect, members' conditional rows +// (formerly tests/e2e/85, 86, 108, 119, 149, 195, 247, 677, 813, 895). +import std; +import mcpplibs.mtest; +import mcpp.test.support; + +using namespace mcpplibs::mtest; +namespace s = mcpp::test::support; +namespace fs = std::filesystem; + +namespace { + +bool failed(const e2e::outcome& r) { return r.started && r.status != 0; } + +// The build.ninja files of a build, outside the build program's own tree (`find target -name +// build.ninja -not -path '*/.build-mcpp/*'`). +std::vector ninjas(const s::project& p) { + std::vector out; + for (const auto& f : s::find_files(p.at("target"), R"(build\.ninja)")) + if (f.generic_string().find("/.build-mcpp/") == std::string::npos) out.push_back(f); + return out; +} + +// The text of a file, or nothing when it does not exist (`grep file` then reads no line). +std::string text_of(const fs::path& f) { return fs::exists(f) ? s::read_file(f) : std::string{}; } + +E2E conditional{"conditional", [] { + "85 target cfg build flags"_test = [] { + // `[target.'cfg(...)'.build]` applies exactly the host's os/arch predicates. + s::project app{s::files{{"mcpp.toml", R"cpp([package] +name = "app" +version = "0.1.0" + +[target.'cfg(linux)'.build] +cxxflags = ["-DCOND_LINUX=1"] +[target.'cfg(macos)'.build] +cxxflags = ["-DCOND_MACOS=1"] +[target.'cfg(windows)'.build] +cxxflags = ["-DCOND_WIN=1"] +[target.'cfg(unix)'.build] +cxxflags = ["-DCOND_UNIX=1"] +[target.'cfg(arch = "x86_64")'.build] +cxxflags = ["-DCOND_X64=1"] +[target.'cfg(arch = "aarch64")'.build] +cxxflags = ["-DCOND_ARM64=1"] +# Boolean combinator: any(linux, macos) == unix-family. +[target.'cfg(any(linux, macos))'.build] +cxxflags = ["-DCOND_UNIXLIKE=1"] +)cpp"}, {"src/main.cpp", R"cpp(// Exactly one OS predicate must apply (the host's), regardless of platform. +#if (defined(COND_LINUX) + defined(COND_MACOS) + defined(COND_WIN)) != 1 +#error "exactly one of cfg(linux)/cfg(macos)/cfg(windows) must apply on any host" +#endif +// cfg(unix) applies iff not windows. +#if defined(COND_WIN) && defined(COND_UNIX) +#error "cfg(unix) wrongly applied on a windows host" +#endif +#if !defined(COND_WIN) && !defined(COND_UNIX) +#error "cfg(unix) should apply on a non-windows host" +#endif +// any(linux, macos) must agree with unix on the CI platforms. +#if defined(COND_UNIXLIKE) != defined(COND_UNIX) +#error "cfg(any(linux,macos)) disagreed with cfg(unix)" +#endif +// Exactly one arch predicate must apply on the CI runners (x86_64 or aarch64). +#if (defined(COND_X64) + defined(COND_ARM64)) != 1 +#error "exactly one of cfg(arch=x86_64)/cfg(arch=aarch64) must apply" +#endif +// Mutual exclusion: the non-host OS predicate must NOT leak in. +int main() { return 0; } +)cpp"}}}; + expect(e2e::succeeded(app.mcpp({"build"}))) << "conditional cfg flags mis-applied for this host"; + }; + + "86 target cfg dependencies"_test = [] { + // `[target.'cfg(...)'.dependencies]`: a matching predicate's dependency resolves, a + // non-matching one is never resolved (its path does not exist). + const auto tmp = temp_dir(); + s::project{tmp / "widget", {{"mcpp.toml", R"cpp([package] +name = "widget" +version = "0.1.0" +[targets.widget] +kind = "lib" +)cpp"}, {"src/widget.cppm", R"cpp(export module widget; +export int widget_anchor() { return 0; } +)cpp"}}}; + s::project app{tmp / "app", {{"mcpp.toml", R"cpp([package] +name = "app" +version = "0.1.0" + +# widget is pulled under whichever family matches the host — so exactly one of +# these applies and widget is ALWAYS resolved (on any of the 3 platforms). +[target.'cfg(unix)'.dependencies] +widget = { path = "../widget" } +[target.'cfg(windows)'.dependencies] +widget = { path = "../widget" } + +# A never-matching predicate (no such arch) points at a non-existent path. If the +# conditional merge wrongly pulled it, resolution of the bogus path would FAIL the +# build — so a clean build proves non-matching deps are excluded. +[target.'cfg(arch = "no_such_arch")'.dependencies] +ghost = { path = "../this_path_does_not_exist" } +)cpp"}, {"src/main.cpp", R"cpp(import widget; // fails to build unless the cfg-matched widget dep was resolved +int main() { return widget_anchor(); } +)cpp"}}}; + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "conditional dep not resolved, or non-matching dep wrongly pulled"; + if (app.exists("mcpp.lock")) { + expect(e2e::contains(app.read("mcpp.lock"), "widget")) << "widget missing from lockfile"; + expect(s::lacks(app.read("mcpp.lock"), "ghost")) << "non-matching cfg dependency 'ghost' leaked into the resolve"; + } + }; + "108 cfg conditional sources"_test[tag{"requires:gcc"}] = [] { + // `[target.'cfg(...)'.build] sources`: the host arch's file is compiled, the other's not. + s::needs({"gcc"}); + std::string this_arch, other_arch; // the host's (`uname -m`), and the other one +#if defined(__x86_64__) || defined(_M_X64) + this_arch = "x86_64", other_arch = "aarch64"; +#elif defined(__aarch64__) || defined(_M_ARM64) + this_arch = "aarch64", other_arch = "x86_64"; +#else + skip("unmapped arch"); +#endif + auto p = s::project::scaffold("condsrc"); + p.write("arch/this.cpp", R"cpp(extern "C" int arch_answer() { return 7; } +)cpp"); + p.write("arch/other.cpp", R"cpp(extern "C" int arch_answer() { return 8; } // would double-define if both compiled +)cpp"); + p.write("src/main.cpp", R"cpp(import std; +extern "C" int arch_answer(); +int main() { + std::println("arch = {}", arch_answer()); + return 0; +} +)cpp"); + p.write("mcpp.toml", std::format("[package]\nname = \"condsrc\"\nversion = \"0.1.0\"\n\n[target.'cfg(arch = \"{}\")'.build]\nsources = [\"arch/this.cpp\"]\n\n[target.'cfg(arch = \"{}\")'.build]\nsources = [\"arch/other.cpp\"]\n", this_arch, other_arch)); + expect(fatal(e2e::succeeded(p.mcpp({"build"})))) << "build failed"; + expect(eq(s::last_line(p.mcpp({"run"}).out), std::string{"arch = 7"})) << "unexpected output"; + expect(s::find_files(p.at("target"), R"(other\.o)").empty()) << "other-arch source compiled"; + }; + + "119 dep cfg sources"_test[tag{"requires:gcc"}] = [] { + // A path dependency's `[target.'cfg(...)'.build]` sources are compiled under build and + // under test (#229). + s::needs({"gcc"}); + const auto tmp = temp_dir(); + s::project{tmp / "mylib", {{"mcpp.toml", R"cpp([package] +name = "mylib" +version = "0.1.0" + +[targets.mylib] +kind = "lib" + +[build] +sources = ["src/**/*.cppm"] + +# impl.cpp is reachable ONLY through these conditional blocks — never through +# the unconditional [build] sources glob above. Exactly one of unix/windows +# always matches the resolved host target. +[target.'cfg(unix)'.build] +sources = ["src/impl.cpp"] +[target.'cfg(windows)'.build] +sources = ["src/impl.cpp"] +)cpp"}, {"src/mylib.cppm", R"cpp(export module mylib; + +// Defined in src/impl.cpp, which is pulled in ONLY via cfg-conditional +// [target.'cfg(...)'.build] sources — extern "C" so the module boundary +// doesn't affect the link name. +extern "C" int impl_value(); + +export int mylib_answer() { return impl_value(); } +)cpp"}, {"src/impl.cpp", R"cpp(extern "C" int impl_value() { return 42; } +)cpp"}}}; + s::project app{tmp / "app", {{"mcpp.toml", R"cpp([package] +name = "app" +version = "0.1.0" + +[dependencies] +mylib = { path = "../mylib" } +)cpp"}, {"src/main.cpp", R"cpp(import mylib; +import std; +int main() { + std::println("value = {}", mylib_answer()); + return mylib_answer() == 42 ? 0 : 1; +} +)cpp"}, {"tests/t.cpp", R"cpp(import mylib; +int main() { + return mylib_answer() == 42 ? 0 : 1; +} +)cpp"}}}; + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "build did not compile the path dep's cfg-conditional sources"; + expect(fatal(eq(s::last_line(app.mcpp({"run"}).out), std::string{"value = 42"}))) << "unexpected run output"; + expect(e2e::succeeded(app.mcpp({"test"}))) << "test mode did not compile the path dep's cfg-conditional sources"; + }; + "149 conditional glob flags"_test = [] { + // Per-glob `flags` inside `[target.'cfg()'.build]`: the host's entry reaches the TU + // and overrides the base table; an off-OS entry does not exist (no dead-glob warning); a + // real dead glob in [build].flags still warns. + const std::string this_os = s::on_linux() ? "linux" : s::on_macos() ? "macos" : "windows"; + const std::string other_os = s::on_linux() || s::on_macos() ? "windows" : "linux"; + auto p = s::project::scaffold("condflags"); + p.write("src/zlib/probe.cpp", R"cpp(#ifndef BASE_DEFINE +#error "base [build].flags entry did not reach the TU" +#endif +#ifndef COND_DEFINE +#error "conditional [target.cfg(os)] flags entry did not reach the TU" +#endif +#ifdef REMOVED_DEFINE +#error "conditional entry failed to override the base table (removal case)" +#endif +extern "C" int probe_ok() { return 1; } +)cpp"); + p.write("src/main.cpp", R"cpp(import std; +extern "C" int probe_ok(); +int main() { std::println("probe = {}", probe_ok()); return 0; } +)cpp"); + p.write("mcpp.toml", std::format("[package]\nname = \"condflags\"\nversion = \"0.1.0\"\n\n[build]\nflags = [\n {{ glob = \"src/zlib/**\", defines = [\"BASE_DEFINE=1\", \"REMOVED_DEFINE=1\"] }},\n]\n\n[target.'cfg({0})'.build]\nflags = [\n {{ glob = \"src/zlib/**\", defines = [\"COND_DEFINE=1\"], cxxflags = [\"-UREMOVED_DEFINE\"] }},\n]\n\n[target.'cfg({1})'.build]\nflags = [\n {{ glob = \"src/{1}_only/**\", defines = [\"OFF_OS=1\"] }},\n]\n", this_os, other_os)); + auto b = p.mcpp({"build"}); + expect(fatal(e2e::succeeded(b))) << "conditional glob flags build failed"; + expect(fatal(eq(s::last_line(p.mcpp({"run"}).out), std::string{"probe = 1"}))) << "unexpected output"; + expect(fatal(s::lacks(s::text(b), "matched no source file"))) << "an off-OS conditional flags entry produced a dead-glob warning"; + p.write("mcpp.toml", p.read("mcpp.toml") + R"cpp( +[[build.flags]] +glob = "src/nonexistent_dir/**" +defines = ["NEVER=1"] +)cpp"); + auto b2 = p.mcpp({"build"}); + expect(fatal(e2e::succeeded(b2))) << "build failed"; + expect(e2e::contains(s::text(b2), "matched no source file")) << "a real dead glob in [build].flags must still warn"; + }; + + "195 target cfg feature deps"_test = [] { + // `[target..feature-deps.]`: a matching section is merged, a non-matching one is + // not; an inactive feature's conditional dep is not resolved; a feature populated only + // by non-matching sections is still declared. + const auto tmp = temp_dir(); + s::project{tmp / "widget", {{"mcpp.toml", R"cpp([package] +name = "widget" +version = "0.1.0" +[targets.widget] +kind = "lib" +)cpp"}, {"src/widget.cppm", R"cpp(export module widget; +export int widget_anchor() { return 0; } +)cpp"}}}; + s::project{tmp / "lib", {{"mcpp.toml", R"cpp([package] +name = "mylib" +version = "0.1.0" + +[build] +sources = ["src/mylib.cpp"] + +# Exactly one of these matches on any of the three CI platforms, so the +# `codegen` feature always pulls widget in. +[target.'cfg(unix)'.feature-deps.codegen] +widget = { path = "../widget", visibility = "public" } +[target.'cfg(windows)'.feature-deps.codegen] +widget = { path = "../widget", visibility = "public" } + +# Never matches. If a non-matching section were merged, resolving this bogus +# path would fail the build. +[target.'cfg(arch = "no_such_arch")'.feature-deps.codegen] +ghost = { path = "../this_path_does_not_exist" } +)cpp"}, {"src/mylib.cpp", "int mylib_fn(){return 1;}\n"}}}; + s::project app{tmp / "app", {{"mcpp.toml", R"cpp([package] +name = "app" +version = "0.1.0" + +[dependencies] +mylib = { path = "../lib", features = ["codegen"] } +)cpp"}, {"src/main.cpp", R"cpp(import widget; // only reachable if the cfg-matched feature-dep resolved +int main() { return widget_anchor(); } +)cpp"}}}; + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "a matching [target.*.feature-deps] was not merged"; + expect(fatal(!app.exists("mcpp.lock") || s::lacks(app.read("mcpp.lock"), "ghost"))) << "a non-matching predicate's feature-dep was pulled in"; + s::project plain{tmp / "plain", {{"mcpp.toml", R"cpp([package] +name = "plain" +version = "0.1.0" + +[dependencies] +mylib = { path = "../lib" } +)cpp"}, {"src/main.cpp", "int main(){}\n"}}}; + expect(fatal(e2e::succeeded(plain.mcpp({"build"})))) << "plain build failed"; + expect(fatal(!plain.exists("mcpp.lock") || s::lacks(plain.read("mcpp.lock"), "widget"))) << "an inactive feature's conditional dep was resolved anyway"; + s::project{tmp / "nomatch", {{"mcpp.toml", R"cpp([package] +name = "nomatchlib" +version = "0.1.0" + +[build] +sources = ["src/nm.cpp"] + +[target.'cfg(arch = "no_such_arch")'.feature-deps.codegen] +ghost = { path = "../this_path_does_not_exist" } +)cpp"}, {"src/nm.cpp", "int nm(){return 1;}\n"}}}; + s::project nmapp{tmp / "nmapp", {{"mcpp.toml", R"cpp([package] +name = "nmapp" +version = "0.1.0" + +[dependencies] +nomatchlib = { path = "../nomatch", features = ["codegen"] } +)cpp"}, {"src/main.cpp", "int main(){}\n"}}}; + auto b3 = nmapp.mcpp({"build"}); + expect(fatal(e2e::succeeded(b3))) << "requesting a conditionally-populated feature failed where no predicate matches"; + expect(s::lacks(s::text(b3), "does not declare requested feature")) << "the feature was reported as undeclared where no predicate matches"; + }; + "247 bare triple conditional native"_test = [] { + // `[target.''.build]` applies to a native build, not only to one with an + // explicit `--target` (the macro NAME is read: MSVC spells `-D` as `/D`). + s::project probe{s::files{{"src/main.cpp", "int main() { return 0; }\n"}, + {"mcpp.toml", "[package]\nname = \"probe\"\nversion = \"0.1.0\"\n[targets.probe]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n"}}}; + expect(fatal(e2e::succeeded(probe.mcpp({"build"})))) << "discovery build failed"; + std::string host_triple; + for (const auto& e : fs::directory_iterator(probe.at("target"))) + if (const auto n = e.path().filename().string(); !n.starts_with(".") && (host_triple.empty() || n < host_triple)) host_triple = n; + expect(fatal(!host_triple.empty())) << "no target/ dir"; + probe.write("mcpp.toml", std::format("[package]\nname = \"probe\"\nversion = \"0.1.0\"\n[targets.probe]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n\n[target.'{}'.build]\ncxxflags = [\"-DMCPP_BARE_TRIPLE=1\"]\n\n[target.'cfg(arch = \"{}\")'.build]\ncxxflags = [\"-DMCPP_CFG_ALIAS=1\"]\n", host_triple, host_triple.substr(0, host_triple.find('-')))); + auto count = [&](std::string_view macro) { + for (const auto& f : s::find_files(probe.at("target"), R"(build\.ninja)")) + if (f.generic_string().find("/.build-mcpp/") == std::string::npos) return s::count_lines(s::read_file(f), macro); + return std::size_t{0}; + }; + probe.remove("target"); + expect(fatal(e2e::succeeded(probe.mcpp({"build"})))) << "native build failed"; + expect(fatal(count("MCPP_CFG_ALIAS") > 0)) << "cfg() did not apply on a native build"; + expect(fatal(count("MCPP_BARE_TRIPLE") > 0)) << "[target.'" << host_triple << "'.build] was inert on a native build"; + probe.remove("target"); + expect(fatal(e2e::succeeded(probe.mcpp({"build", "--target", host_triple})))) << "explicit build failed"; + expect(count("MCPP_BARE_TRIPLE") > 0) << "bare triple inert with --target"; + expect(count("MCPP_CFG_ALIAS") > 0) << "cfg() inert with --target"; + }; + "677 a conditional dependency replaces the unconditional one"_test[tag{"requires:elf"}] = [] { + // A matching `[target..dependencies]` declaration replaces the unconditional one of + // the same identity (its linkage applies, the graph records its table); a non-matching + // one does not; a table naming no source is refused with the restated line; a misspelt + // section is reported. + s::needs({"elf"}); + const auto tmp = temp_dir(); + s::project{tmp / "fw", {{"mcpp.toml", R"cpp([package] +namespace = "demo" +name = "fw" +version = "0.1.0" +[targets.fw] +kind = "lib" +)cpp"}, {"src/fw.cppm", "export module fw;\nexport int fw_anchor() { return 41; }\n"}}}; + auto make_app = [&](std::string_view dir, std::string_view tail) { + return s::project{tmp / dir, {{"mcpp.toml", std::format("[package]\nname = \"{}\"\nversion = \"0.1.0\"\n{}", dir, tail)}, + {"src/main.cpp", "import fw;\nint main() { return fw_anchor() == 41 ? 0 : 1; }\n"}}}; + }; + auto table_of = [](const s::project& p, std::string_view canonical) { + const auto json = s::find_file(p.at("target"), "resolution.json"); + expect(fatal(json.has_value())) << "no resolution.json"; + std::string out; + for (const auto& pkg : s::json_file(*json)["graph"]["packages"]) + if (pkg["package"]["canonical"] == canonical) + for (const auto& r : pkg["requested_by"]) out += (out.empty() ? "" : " ") + r["table"].get(); + return out; + }; + auto matching = make_app("matching", "[dependencies]\ndemo.fw = { path = \"../fw\" }\n[target.'cfg(os = \"linux\")'.dependencies]\ndemo.fw = { path = \"../fw\", linkage = \"shared\" }\n"); + expect(fatal(e2e::succeeded(matching.mcpp({"build"})))) << "A: build failed"; + expect(fatal(s::find_file(matching.at("target"), "libfw.so").has_value())) << "A: the conditional linkage did not produce libfw.so"; + const auto exe = s::find_file(matching.at("target"), "matching"); + expect(fatal(exe.has_value())) << "A: no program"; + bool needs = false; + for (const auto& n : s::read_header(*exe).needed) needs = needs || n.find("libfw.so") != std::string::npos; + expect(fatal(needs)) << "A: the program does not NEED libfw.so"; + expect(fatal(eq(table_of(matching, "demo.fw@0.1.0"), std::string{"[target.'cfg(os = \"linux\")'.dependencies]"}))) << "A: graph table"; + auto other = make_app("other", "[dependencies]\ndemo.fw = { path = \"../fw\" }\n[target.'cfg(os = \"windows\")'.dependencies]\ndemo.fw = { path = \"../fw\", linkage = \"shared\" }\n"); + expect(fatal(e2e::succeeded(other.mcpp({"build"})))) << "B: build failed"; + expect(fatal(!s::find_file(other.at("target"), "libfw.so").has_value())) << "B: a non-matching selector produced libfw.so"; + expect(fatal(eq(table_of(other, "demo.fw@0.1.0"), std::string{"[dependencies]"}))) << "B: graph table"; + auto modifier = make_app("modifier", "[dependencies]\ndemo.fw = { path = \"../fw\" }\n[target.'cfg(os = \"linux\")'.dependencies]\ndemo.fw = { linkage = \"shared\" }\n"); + auto c = modifier.mcpp({"build"}); + expect(fatal(failed(c))) << "C: a table that names no source built"; + expect(fatal(e2e::contains(s::text(c), "[target.'cfg(os = \"linux\")'.dependencies] demo.fw.linkage = \"shared\""))) << "C: the warning does not name the conditional table"; + expect(fatal(e2e::contains(s::text(c), "demo.fw = { path = \"../fw\", linkage = \"shared\" }"))) << "C: the warning does not restate the dependency with its source"; + auto typo = make_app("typo", "[dependencies]\ndemo.fw = { path = \"../fw\" }\n[target.'cfg(os = \"linux\")'.dependecies]\ndemo.fw = { path = \"../fw\", linkage = \"shared\" }\n"); + auto dd = typo.mcpp({"build"}); + expect(fatal(e2e::succeeded(dd))) << "D: build failed"; + expect(e2e::contains(s::text(dd), "[target.'cfg(os = \"linux\")'.dependecies] is not a section mcpp reads")) << "D: the misspelled section was not reported"; + }; + "813 conditional dialect cxxflags root only"_test[tag{"requires:gcc"}] = [] { + // `[target..build] dialect_cxxflags` is a graph-wide switch under a target + // condition (#717): a matching row reaches the std BMI, the scan and every TU after the + // root's own [build]; a non-matching one contributes nothing; switching rows rebuilds the + // std BMI and switching back reuses it; a dependency's own key reaches no command. + s::needs({"gcc"}); + const auto tmp = temp_dir(); + const s::options o{.where = s::home::inherited, .env = {{"MCPP_COMPILE_COMMANDS", "compile_commands.json"}}}; + const std::vector build{"build", "--toolchain", "gcc@16.1.0"}; + const std::string main_cpp = "import std;\nint main() { std::println(\"ok\"); return 0; }\n"; + // The first std-module record of the home whose text holds `needle` (`grep -rl | head -1`). + auto std_module_json_for = [](std::string_view needle) -> std::optional { + const auto root = s::case_home(s::home::inherited) / "build-cache/v1/std"; + if (!fs::exists(root)) return std::nullopt; + std::vector files; + for (const auto& e : fs::recursive_directory_iterator(root)) + if (e.is_regular_file()) files.push_back(e.path()); + std::ranges::sort(files); + for (const auto& f : files) + if (s::read_file(f).find(needle) != std::string::npos) return f; + return std::nullopt; + }; + + // 1: a matching row reaches the std BMI, the scan and every TU, after the root's own [build]. + s::project one{tmp / "one", {{"mcpp.toml", R"cpp([package] +name = "app717a" +version = "0.1.0" +standard = "c++23" + +[build] +dialect_cxxflags = ["-DBASE717"] + +[targets.app717a] +kind = "bin" +main = "src/main.cpp" + +[target.linux.build] +dialect_cxxflags = ["-DX717A"] +)cpp"}, {"src/main.cpp", main_cpp}}}; + expect(fatal(e2e::succeeded(one.mcpp(build, o)))) << "the build with a matching [target.linux.build] row failed"; + const auto n1 = ninjas(one); + expect(fatal(!n1.empty())) << "no build.ninja produced"; + std::string cxxflags_line; + for (const auto& l : s::lines_with(s::read_file(n1.front()), "cxxflags")) + if (l.starts_with("cxxflags")) { cxxflags_line = l; break; } + expect(fatal(e2e::contains(cxxflags_line, "-DX717A"))) << "the row's flag is not in build.ninja's global cxxflags (reaches neither scan nor TUs)"; + expect(fatal(e2e::contains(cxxflags_line, "-DBASE717"))) << "the root's own unconditional flag is missing from cxxflags"; + expect(fatal(cxxflags_line.find("-DBASE717") < cxxflags_line.find("-DX717A"))) << "the conditional row's flag does not follow the root's own [build] flag"; + expect(fatal(e2e::contains(text_of(one.at("compile_commands.json")), "-DX717A"))) << "-DX717A is missing from compile_commands.json"; + const auto stdA = std_module_json_for("-DX717A"); + expect(fatal(stdA.has_value())) << "no std-module.json records a std_flag containing -DX717A"; + expect(fatal(e2e::contains(s::read_file(*stdA), "-DBASE717"))) << "the std module's cached prebuild command is missing the root's unconditional flag"; + + // 2: a selector that does not match the resolved target contributes nothing. + s::project two{tmp / "two", {{"mcpp.toml", R"cpp([package] +name = "app717b" +version = "0.1.0" +standard = "c++23" + +[build] +dialect_cxxflags = ["-DBASE717"] + +[targets.app717b] +kind = "bin" +main = "src/main.cpp" + +[target.windows.build] +dialect_cxxflags = ["-DX717A"] +)cpp"}, {"src/main.cpp", main_cpp}}}; + expect(fatal(e2e::succeeded(two.mcpp(build, o)))) << "the build with a non-matching [target.windows.build] row failed"; + const auto n2 = ninjas(two); + expect(fatal(!n2.empty())) << "no build.ninja produced"; + const auto ninja2 = s::read_file(n2.front()); + expect(fatal(s::lacks(ninja2 + text_of(two.at("compile_commands.json")), "-DX717A"))) << "-DX717A from a non-matching [target.windows.build] row reached a Linux build"; + bool base = false; + for (const auto& l : s::lines_with(ninja2, "-DBASE717")) base = base || l.starts_with("cxxflags"); + expect(fatal(base)) << "the root's own unconditional flag is missing"; + + // 3: switching between a matching and a non-matching row rebuilds the std BMI (A, B, A). + s::project cyc{tmp / "cyc", {{"src/main.cpp", main_cpp}}}; + auto write_cyc = [&](std::string_view selector) { cyc.write("mcpp.toml", std::format(R"cpp([package] +name = "app717c" +version = "0.1.0" +standard = "c++23" + +[targets.app717c] +kind = "bin" +main = "src/main.cpp" + +[target.{0}.build] +dialect_cxxflags = ["-DCYC717"] +)cpp", selector)); }; + auto cyc_build = [&](std::string_view selector) { + write_cyc(selector); + cyc.remove("target"); + cyc.remove("compile_commands.json"); + auto r = cyc.mcpp(build, o); + const auto n = ninjas(cyc); + return std::pair{e2e::succeeded(r), n.empty() ? std::string{} : s::read_file(n.front())}; + }; + auto [a1_ok, a1_ninja] = cyc_build("linux"); + expect(fatal(a1_ok)) << "build A (matching) failed"; + expect(fatal(e2e::contains(a1_ninja, "-DCYC717"))) << "build A did not carry -DCYC717"; + const auto stdA1 = std_module_json_for("-DCYC717"); + expect(fatal(stdA1.has_value())) << "no std-module.json for build A"; + auto [b_ok, b_ninja] = cyc_build("windows"); + expect(fatal(b_ok)) << "build B (non-matching) failed"; + expect(fatal(s::lacks(b_ninja + text_of(cyc.at("compile_commands.json")), "-DCYC717"))) << "build B (windows, non-matching on a Linux host) still carries -DCYC717"; + auto [a2_ok, a2_ninja] = cyc_build("linux"); + expect(fatal(a2_ok)) << "build A2 (matching again) failed"; + expect(fatal(e2e::contains(a2_ninja, "-DCYC717"))) << "build A2 did not carry -DCYC717"; + const auto stdA2 = std_module_json_for("-DCYC717"); + expect(fatal(stdA2.has_value())) << "no std-module.json for build A2"; + expect(fatal(*stdA2 == *stdA1)) << "switching back to the matching row did not reuse build A's std BMI cache entry: " << stdA1->string() << " then " << stdA2->string(); + + // 4: a dependency's own dialect_cxxflags reaches no command and does not move the fingerprint. + s::project dep{tmp / "dep717", {{"mcpp.toml", R"cpp([package] +name = "dep717" +version = "0.1.0" +standard = "c++23" + +[targets.dep717] +kind = "lib" + +[build] +dialect_cxxflags = ["-DDEPFLAG_V1"] +)cpp"}, {"src/dep717.cppm", "export module dep717;\nexport int value() { return 42; }\n"}}}; + s::project four{tmp / "four", {{"mcpp.toml", R"cpp([package] +name = "app717d" +version = "0.1.0" +standard = "c++23" + +[dependencies] +dep717 = { path = "../dep717" } + +[targets.app717d] +kind = "bin" +main = "src/main.cpp" +)cpp"}, {"src/main.cpp", "import dep717;\nint main() { return value() == 42 ? 0 : 1; }\n"}}}; + expect(fatal(e2e::succeeded(four.mcpp(build, o)))) << "the first build (dependency carries -DDEPFLAG_V1) failed"; + const auto n4a = ninjas(four); + expect(fatal(n4a.size() == 1u)) << "more than one build.ninja after the first build"; + expect(fatal(s::lacks(s::read_file(n4a.front()) + text_of(four.at("compile_commands.json")), "-DDEPFLAG_V1"))) << "the dependency's own dialect_cxxflags reached a command"; + auto m = dep.read("mcpp.toml"); + m.replace(m.find("-DDEPFLAG_V1"), 12, "-DDEPFLAG_V2"); + dep.write("mcpp.toml", m); + expect(fatal(e2e::succeeded(four.mcpp(build, o)))) << "the second build (dependency's flag changed) failed"; + const auto n4b = ninjas(four); + expect(fatal(n4b.size() == 1u)) << "changing the dependency's own dialect_cxxflags left a second output directory behind (the fingerprint moved)"; + expect(fatal(n4b.front() == n4a.front())) << "changing the dependency's own dialect_cxxflags moved the graph's output directory"; + expect(s::lacks(s::read_file(n4b.front()) + text_of(four.at("compile_commands.json")), "-DDEPFLAG_V2")) << "the dependency's changed dialect_cxxflags reached a command"; + }; + + "895 conditional configuration rows count as they evaluate"_test[tag{"requires:gcc"}] = [] { + // A member's conditional rows count as they evaluate for the build (#786): its own + // `[target..build] dialect_cxxflags` reaches the build under a virtual root (R1); a + // row for another target does not split the members (R2); rows that evaluate + // differently do, each member keeping its own dialect (R3). + s::needs({"gcc"}); + const auto tmp = temp_dir(); + const s::options inherited{.where = s::home::inherited}; + const std::string host = R"(cfg(any(os = "linux", os = "macos", os = "windows")))"; + auto member = [](const s::project& ws, std::string_view name, std::string_view extra) { + ws.write(std::format("{0}/mcpp.toml", name), + std::format("[package]\nname = \"{0}\"\nversion = \"0.1.0\"\n[targets.{0}]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n{1}\n", name, extra)); + ws.write(std::format("{}/src/main.cpp", name), + "#include \n#ifndef DIAL\n#define DIAL 0\n#endif\nint main() { std::printf(\"DIAL=%d\\n\", DIAL); }\n"); + }; + auto run_of = [](const s::project& ws, std::string_view name) -> std::string { + for (const auto& f : s::find_files(ws.at("target"), std::format("{0}|{0}\\.exe", name))) { + const auto g = f.generic_string(); + if (g.find(std::format("/bin/{}", name)) == std::string::npos) continue; + auto r = s::run(f); + auto out = r.out; + std::erase(out, '\r'); + while (!out.empty() && out.back() == '\n') out.pop_back(); + return out; + } + return "(no binary)"; + }; + + s::project r1{tmp / "r1", {{"mcpp.toml", "[workspace]\nmembers = [\"app\"]\n"}}}; + member(r1, "app", std::format("[target.'{}'.build]\ndialect_cxxflags = [\"-DDIAL=2\"]", host)); + expect(fatal(e2e::succeeded(r1.mcpp({"build", "-p", "app"}, inherited)))) << "build"; + const auto got = run_of(r1, "app"); + expect(fatal(got == "DIAL=2")) << "the member's own conditional dialect_cxxflags did not reach the build: " << got; + + s::project r2{tmp / "r2", {{"mcpp.toml", "[workspace]\nmembers = [\"a\", \"b\"]\n"}}}; + member(r2, "a", "[target.'cfg(os = \"none\")'.build]\ndialect_cxxflags = [\"-DNEVER=1\"]"); + member(r2, "b", ""); + expect(fatal(e2e::succeeded(r2.mcpp({"build", "--workspace"}, inherited)))) << "--workspace"; + expect(fatal(ninjas(r2).size() == 1u)) << "a row for another target split the members into " << ninjas(r2).size() << " configurations"; + + s::project r3{tmp / "r3", {{"mcpp.toml", "[workspace]\nmembers = [\"a\", \"b\"]\n"}}}; + member(r3, "a", std::format("[target.'{}'.build]\ndialect_cxxflags = [\"-DDIAL=3\"]", host)); + member(r3, "b", ""); + expect(fatal(e2e::succeeded(r3.mcpp({"build", "--workspace"}, inherited)))) << "--workspace"; + expect(fatal(ninjas(r3).size() == 2u)) << "members whose rows evaluate differently share " << ninjas(r3).size() << " configuration(s)"; + const auto a = run_of(r3, "a"); + expect(fatal(a == "DIAL=3")) << "member a lost its dialect: " << a; + const auto b = run_of(r3, "b"); + expect(b == "DIAL=0") << "member b received a's dialect: " << b; + }; +}}; + +} // namespace diff --git a/tests/e2e/features/defaults.cpp b/tests/e2e/features/defaults.cpp new file mode 100644 index 000000000..9bbc2a471 --- /dev/null +++ b/tests/e2e/features/defaults.cpp @@ -0,0 +1,397 @@ +// Defaults: default features (a consumer opting out, the opt-out with an explicit request, +// through a dependency; forwarding, a forward along every dependency table) and a required +// capability's default provider (formerly tests/e2e/126, 127, 128, 710, 1045). +import std; +import mcpplibs.mtest; +import mcpp.test.support; + +using namespace mcpplibs::mtest; +namespace s = mcpp::test::support; +namespace fs = std::filesystem; + +namespace { + +bool failed(const e2e::outcome& r) { return r.started && r.status != 0; } + +const s::options cdb{.env = {{"MCPP_COMPILE_COMMANDS", "compile_commands.json"}}}; + +E2E defaults{"defaults", [] { + "126 default features opt out"_test = [] { + // `default-features = false` keeps a dependency's default feature (and its define) off; + // the default is on otherwise; an explicit request activates it beside the opt-out. + const auto tmp = temp_dir(); + s::project{tmp / "widget", {{"mcpp.toml", R"cpp([package] +name = "widget" +version = "0.1.0" + +[features] +default = ["turbo"] +turbo = { defines = ["WIDGET_TURBO=1"] } + +[build] +include_dirs = ["include"] + +[targets.widget] +kind = "lib" +)cpp"}, {"include/widget/widget.hpp", R"cpp(#pragma once +inline int widget_mode() { +#ifdef WIDGET_TURBO + return 1; +#else + return 0; +#endif +} +)cpp"}, {"src/widget.cppm", R"cpp(export module widget; +export int widget_anchor() { return 0; } +)cpp"}}}; + s::project off{tmp / "appoff", {{"mcpp.toml", R"cpp([package] +name = "appoff" +version = "0.1.0" + +[dependencies] +widget = { path = "../widget", default-features = false } +)cpp"}, {"src/main.cpp", R"cpp(#include +#ifdef WIDGET_TURBO +#error "WIDGET_TURBO leaked despite default-features = false" +#endif +int main() { return widget_mode() == 0 ? 0 : 2; } +)cpp"}}}; + expect(fatal(e2e::succeeded(off.mcpp({"build"}, cdb)))) << "(A) opt-out build failed"; + expect(fatal(s::lacks(off.read("compile_commands.json"), "WIDGET_TURBO"))) << "(A) WIDGET_TURBO present despite default-features = false"; + const auto bin = off.artifact(s::exe("appoff")); + expect(fatal(bin.has_value())) << "(A) built binary not found"; + expect(fatal(e2e::succeeded(s::run(*bin)))) << "(A) binary observed turbo despite opt-out"; + s::project on{tmp / "appon", {{"mcpp.toml", R"cpp([package] +name = "appon" +version = "0.1.0" + +[dependencies] +widget = { path = "../widget" } +)cpp"}, {"src/main.cpp", R"cpp(#include +#ifndef WIDGET_TURBO +#error "WIDGET_TURBO missing though default features are on" +#endif +int main() { return widget_mode() == 1 ? 0 : 2; } +)cpp"}}}; + expect(fatal(e2e::succeeded(on.mcpp({"build"}, cdb)))) << "(B) default-on build failed"; + expect(fatal(e2e::contains(on.read("compile_commands.json"), "WIDGET_TURBO"))) << "(B) WIDGET_TURBO missing with default features on"; + s::project re{tmp / "appre", {{"mcpp.toml", R"cpp([package] +name = "appre" +version = "0.1.0" + +[dependencies] +widget = { path = "../widget", default-features = false, features = ["turbo"] } +)cpp"}, {"src/main.cpp", R"cpp(#include +#ifndef WIDGET_TURBO +#error "explicit features = [turbo] did not activate alongside default-features = false" +#endif +int main() { return widget_mode() == 1 ? 0 : 2; } +)cpp"}}}; + expect(fatal(e2e::succeeded(re.mcpp({"build"}, cdb)))) << "(C) opt-out + explicit build failed"; + expect(e2e::contains(re.read("compile_commands.json"), "WIDGET_TURBO")) << "(C) explicit feature did not activate alongside opt-out"; + }; + "127 transitive default features optout"_test[tag{"requires:elf"}][tag{"requires:gcc"}] = [] { + // A consumer's `default-features = false` holds when the consumer is itself a dependency. + s::needs({"elf", "gcc"}); + const auto tmp = temp_dir(); + s::project{tmp / "B", {{"mcpp.toml", R"cpp([package] +name = "B" +version = "0.1.0" +[modules] +sources = ["src/**/*.cpp"] +[features] +default = ["heavy"] +heavy = { defines = ["B_HEAVY=1"] } +[targets.B] +kind = "lib" +)cpp"}, {"src/b.cpp", R"cpp(int b_heavy() { +#ifdef B_HEAVY + return 1; +#else + return 0; +#endif +} +)cpp"}}}; + s::project a{tmp / "A", {{"mcpp.toml", R"cpp([package] +name = "A" +version = "0.1.0" +[modules] +sources = ["src/**/*.cpp"] +[dependencies] +B = { path = "../B", default-features = false } +[targets.A] +kind = "lib" +)cpp"}, {"src/a.cpp", R"cpp(extern int b_heavy(); +int a_val() { return b_heavy(); } +)cpp"}}}; + s::project app{tmp / "app", {{"mcpp.toml", R"cpp([package] +name = "app" +version = "0.1.0" +[modules] +sources = ["src/**/*.cpp"] +[dependencies] +A = { path = "../A" } +[targets.app] +kind = "bin" +main = "src/main.cpp" +)cpp"}, {"src/main.cpp", R"cpp(import std; +extern int a_val(); +int main() { std::println("heavy={}", a_val()); return 0; } +)cpp"}}}; + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "build failed"; + expect(fatal(eq(s::last_starting(app.mcpp({"run"}).out, "heavy="), std::string{"heavy=0"}))) << "transitive default-features opt-out not honored"; + a.write("mcpp.toml", R"cpp([package] +name = "A" +version = "0.1.0" +[modules] +sources = ["src/**/*.cpp"] +[dependencies] +B = { path = "../B" } +[targets.A] +kind = "lib" +)cpp"); + app.mcpp({"clean"}); + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "control build failed"; + expect(eq(s::last_starting(app.mcpp({"run"}).out, "heavy="), std::string{"heavy=1"})) << "control (defaults kept) expected heavy=1"; + }; + + "128 feature forwarding"_test = [] { + // A feature may open a feature of a dependency (`withextra = ["leaf/extra"]`), under + // build and test, and only when it is requested. + const auto tmp = temp_dir(); + s::project{tmp / "leaf", {{"mcpp.toml", R"cpp([package] +name = "leaf" +version = "0.1.0" + +[features] +default = [] +extra = { defines = ["LEAF_EXTRA=1"] } + +[build] +include_dirs = ["include"] + +[targets.leaf] +kind = "lib" +)cpp"}, {"include/leaf/leaf.hpp", R"cpp(#pragma once +inline int leaf_mode() { +#ifdef LEAF_EXTRA + return 1; +#else + return 0; +#endif +} +)cpp"}, {"src/leaf.cppm", R"cpp(export module leaf; +export int leaf_anchor() { return 0; } +)cpp"}}}; + s::project{tmp / "mid", {{"mcpp.toml", R"cpp([package] +name = "mid" +version = "0.1.0" + +[features] +default = [] +withextra = ["leaf/extra"] + +[dependencies] +leaf = { path = "../leaf", default-features = false } + +[targets.mid] +kind = "lib" +)cpp"}, {"src/mid.cppm", R"cpp(export module mid; +export int mid_anchor() { return 0; } +)cpp"}}}; + s::project on{tmp / "appon", {{"mcpp.toml", R"cpp([package] +name = "appon" +version = "0.1.0" + +[dependencies] +mid = { path = "../mid", features = ["withextra"] } +)cpp"}, {"src/main.cpp", R"cpp(int main() { return 0; } +)cpp"}}}; + expect(fatal(e2e::succeeded(on.mcpp({"build"}, cdb)))) << "(A) build failed"; + expect(fatal(e2e::contains(on.read("compile_commands.json"), "LEAF_EXTRA"))) << "(A) leaf/extra was NOT activated by the forward"; + expect(fatal(e2e::succeeded(on.mcpp({"test"}, cdb)))) << "(A) test build failed"; + expect(fatal(e2e::contains(on.read("compile_commands.json"), "LEAF_EXTRA"))) << "(A/test) forward did not hold on the test path"; + s::project off{tmp / "appoff", {{"mcpp.toml", R"cpp([package] +name = "appoff" +version = "0.1.0" + +[dependencies] +mid = { path = "../mid" } +)cpp"}, {"src/main.cpp", R"cpp(int main() { return 0; } +)cpp"}}}; + expect(fatal(e2e::succeeded(off.mcpp({"build"}, cdb)))) << "(B) build failed"; + expect(fatal(s::lacks(off.read("compile_commands.json"), "LEAF_EXTRA"))) << "(B) leaf/extra leaked though no consumer requested mid/withextra"; + expect(fatal(e2e::succeeded(off.mcpp({"test"}, cdb)))) << "(B) test build failed"; + expect(s::lacks(off.read("compile_commands.json"), "LEAF_EXTRA")) << "(B/test) leaf/extra leaked on the test path"; + }; + "710 a forward reaches every dependency table"_test[tag{"requires:gcc"}] = [] { + // A feature forward is validated against every dependency table of its manifest: a + // two-level forward along build-dependency edges builds under --strict and reaches the + // leaf's tool; a row-declared dependency's forward is not reported; a forward to a key + // no table declares is named, and refused under --strict. + s::needs({"gcc"}); + const auto tmp = temp_dir(); + const s::options linked{.where = s::home::linked}; + s::project{tmp / "kt", {{"mcpp.toml", R"cpp([package] +name = "kt" +namespace = "spike" +version = "0.1.0" + +[targets.kt] +kind = "bin" +main = "src/main.cpp" +)cpp"}, {"src/main.cpp", "int main() { return 0; }\n"}}}; + s::project{tmp / "leaf", {{"mcpp.toml", R"cpp([package] +name = "leaf" +namespace = "spike" +version = "0.1.0" + +[targets.leaf] +kind = "lib" + +[build] +sources = ["src/*.cpp"] + +[features] +kt = [] + +[feature-deps.kt] +spike.kt = { path = "../kt", tools = ["kt"] } +)cpp"}, {"src/leaf.cpp", "int leaf_answer() { return 1; }\n"}}}; + s::project{tmp / "rules", {{"mcpp.toml", R"cpp([package] +name = "rules" +namespace = "spike" +version = "0.1.0" + +[targets.rules] +kind = "lib" + +[build] +sources = ["src/*.cpp"] + +[build-dependencies] +spike.leaf = { path = "../leaf" } + +[features] +kotlin = ["spike.leaf/kt"] +)cpp"}, {"src/rules.cpp", "int rules_answer() { return 1; }\n"}}}; + s::project{tmp / "win", {{"mcpp.toml", R"cpp([package] +name = "win" +namespace = "spike" +version = "0.1.0" + +[targets.win] +kind = "lib" + +[build] +sources = ["src/*.cpp"] + +[features] +x = [] +)cpp"}, {"src/win.cpp", "int win_answer() { return 1; }\n"}}}; + s::project fw{tmp / "fw", {{"mcpp.toml", R"cpp([package] +name = "fw" +namespace = "spike" +version = "0.1.0" + +[targets.fw] +kind = "lib" + +[build] +sources = ["src/*.cpp"] + +[build-dependencies] +spike.rules = { path = "../rules" } + +[target.'cfg(os = "windows")'.dependencies] +spike.win = { path = "../win" } + +[features] +kotlin = ["spike.rules/kotlin", "spike.win/x"] +)cpp"}, {"src/fw.cpp", "int fw_answer() { return 42; }\n"}}}; + s::project app{tmp / "app", {{"mcpp.toml", R"cpp([package] +name = "app" +version = "0.1.0" + +[targets.app] +kind = "bin" +main = "src/main.cpp" + +[dependencies] +spike.fw = { path = "../fw", features = ["kotlin"] } +)cpp"}, {"src/main.cpp", R"cpp(int fw_answer(); +int main() { return fw_answer() == 42 ? 0 : 1; } +)cpp"}}}; + auto b1 = app.mcpp({"build", "--strict"}, linked); + expect(fatal(e2e::succeeded(b1))) << "the two-level build-dependency forward was refused under --strict"; + expect(fatal(s::lacks(s::text(b1), "forwards to dependency"))) << "a forward along a build-dependency edge was reported as undeclared"; + expect(fatal(e2e::contains(s::text(b1), "Building host tool kt"))) << "the forwarded feature did not reach the leaf (its tool was not built)"; + expect(fatal(s::lacks(s::text(b1), "spike.win"))) << "a row-declared dependency's forward was reported"; + auto manifest = fw.read("mcpp.toml"); + const std::string_view from = "kotlin = [\"spike.rules/kotlin\", \"spike.win/x\"]"; + expect(fatal(manifest.find(from) != std::string::npos)) << "the fixture's forward line is missing"; + manifest.replace(manifest.find(from), from.size(), "kotlin = [\"spike.rules/kotlin\", \"spike.nowhere/x\"]"); + fw.write("mcpp.toml", manifest); + app.remove("target"); + auto b3 = app.mcpp({"build"}, linked); + expect(fatal(e2e::succeeded(b3))) << "an undeclared forward failed a non-strict build"; + expect(fatal(e2e::contains(s::text(b3), "forwards to dependency 'spike.nowhere'"))) << "a forward to an undeclared key was not named"; + auto b4 = app.mcpp({"build", "--strict"}, linked); + expect(fatal(failed(b4))) << "--strict accepted a forward to a key no table declares"; + expect(e2e::contains(s::text(b4), "spike.nowhere")) << "the --strict refusal does not name the key"; + }; + "1045 a required capability takes its default provider"_test[tag{"requires:elf"}] = [] { + // A library's required capability with no provider in the graph takes the provider its + // [defaults] table names for the target (over `*`), reported; a provider the consumer + // supplies is used instead, and no default is added. + s::needs({"elf"}); + const auto tmp = temp_dir(); + for (std::string_view p : {"a", "b"}) + s::project{tmp / std::format("greet-{}", p), {{"mcpp.toml", std::format(R"cpp([package] +name = "greet-{0}" +version = "0.1.0" +provides = ["acme:greeter"] + +[targets.greet-{0}] +kind = "lib" +)cpp", p)}}}; + s::project{tmp / "greet-a", {{"src/a.cpp", "extern \"C\" int greet() { return 1; }\n"}}}; + s::project{tmp / "greet-b", {{"src/b.cpp", "extern \"C\" int greet() { return 2; }\n"}}}; + s::project{tmp / "lib", {{"mcpp.toml", R"cpp([package] +name = "lib" +version = "0.1.0" +requires = ["acme:greeter"] + +[targets.lib] +kind = "lib" + +[defaults."acme:greeter"] +linux = { path = "../greet-a" } +"*" = { path = "../greet-b" } +)cpp"}, {"src/lib.cpp", "extern \"C\" int greet(); int lib_greet() { return greet(); }\n"}}}; + s::project app{tmp / "app", {{"mcpp.toml", R"cpp([package] +name = "app" +version = "0.1.0" + +[dependencies] +lib = { path = "../lib" } +)cpp"}, {"src/main.cpp", "int lib_greet(); int main() { return lib_greet(); }\n"}}}; + auto b1 = app.mcpp({"build"}); + expect(fatal(e2e::succeeded(b1))) << "the build failed\n" << s::text(b1); + expect(fatal(s::has_line(s::text(b1), "Default .*greet-a for acme:greeter of lib \\(linux\\)"))) << "the default is not reported\n" << s::text(b1); + std::optional exe; + for (const auto& f : s::find_files(app.at("target"), "app")) + if (!exe && f.generic_string().ends_with("/bin/app")) exe = f; + expect(fatal(exe.has_value())) << "no target/*/bin/app"; + const auto r1 = s::run(*exe); + expect(fatal(r1.status == 1)) << "the program returned " << r1.status << ", not the Linux default's 1"; + app.write("mcpp.toml", app.read("mcpp.toml") + "greet-b = { path = \"../greet-b\" }\n"); + auto b2 = app.mcpp({"build"}); + expect(fatal(e2e::succeeded(b2))) << "the build with the consumer's provider failed\n" << s::text(b2); + expect(fatal(s::lacks(s::text(b2), "Default "))) << "a default was added though the graph provides the capability"; + const auto r2 = s::run(*exe); + expect(r2.status == 2) << "the program returned " << r2.status << ", not the consumer's provider's 2"; + }; +}}; + +} // namespace diff --git a/tests/e2e/features/layers.cpp b/tests/e2e/features/layers.cpp new file mode 100644 index 000000000..3c235633b --- /dev/null +++ b/tests/e2e/features/layers.cpp @@ -0,0 +1,299 @@ +// Layer predicates and runtime tables in target-conditional configuration, a requirement on +// the target axis, a requirement a layer decides (formerly tests/e2e/328, 637, 654, 1007). +import std; +import mcpplibs.mtest; +import mcpp.test.support; + +using namespace mcpplibs::mtest; +namespace s = mcpp::test::support; +namespace fs = std::filesystem; + +namespace { + +bool failed(const e2e::outcome& r) { return r.started && r.status != 0; } + +const s::options cdb{.env = {{"MCPP_COMPILE_COMMANDS", "compile_commands.json"}}}; + +E2E layers{"layers", [] { + "328 layer predicates apply and unknown ones say so"_test = [] { + // `cfg(c-abi = ...)` applies when the resolved c-abi matches and not otherwise, once + // beside a triple leg, in a dependency too; an unknown cfg key is named with the + // vocabulary (an error under --strict); a layer-conditioned dependency is reported while + // the section's build inputs still apply. + const auto tmp = temp_dir(); + s::project probe{tmp / "probe", {{"mcpp.toml", R"cpp([package] +name = "probe" +version = "0.1.0" +)cpp"}, {"src/main.cpp", "int main() { return 0; }\n"}}}; + auto report = probe.mcpp({"build"}, {.env = {{"MCPP_VERBOSE", "1"}, {"MCPP_COMPILE_COMMANDS", "compile_commands.json"}}}); + expect(fatal(e2e::succeeded(report))) << "the probe build errored"; + std::smatch m; + const auto text = s::text(report); + expect(fatal(std::regex_search(text, m, std::regex{R"(c-abi *([A-Za-z0-9_+-]*))"}))) << "could not read the resolved c-abi"; + const std::string cabi = m[1].str(), notcabi = "not-" + cabi; + s::project layers{tmp / "layers", {{"src/main.cpp", R"cpp(#ifndef PROBE_MATCHED +#error "a matching cfg(c-abi = ...) section did not apply" +#endif +#ifdef PROBE_MUST_NOT_APPLY +#error "a NON-matching cfg(c-abi = ...) section applied" +#endif +#ifndef PROBE_COMBINED +#error "cfg(all(unix, c-abi = ...)) did not apply" +#endif +int main() { return 0; } +)cpp"}}}; + layers.write("mcpp.toml", std::format(R"cpp([package] +name = "layers" +version = "0.1.0" + +[target.'cfg(c-abi = "{0}")'.build] +defines = ["PROBE_MATCHED=1"] + +[target.'cfg(c-abi = "{1}")'.build] +defines = ["PROBE_MUST_NOT_APPLY=1"] + +[target.'cfg(all(any(unix, windows), c-abi = "{0}"))'.build] +defines = ["PROBE_COMBINED=1"] +)cpp", cabi, notcabi)); + expect(fatal(e2e::succeeded(layers.mcpp({"build"}, cdb)))) << "layer predicates did not behave"; + s::project once{tmp / "once", {{"src/main.cpp", "int main() { return 0; }\n"}}}; + once.write("mcpp.toml", std::format(R"cpp([package] +name = "once" +version = "0.1.0" + +[target.'cfg(any(unix, windows, c-abi = "{0}"))'.build] +defines = ["PROBE_ONCE=1"] +)cpp", cabi)); + expect(fatal(e2e::succeeded(once.mcpp({"build"}, cdb)))) << "the mixed-predicate build errored"; + const auto db = once.read("compile_commands.json"); + const std::regex flag{R"([-/]DPROBE_ONCE=1)"}; + const auto n = std::distance(std::sregex_iterator(db.begin(), db.end(), flag), std::sregex_iterator()); + expect(fatal(eq(n, std::ptrdiff_t{1}))) << "a predicate with both a triple leg and a layer leg contributed " << n << " times, expected 1"; + s::project lib{tmp / "dep/lib", {{"src/layerlib.cppm", R"cpp(export module layerlib; +#ifndef LIB_MATCHED +#error "a DEPENDENCY's matching cfg(c-abi = ...) section did not apply" +#endif +#ifdef LIB_MUST_NOT_APPLY +#error "a DEPENDENCY's non-matching cfg(c-abi = ...) section applied" +#endif +export int lib_answer() { return 42; } +)cpp"}}}; + lib.write("mcpp.toml", std::format(R"cpp([package] +name = "layerlib" +version = "0.1.0" + +[target.'cfg(c-abi = "{0}")'.build] +defines = ["LIB_MATCHED=1"] + +[target.'cfg(c-abi = "{1}")'.build] +defines = ["LIB_MUST_NOT_APPLY=1"] +)cpp", cabi, notcabi)); + s::project app{tmp / "dep/app", {{"mcpp.toml", R"cpp([package] +name = "app" +version = "0.1.0" + +[dependencies] +layerlib = { path = "../lib" } +)cpp"}, {"src/main.cpp", R"cpp(import layerlib; +int main() { return lib_answer() == 42 ? 0 : 1; } +)cpp"}}}; + expect(fatal(e2e::succeeded(app.mcpp({"build"}, cdb)))) << "layer predicates did not reach a dependency"; + s::project unknown{tmp / "unknown", {{"mcpp.toml", R"cpp([package] +name = "unknown" +version = "0.1.0" + +[target.'cfg(no-such-key = "x")'.build] +defines = ["NEVER"] +)cpp"}, {"src/main.cpp", "int main() { return 0; }\n"}}}; + auto w = unknown.mcpp({"build"}, cdb); + expect(fatal(e2e::succeeded(w))) << "an unknown cfg key must warn, not fail"; + expect(fatal(e2e::contains(s::text(w), "no-such-key"))) << "the unknown cfg key was not named"; + expect(fatal(e2e::contains(s::text(w), "c-abi"))) << "the message does not list the supported keys"; + expect(fatal(failed(unknown.mcpp({"build", "--strict"}, cdb)))) << "--strict did not turn the warning into an error"; + s::project depcond{tmp / "depcond", {{"src/main.cpp", R"cpp(#ifndef INPUTS_STILL_APPLY +#error "the warning about dependencies also suppressed the build inputs" +#endif +int main() { return 0; } +)cpp"}}}; + depcond.write("mcpp.toml", std::format(R"cpp([package] +name = "depcond" +version = "0.1.0" + +[target.'cfg(c-abi = "{1}")'.dependencies] +never-resolved = "1.0.0" + +[target.'cfg(c-abi = "{0}")'.build] +defines = ["INPUTS_STILL_APPLY=1"] +)cpp", cabi, notcabi)); + auto dc = depcond.mcpp({"build"}, cdb); + expect(fatal(e2e::succeeded(dc))) << "a layer-conditioned dependency must warn, not fail"; + expect(e2e::contains(s::text(dc), "conditions dependencies on a target-side layer")) << "no diagnostic for a layer-conditioned dependency"; + }; + + "637 a conditional runtime table alone is applied"_test[tag{"requires:gcc"}] = [] { + // `[target..runtime] libraries` reaches the link line when it is alone under its + // predicate and beside another table; a non-matching predicate contributes nothing; a + // typo in either runtime table is reported while the neighbouring key applies. + s::needs({"gcc"}); + const auto tmp = temp_dir(); + auto graph = [](const s::project& p) { + const auto nj = s::find_file(p.at("target"), "build.ninja"); + expect(fatal(nj.has_value())) << "no build graph"; + return s::read_file(*nj); + }; + for (auto [leg, extra] : {std::pair{"alone", ""}, {"sibling", "[target.linux.build]\ndefines = [\"UNRELATED_TO_LIBRARIES\"]"}}) { + s::project p{tmp / leg, {{"src/main.cpp", R"cpp(int main() { return 0; } +)cpp"}}}; + p.write("mcpp.toml", std::string(R"cpp([package] +name = "condrt" +version = "0.1.0" + +[target.linux.runtime] +libraries = ["m"] +)cpp") + std::string(extra) + "\n"); + expect(fatal(e2e::succeeded(p.mcpp({"build"})))) << "[" << leg << "] build"; + expect(fatal(s::has_line(graph(p), R"(-lm\b)"))) << "[" << leg << "] [target.linux.runtime] libraries did not reach the link line"; + } + s::project offhost{tmp / "offhost", {{"src/main.cpp", R"cpp(int main() { return 0; } +)cpp"}, {"mcpp.toml", R"cpp([package] +name = "condrt" +version = "0.1.0" + +[target.windows.runtime] +libraries = ["user32"] +)cpp"}}}; + expect(fatal(e2e::succeeded(offhost.mcpp({"build"})))) << "[offhost] build"; + expect(fatal(s::lacks(graph(offhost), "-luser32"))) << "[offhost] a windows predicate contributed to a linux link line"; + s::project typo{tmp / "typo", {{"src/main.cpp", R"cpp(int main() { return 0; } +)cpp"}, {"mcpp.toml", R"cpp([package] +name = "condrt" +version = "0.1.0" + +[runtime] +dlopen_lib = ["one"] + +[target.linux.runtime] +libraries = ["m"] +framework = ["Cocoa"] +)cpp"}}}; + auto t = typo.mcpp({"build"}); + expect(fatal(e2e::succeeded(t))) << "[typo] build"; + expect(e2e::contains(s::text(t), "unsupported key 'dlopen_lib'")) << "[typo] a plausible typo in [runtime] was accepted in silence"; + expect(e2e::contains(s::text(t), "unsupported key 'framework'")) << "[typo] a plausible typo in [target..runtime] was accepted in silence"; + expect(s::has_line(graph(typo), R"(-lm\b)")) << "[typo] the correctly spelled key stopped working"; + }; + "654 a requirement on the target axis"_test[tag{"requires:elf"}][tag{"requires:gcc"}] = [] { + // `requires_abi` under `[target.]` and `[target..feature-requires-abi]` joins + // the package's requirements only when the selector matches: a non-matching one imposes + // nothing; a matching one is refused without the root's table, naming the dependency and + // the selector, and satisfied by it (-pthread, its own fingerprint); per feature too. + s::needs({"elf", "gcc"}); + const auto tmp = temp_dir(); + s::project dep{tmp / "tgtdep", {{"src/tgtdep.cppm", "export module tgtdep;\nexport int tgt_value() { return 7; }\n"}}}; + s::project app{tmp / "app", {{"src/main.cpp", R"cpp(import tgtdep; +int main() { return tgt_value() == 7 ? 0 : 1; } +)cpp"}}}; + auto write_dep = [&](std::string_view extra) { dep.write("mcpp.toml", std::format(R"cpp([package] +name = "tgtdep" +version = "0.1.0" + +[targets.tgtdep] +kind = "lib" + +[features] +mt = {{}} + +{0} +)cpp", extra)); }; + auto write_app = [&](std::string_view table, std::string_view trailing) { app.write("mcpp.toml", std::format(R"cpp([package] +name = "app" +version = "0.1.0" + +[dependencies] +tgtdep = {0} + +{1} +)cpp", table, trailing)); }; + auto newest_ninja = [&] { + std::optional best; + for (const auto& f : s::find_files(app.at("target"), R"(build\.ninja)")) + if (f.generic_string().find("/.build-mcpp/") == std::string::npos && (!best || fs::last_write_time(f) > fs::last_write_time(*best))) best = f; + expect(fatal(best.has_value())) << "no build.ninja"; + return *best; + }; + auto pthread_in_dep_command = [&] { + auto db = newest_ninja().parent_path() / "compile_commands.json"; + if (!fs::exists(db)) db = s::newest_file(app.dir(), "compile_commands.json").value_or(db); + auto text = s::read_file(db); + std::erase(text, '\n'); + const std::regex unit{R"(\{[^{}]*tgtdep\.cppm[^{}]*\})"}; + for (std::sregex_iterator it(text.begin(), text.end(), unit), end; it != end; ++it) + if (it->str().find("-pthread") != std::string::npos) return true; + return false; + }; + const std::string threads_table = "[target.'cfg(linux)'.abi]\nthreads = true\n"; + const std::string refusal = "`tgtdep` requires the artefact's ABI to have threads ([target.'cfg(linux)'])"; + write_dep("[target.'cfg(windows)']\nrequires_abi = { threads = true }\n"); + write_app("{ path = \"../tgtdep\" }", ""); + expect(fatal(e2e::succeeded(app.mcpp({"build"}, cdb)))) << "a windows-only target-axis requirement failed a host build"; + const auto plain_dir = newest_ninja().parent_path(); + expect(fatal(!pthread_in_dep_command())) << "control: -pthread is on the dependency's compile command with no root table"; + write_dep("[target.'cfg(linux)']\nrequires_abi = { threads = true }\n"); + auto refused = app.mcpp({"build"}, cdb); + expect(fatal(failed(refused))) << "a target-axis threads requirement was built without the root's table"; + expect(fatal(e2e::contains(s::text(refused), refusal))) << "the refusal does not name the dependency and the selector"; + write_app("{ path = \"../tgtdep\" }", threads_table); + expect(fatal(e2e::succeeded(app.mcpp({"build"}, cdb)))) << "the build with the root's table failed"; + expect(fatal(pthread_in_dep_command())) << "-pthread did not reach the dependency's compile command"; + expect(fatal(newest_ninja().parent_path() != plain_dir)) << "the build with threads reused the fingerprint directory of the plain build"; + expect(fatal(e2e::succeeded(app.mcpp({"run"}, cdb)))) << "the program did not run"; + app.remove("target"); + write_dep("[target.'cfg(linux)'.feature-requires-abi]\nmt = { threads = true }\n"); + write_app("{ path = \"../tgtdep\" }", ""); + expect(fatal(e2e::succeeded(app.mcpp({"build"}, cdb)))) << "an inactive feature's target-axis requirement failed the build"; + expect(fatal(!pthread_in_dep_command())) << "an inactive feature's requirement put -pthread on the compile command"; + write_app("{ path = \"../tgtdep\", features = [\"mt\"] }", ""); + auto active = app.mcpp({"build"}, cdb); + expect(fatal(failed(active))) << "an active feature's target-axis requirement was built without the root's table"; + expect(fatal(e2e::contains(s::text(active), refusal))) << "the refusal does not name the selector for the active feature's requirement"; + write_app("{ path = \"../tgtdep\", features = [\"mt\"] }", threads_table); + expect(fatal(e2e::succeeded(app.mcpp({"build"}, cdb)))) << "the active feature's requirement was not satisfied by the root's table"; + expect(pthread_in_dep_command()) << "-pthread did not reach the dependency's compile command"; + }; + "1007 a requirement a layer decides is checked"_test[tag{"requires:unix-shell"}] = [] { + // A requirement stated in a table whose selector names a layer is checked once the + // layers are resolved, as a requirement of [package] is: refused before anything + // compiles, naming the dependency and what it requires; a non-matching table states nothing. + s::needs({"unix-shell"}); + const auto tmp = temp_dir(); + s::project needs{tmp / "needs", {{"src/needs.cpp", "int needs() { return 7; }\n"}}}; + auto write_needs = [&](std::string_view selector) { needs.write("mcpp.toml", std::format(R"cpp([package] +name = "needs" +version = "0.1.0" + +[target.'{0}'] +requires = ["mcpp:compiler-runtime=nonesuch"] + +[targets.needs] +kind = "lib" +)cpp", selector)); }; + s::project app{tmp / "app", {{"mcpp.toml", R"cpp([package] +name = "app" +version = "0.1.0" + +[dependencies] +needs = { path = "../needs" } +)cpp"}, {"src/main.cpp", "int needs();\nint main() { return needs() == 7 ? 0 : 1; }\n"}}}; + write_needs(R"(cfg(not(compiler = "nonesuch")))"); + auto a = app.mcpp({"build"}); + expect(fatal(failed(a))) << "A: a requirement a layer decides was not checked\n" << s::text(a); + expect(fatal(e2e::contains(s::text(a), "needs@0.1.0` requires the compiler-runtime to be `nonesuch`"))) << "A: the refusal does not name the dependency, the layer and what it requires\n" << s::text(a); + expect(fatal(s::lacks(s::text(a), "Compiling"))) << "A: something was compiled before the refusal"; + write_needs(R"(cfg(compiler = "nonesuch"))"); + app.remove("target"); + auto b = app.mcpp({"build"}); + expect(e2e::succeeded(b)) << "B: a table this build does not match stated a requirement\n" << s::text(b); + }; +}}; + +} // namespace diff --git a/tests/e2e/features/sources.cpp b/tests/e2e/features/sources.cpp new file mode 100644 index 000000000..a6d9bc4f7 --- /dev/null +++ b/tests/e2e/features/sources.cpp @@ -0,0 +1,223 @@ +// Feature-gated sources: a dependency's under `mcpp test`, a manifest's [features] `sources`, a gate +// and a provider, per-glob feature flags (formerly tests/e2e/100, 106, 138, 146). +import std; +import mcpplibs.mtest; +import mcpp.test.support; + +using namespace mcpplibs::mtest; +namespace s = mcpp::test::support; +namespace fs = std::filesystem; + +namespace { + +bool failed(const e2e::outcome& r) { return r.started && r.status != 0; } + +// The most recently written under `target`, outside the build programs' trees. +std::optional newest_graph(const fs::path& target, std::string_view name) { + std::optional best; + for (const auto& f : s::find_files(target, std::regex_replace(std::string(name), std::regex{R"(\.)"}, R"(\.)"))) + if (f.generic_string().find("/.build-mcpp/") == std::string::npos && (!best || fs::last_write_time(f) > fs::last_write_time(*best))) best = f; + return best; +} + +E2E sources{"sources", [] { + "100 feature sources test mode"_test = [] { + // A dependency's feature-gated `sources` are compiled under `mcpp test` as under + // `mcpp build`, and not without the feature. + auto app = s::project::scaffold("app"); + app.remove("src/main.cpp"); + app.remove("tests/test_smoke.cpp"); + app.write("mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies.compat]\ncjson = { version = \"1.7.19\", features = [\"utils\"] }\n"); + app.write("tests/t.cpp", "#include \n#include \nint main() {\n cJSON* root = cJSON_Parse(\"{\\\"a\\\":1}\");\n if (!root) return 1;\n char* p = cJSONUtils_FindPointerFromObjectTo(root, root);\n int ok = (p != 0);\n cJSON_Delete(root);\n return ok ? 0 : 1;\n}\n"); + expect(fatal(e2e::succeeded(app.mcpp({"test"})))) << "feature-gated sources not compiled under `mcpp test`"; + auto nj = newest_graph(app.at("target"), "test.ninja"); + expect(fatal(nj && e2e::contains(s::read_file(*nj), "cJSON_Utils"))) << "cJSON_Utils.c absent from the test-mode build graph"; + app.write("src/main.cpp", "#include \n#include \nint main() {\n cJSON* root = cJSON_Parse(\"{\\\"a\\\":1}\");\n char* p = cJSONUtils_FindPointerFromObjectTo(root, root);\n (void)p; cJSON_Delete(root); return 0;\n}\n"); + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "build with feature sources failed"; + nj = newest_graph(app.at("target"), "build.ninja"); + expect(fatal(nj && e2e::contains(s::read_file(*nj), "cJSON_Utils"))) << "cJSON_Utils.c absent from build graph"; + app.write("mcpp.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies.compat]\ncjson = \"1.7.19\"\n"); + app.write("src/main.cpp", "#include \nint main() { cJSON* r = cJSON_Parse(\"{\\\"a\\\":1}\"); cJSON_Delete(r); return 0; }\n"); + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "default (no feature) build failed"; + nj = newest_graph(app.at("target"), "build.ninja"); + expect(fatal(nj.has_value())) << "no build.ninja"; + expect(s::lacks(s::read_file(*nj), "cJSON_Utils")) << "cJSON_Utils.c compiled without the `utils` feature"; + }; + + "106 feature gated sources toml"_test[tag{"requires:gcc"}] = [] { + // [features] `sources`: off and on, under build and under test. + s::needs({"gcc"}); + auto p = s::project::scaffold("featsrc"); + p.write("src/extra.cpp", "extern \"C\" int extra_answer() { return 42; }\n"); + p.write("src/main.cpp", "import std;\nextern \"C\" int extra_answer();\nint main() {\n#ifdef HAS_EXTRA\n std::println(\"extra = {}\", extra_answer());\n#else\n std::println(\"extra = none\");\n#endif\n return 0;\n}\n"); + p.write("mcpp.toml", "[package]\nname = \"featsrc\"\nversion = \"0.1.0\"\n\n[features]\ndefault = []\nextra = { sources = [\"src/extra.cpp\"], defines = [\"HAS_EXTRA\"] }\n"); + p.remove("tests/test_smoke.cpp"); + expect(fatal(e2e::succeeded(p.mcpp({"build"})))) << "build (off) failed"; + expect(fatal(s::find_files(p.at("target"), R"(extra\.o)").empty())) << "extra.cpp compiled with feature off"; + expect(fatal(eq(s::last_line(p.mcpp({"run"}).out), std::string{"extra = none"}))) << "unexpected (off)"; + expect(fatal(e2e::succeeded(p.mcpp({"build", "--features", "extra"})))) << "build (on) failed"; + const auto bin = s::newest_file(p.at("target"), "featsrc"); + expect(fatal(bin.has_value())) << "binary not found"; + expect(fatal(eq(s::last_line(s::run(*bin).out), std::string{"extra = 42"}))) << "unexpected (on)"; + p.write("tests/test_featsrc.cpp", "#ifdef HAS_EXTRA\nextern \"C\" int extra_answer();\nint main() { return extra_answer() == 42 ? 0 : 1; }\n#else\nint main() { return 0; }\n#endif\n"); + expect(fatal(e2e::succeeded(p.mcpp({"test"})))) << "mcpp test (off) failed"; + expect(e2e::succeeded(p.mcpp({"test", "--features", "extra"}))) << "mcpp test (on) failed"; + }; + "138 feature sources gate vs provider"_test[tag{"requires:unix-shell"}] = [] { + // A feature that gates a source (in no base list, but under the inferred `src/**`) and a + // feature that switches a source the package provides, under build and test. + s::needs({"unix-shell"}); + const auto tmp = temp_dir(); + s::project provider{tmp / "provider", {{"mcpp.toml", R"cpp([package] +name = "provider" +version = "0.1.0" + +# NO `[build] sources`, WHICH IS THE POINT. The base set is the inferred +# `src/**`, which matches `src/gated/only_with_feature.cpp` — so removing the +# feature's glob STRING from a list it was never in gates nothing. +[features] +default = [] +gated = { sources = ["src/gated/**"] } +)cpp"}, {"src/lib.cpp", R"cpp(int provider_value() { return 7; } +)cpp"}, {"src/gated/only_with_feature.cpp", R"cpp(// The header arrives with the feature's dependencies and does not exist +// without it. Compiling this file without the feature is the defect. +#include +int gated_value() { return 1; } +)cpp"}, {"tests/basic.cpp", R"cpp(extern int provider_value(); +int main() { return provider_value() == 7 ? 0 : 1; } +)cpp"}}}; + expect(fatal(e2e::succeeded(provider.mcpp({"build"})))) << "provider: build compiled a feature-only source"; + expect(fatal(e2e::succeeded(provider.mcpp({"test"})))) << "provider: TEST compiled a feature-only source — the defect this file exists for"; + expect(fatal(s::find_files(provider.at("target"), ".*only_with_feature.*").empty())) << "provider: an object was produced for a source the feature did not activate"; + auto gated = provider.mcpp({"build", "--features", "gated"}); + expect(fatal(failed(gated))) << "provider: the feature did not bring its own source in"; + expect(fatal(e2e::contains(s::text(gated), "a_header_the_feature_would_have_brought.h"))) << "provider: activating the feature failed for some other reason"; + s::project gate{tmp / "gate", {{"mcpp.toml", R"cpp([package] +name = "gate" +version = "0.1.0" + +# `src/gated_main.cpp` IS IN BOTH LISTS, EXACTLY AS gtest'S DESCRIPTOR HAS IT. +# The package provides the file; the feature is a switch over it. +[build] +sources = ["src/lib.cpp", "src/gated_main.cpp"] + +[features] +default = [] +main = { sources = ["src/gated_main.cpp"] } +)cpp"}, {"src/lib.cpp", R"cpp(int gate_value() { return 11; } +)cpp"}, {"src/gated_main.cpp", R"cpp(int gated_main_marker() { return 42; } +)cpp"}, {"tests/basic.cpp", R"cpp(extern int gate_value(); +int main() { return gate_value() == 11 ? 0 : 1; } +)cpp"}}}; + expect(fatal(e2e::succeeded(gate.mcpp({"build"})))) << "gate: build failed"; + expect(fatal(s::find_files(gate.at("target"), ".*gated_main.*").empty())) << "gate: build compiled a gated source with the feature inactive"; + gate.remove("target"); + expect(fatal(e2e::succeeded(gate.mcpp({"test"})))) << "gate: test failed"; + expect(!s::find_files(gate.at("target"), ".*gated_main.*").empty()) << "gate: a source the package provides unconditionally vanished under `mcpp test`"; + }; + + "146 feature flags"_test[tag{"requires:gcc"}] = [] { + // [features]..flags: per-feature per-glob flags reach exactly the matched TU, off + // and on, under build and test; a dead glob warns only when its feature is on; a + // dependency's feature flags stay private while its defines propagate. + s::needs({"gcc"}); + auto p = s::project::scaffold("featflags"); + p.write("src/simd/kernel.cpp", R"cpp(#ifndef KERNEL_TAG +#error "feature flags did not reach the glob-matched TU" +#endif +extern "C" int kernel_tag() { return KERNEL_TAG; } +)cpp"); + p.write("src/plain.cpp", R"cpp(#ifdef KERNEL_TAG +#error "feature flags leaked outside their glob (must be per-TU, not package-wide)" +#endif +extern "C" int plain_ok() { return 1; } +)cpp"); + p.write("src/main.cpp", R"cpp(import std; +extern "C" int plain_ok(); +#ifdef MCPP_FEATURE_SIMD +extern "C" int kernel_tag(); +#endif +int main() { +#ifdef MCPP_FEATURE_SIMD + std::println("tag = {}", kernel_tag() + plain_ok()); +#else + std::println("tag = none"); +#endif + return 0; +} +)cpp"); + p.write("mcpp.toml", R"cpp([package] +name = "featflags" +version = "0.1.0" + +[build] +sources = ["src/*.cpp"] + +[features] +default = [] +simd = { sources = ["src/simd/**"], flags = [ + { glob = "src/simd/**", defines = ["KERNEL_TAG=6"] } ] } +dead = { flags = [ { glob = "src/never/**", defines = ["NOPE"] } ] } +)cpp"); + p.remove("tests/test_smoke.cpp"); + auto off = p.mcpp({"build"}); + expect(fatal(e2e::succeeded(off))) << "build (off) failed"; + expect(fatal(s::lacks(s::text(off), "matched no source file"))) << "feature-off build must not warn about feature flag globs"; + expect(fatal(eq(s::last_line(p.mcpp({"run"}).out), std::string{"tag = none"}))) << "unexpected (off)"; + expect(fatal(e2e::succeeded(p.mcpp({"build", "--features", "simd"})))) << "build (on) failed"; + const auto bin = s::newest_file(p.at("target"), "featflags"); + expect(fatal(bin.has_value())) << "binary not found"; + expect(fatal(eq(s::last_line(s::run(*bin).out), std::string{"tag = 7"}))) << "unexpected (on) (want 6+1)"; + auto dead = p.mcpp({"build", "--features", "simd,dead"}); + expect(fatal(e2e::succeeded(dead))) << "build (dead) failed"; + expect(fatal(e2e::contains(s::text(dead), "features.dead.flags glob 'src/never/**' matched no source file"))) << "missing feature-scoped dead-glob warning"; + p.write("tests/test_featflags.cpp", R"cpp(#ifdef MCPP_FEATURE_SIMD +extern "C" int kernel_tag(); +int main() { return kernel_tag() == 6 ? 0 : 1; } +#else +int main() { return 0; } +#endif +)cpp"); + auto test_off = p.mcpp({"test"}); + expect(fatal(e2e::succeeded(test_off))) << "mcpp test (off) failed"; + expect(fatal(s::lacks(s::text(test_off), "matched no source file"))) << "feature-off test must not warn about feature flag globs"; + expect(fatal(e2e::succeeded(p.mcpp({"test", "--features", "simd"})))) << "mcpp test (on) failed"; + const auto tmp = temp_dir(); + s::project{tmp / "widget", {{"mcpp.toml", R"cpp([package] +name = "widget" +version = "0.1.0" + +[features] +default = [] +turbo = { defines = ["WIDGET_IFACE=1"], flags = [ + { glob = "src/**", defines = ["WIDGET_PRIVATE=1"] } ] } + +[targets.widget] +kind = "lib" +)cpp"}, {"src/widget.cppm", R"cpp(export module widget; +#ifndef WIDGET_PRIVATE +#error "feature flags did not reach the dep's own matched TU" +#endif +export int widget_anchor() { return WIDGET_PRIVATE; } +)cpp"}}}; + s::project app{tmp / "app", {{"mcpp.toml", R"cpp([package] +name = "app" +version = "0.1.0" + +[dependencies] +widget = { path = "../widget", features = ["turbo"] } +)cpp"}, {"src/main.cpp", R"cpp(import widget; +#ifndef WIDGET_IFACE +#error "feature defines must interface-propagate to the consumer" +#endif +#ifdef WIDGET_PRIVATE +#error "feature flags must NOT propagate to the consumer" +#endif +int main() { return widget_anchor() == 1 ? 0 : 1; } +)cpp"}}}; + expect(fatal(e2e::succeeded(app.mcpp({"build"})))) << "dep build failed"; + expect(e2e::succeeded(app.mcpp({"run"}))) << "dep run failed"; + }; +}}; + +} // namespace From 6f2864b5fe3f4fcca654581c00b281fb08c78f7f Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 13 Nov 2026 19:11:37 +0900 Subject: [PATCH 009/115] e2e: the pack subsystem as C++ cases (37 scripts) The 37 scripts MIGRATION.tsv assigns to `pack` (the packing modes and the closures they carry, library packages and the gate a prebuilt package keeps, formats a package provides, the report and the axes of a package, packs over workspace members) are 37 cases of the program e2e/pack (tests/e2e/pack/*.cpp, 4 files: formats, library, members, modes). Each case is named by its script's sentence and keeps its number; its `# requires:` words are its tags. Scripts: 30 215 216 242 243 244 245 249 250 251 252 253 254 261 264 265 638 649 652 662 669 689 722 723 801 828 829 841 867 868 869 870 871 954 1008 1043 1061. No workflow runs any of them by path; all 37 are removed. Old run (the scripts, against this branch's mcpp, Linux x86_64): 36 passed, 1 failed (669). New run (`mcpp test e2e/pack`): 36 passed, 1 failed (669), 0 skipped. 669 fails on this host in both runs at the step 779 and 790 fail at (batches 2 and 4): it links a prebuilt library with the `g++` PATH names. Under the script that is an xlings subos g++ whose library records NEEDED libc.so, so the pack refuses it; under `mcpp test` the subos g++ does not find crti.o. tests/support gains readers for what the scripts asked of binutils, tar, zip and python3: ELF sections and symbol tables (defined or all), DT_SONAME and whether a search path is DT_RPATH or DT_RUNPATH; a tar archive's members and their extraction (by the machine's tar); a zip's central directory; SHA-256. Every assertion is kept. Where a reading changed: - `file`, readelf, nm and the scripts' python3 ELF readers (read_tag, has_symtab, elf_facts) are read from the files by these readers; 243's `ar t` is read from the archive (the script read it when ar worked). - python3 readings of JSON (723, 841, 869, 870, 954) are nlohmann::json; 954's tarfile/zipfile/hashlib readings are tar's member list, the zip central directory and SHA-256. - 871 compares compile intervals on the .ninja_log's absolute clock (each edge ends at its output's recorded mtime and begins its duration earlier), allowing 50 ms for the write that precedes a compiler's exit. `mcpp test` runs ninja twice (the program, then the tests) and each run appends with its own relative clock, so the script's comparison of the relative columns paired edges of different runs; on this host its awk compared them as strings and found nothing either way. Two compiles run at once here overlap by about 300 ms. - `find | head -1` is the first path in sorted order. - sed edits of a manifest (244's abi tag, 828, 1043, 1061) are the same edits on its text; 244's runs per line, as sed's does. - `cp -r` copies (638) keep links as links. --- ...provided_format_is_told_the_compression.sh | 79 -- ..._and_a_dotted_name_are_packed_as_stated.sh | 63 -- ...her_system_is_packed_without_running_it.sh | 99 -- .../215_pack_has_no_build_machine_paths.sh | 159 ---- ...6_selfcontained_refuses_host_capability.sh | 180 ---- .../242_pack_library_interface_and_headers.sh | 138 --- .../e2e/243_pack_library_interface_closure.sh | 146 --- tests/e2e/244_pack_library_gate.sh | 125 --- .../245_pack_library_fat_target_selection.sh | 122 --- .../e2e/249_pack_workspace_root_unchanged.sh | 71 -- tests/e2e/250_pack_names_the_target.sh | 73 -- tests/e2e/251_pack_library_shared.sh | 124 --- tests/e2e/252_pack_library_old_client.sh | 139 --- ...253_pack_library_undetermined_partition.sh | 146 --- .../e2e/254_pack_library_platform_coverage.sh | 138 --- .../261_pack_follows_the_module_extension.sh | 150 --- tests/e2e/264_pack_library_is_relocatable.sh | 155 --- tests/e2e/265_pack_strips_but_stays_usable.sh | 189 ---- tests/e2e/30_pack_modes.sh | 198 ---- tests/e2e/638_pack_format_dispatch.sh | 369 ------- .../649_pack_carries_runtime_deploy_files.sh | 43 - tests/e2e/652_an_application_is_a_kind.sh | 106 --- ...tages_declared_files_before_the_closure.sh | 160 ---- ...669_an_elf_host_pack_states_its_closure.sh | 126 --- tests/e2e/689_pack_takes_features.sh | 124 --- .../722_pack_strips_what_the_graph_built.sh | 210 ---- .../e2e/723_pack_reports_what_it_produced.sh | 118 --- ...cy_program_is_shipped_with_the_consumer.sh | 122 --- .../e2e/828_a_library_states_its_interface.sh | 90 -- tests/e2e/829_pack_takes_a_member.sh | 61 -- tests/e2e/841_pack_summarizes_many_outputs.sh | 82 -- ...ers_plans_builds_and_runs_programs_once.sh | 205 ---- ..._is_refused_before_anything_is_compiled.sh | 324 ------- ...members_names_each_member_in_its_report.sh | 183 ---- ...l_members_resolves_and_fails_per_member.sh | 131 --- ...ild_and_every_drive_takes_the_job_count.sh | 114 --- tests/e2e/954_pack_has_three_axes.sh | 168 ---- tests/e2e/MIGRATION.tsv | 74 +- tests/e2e/pack/formats.cpp | 897 ++++++++++++++++++ tests/e2e/pack/library.cpp | 839 ++++++++++++++++ tests/e2e/pack/members.cpp | 836 ++++++++++++++++ tests/e2e/pack/modes.cpp | 693 ++++++++++++++ tests/support/src/support.cpp | 172 +++- tests/support/src/support.cppm | 28 +- 44 files changed, 3496 insertions(+), 5273 deletions(-) delete mode 100755 tests/e2e/1008_a_provided_format_is_told_the_compression.sh delete mode 100755 tests/e2e/1043_several_programs_and_a_dotted_name_are_packed_as_stated.sh delete mode 100755 tests/e2e/1061_an_artifact_for_another_system_is_packed_without_running_it.sh delete mode 100755 tests/e2e/215_pack_has_no_build_machine_paths.sh delete mode 100755 tests/e2e/216_selfcontained_refuses_host_capability.sh delete mode 100755 tests/e2e/242_pack_library_interface_and_headers.sh delete mode 100755 tests/e2e/243_pack_library_interface_closure.sh delete mode 100755 tests/e2e/244_pack_library_gate.sh delete mode 100755 tests/e2e/245_pack_library_fat_target_selection.sh delete mode 100755 tests/e2e/249_pack_workspace_root_unchanged.sh delete mode 100755 tests/e2e/250_pack_names_the_target.sh delete mode 100755 tests/e2e/251_pack_library_shared.sh delete mode 100755 tests/e2e/252_pack_library_old_client.sh delete mode 100755 tests/e2e/253_pack_library_undetermined_partition.sh delete mode 100755 tests/e2e/254_pack_library_platform_coverage.sh delete mode 100755 tests/e2e/261_pack_follows_the_module_extension.sh delete mode 100755 tests/e2e/264_pack_library_is_relocatable.sh delete mode 100755 tests/e2e/265_pack_strips_but_stays_usable.sh delete mode 100755 tests/e2e/30_pack_modes.sh delete mode 100755 tests/e2e/638_pack_format_dispatch.sh delete mode 100755 tests/e2e/649_pack_carries_runtime_deploy_files.sh delete mode 100755 tests/e2e/652_an_application_is_a_kind.sh delete mode 100755 tests/e2e/662_pack_stages_declared_files_before_the_closure.sh delete mode 100755 tests/e2e/669_an_elf_host_pack_states_its_closure.sh delete mode 100644 tests/e2e/689_pack_takes_features.sh delete mode 100755 tests/e2e/722_pack_strips_what_the_graph_built.sh delete mode 100755 tests/e2e/723_pack_reports_what_it_produced.sh delete mode 100755 tests/e2e/801_a_dependency_program_is_shipped_with_the_consumer.sh delete mode 100755 tests/e2e/828_a_library_states_its_interface.sh delete mode 100755 tests/e2e/829_pack_takes_a_member.sh delete mode 100644 tests/e2e/841_pack_summarizes_many_outputs.sh delete mode 100644 tests/e2e/867_a_pack_over_several_members_plans_builds_and_runs_programs_once.sh delete mode 100644 tests/e2e/868_a_pack_over_several_members_is_refused_before_anything_is_compiled.sh delete mode 100644 tests/e2e/869_a_pack_over_several_members_names_each_member_in_its_report.sh delete mode 100644 tests/e2e/870_a_pack_over_several_members_resolves_and_fails_per_member.sh delete mode 100644 tests/e2e/871_a_pack_states_its_build_and_every_drive_takes_the_job_count.sh delete mode 100755 tests/e2e/954_pack_has_three_axes.sh create mode 100644 tests/e2e/pack/formats.cpp create mode 100644 tests/e2e/pack/library.cpp create mode 100644 tests/e2e/pack/members.cpp create mode 100644 tests/e2e/pack/modes.cpp diff --git a/tests/e2e/1008_a_provided_format_is_told_the_compression.sh b/tests/e2e/1008_a_provided_format_is_told_the_compression.sh deleted file mode 100755 index 3b35b7907..000000000 --- a/tests/e2e/1008_a_provided_format_is_told_the_compression.sh +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env bash -# requires: pack -# 1008 -- a format a package provides is told the compression the command or -# `[pack] compress` stated (MCPP_PACK_COMPRESS, `mcpp::pack_compress()`), and -# decides what its container does with it (2026.11.3.1). -# -# `--compress` was refused with every provided format ("writes none"), so a -# provider whose container can be compressed --- openkal-platform's okb, which -# stores its images compressed under `gzip` --- had no way to learn it. -# -# Legs: -# A. `--pkg-format probe --compress gzip:9`: the program reads `gzip:9`. -# B. `--pkg-format probe`: it reads nothing. -# C. `[pack] compress = "zstd"`: it reads `zstd`. -# D. `--pkg-format dir --compress gzip` is still refused: a directory writes -# no container. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } - -mkdir -p app/src -cat > app/mcpp.toml <<'EOF2' -[package] -name = "app" -version = "0.1.0" -standard = "c++20" - -[targets.app] -kind = "bin" -main = "src/main.cpp" -EOF2 -printf 'int main() { return 0; }\n' > app/src/main.cpp -cat > app/build.mcpp <<'EOF2' -import mcpp; -#include -#include -int main() { - mcpp::provides_pack_format("probe"); - if (std::string_view(mcpp::pack_format()) != "probe") return 0; - const std::string out = std::string(mcpp::out_dir()) + "/probe.txt"; - const std::string text = std::string("compress=") + mcpp::pack_compress(); - mcpp::action a; - a.id = "probe"; - a.role = "artifact"; - a.description = "probe"; - a.arg("/bin/sh").arg("-c").arg(("printf '%s' '" + text + "' > " + out).c_str()) - .input("${mcpp.target_file:app}") - .output(out.c_str()) - .submit(); - return 0; -} -EOF2 -cd app - -# ── A ─────────────────────────────────────────────────────────────────────── -"$MCPP" pack --pkg-format probe --compress gzip:9 > a.log 2>&1 || fail "A: pack failed" a.log -probe=$(find target -name probe.txt | head -1) -[ "$(cat "$probe")" = "compress=gzip:9" ] || fail "A: the program read '$(cat "$probe" 2>/dev/null)'" a.log -echo "ok: A, a provided format is told the compression the command states" - -# ── B ─────────────────────────────────────────────────────────────────────── -"$MCPP" pack --pkg-format probe > b.log 2>&1 || fail "B: pack failed" b.log -[ "$(cat "$probe")" = "compress=" ] || fail "B: with none stated the program read '$(cat "$probe")'" b.log -echo "ok: B, and nothing when none is stated" - -# ── C ─────────────────────────────────────────────────────────────────────── -printf '\n[pack]\ncompress = "zstd"\n' >> mcpp.toml -"$MCPP" pack --pkg-format probe > c.log 2>&1 || fail "C: pack failed" c.log -[ "$(cat "$probe")" = "compress=zstd" ] || fail "C: under [pack] compress the program read '$(cat "$probe")'" c.log -echo "ok: C, and the one [pack] compress states" - -# ── D ─────────────────────────────────────────────────────────────────────── -if "$MCPP" pack --pkg-format dir --compress gzip > d.log 2>&1; then fail "D: a directory was compressed" d.log; fi -grep -q "writes none" d.log || fail "D: the refusal" d.log -echo "ok: D, a directory writes no container and is refused" diff --git a/tests/e2e/1043_several_programs_and_a_dotted_name_are_packed_as_stated.sh b/tests/e2e/1043_several_programs_and_a_dotted_name_are_packed_as_stated.sh deleted file mode 100755 index 5f9eff268..000000000 --- a/tests/e2e/1043_several_programs_and_a_dotted_name_are_packed_as_stated.sh +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env bash -# requires: elf unix-shell -source "$(dirname "${BASH_SOURCE[0]}")/_toolchain_env.sh" -# 1043 -- several programs are packed as stated, and a program whose name has a dot is packed -# (mcpp#911 B1, B3). -# -# A package with several programs is not packed without a word: the one named after the package, -# else the first by name, is packed, and the others are said with the two ways to choose -# otherwise, `mcpp pack ` and `mcpp pack --all-programs`. A manifest `mcpp pack` -# generates quotes a target name with a dot (`COMMAND.COM`), so the generated project parses. -# -# Criteria: with none named after the package the first by name is packed and the others are said; -# `--all-programs` ships both under bin/; a single-file -# pack of `COMMAND.COM` builds its launcher and the file runs with the program's exit status. -set -e -t=$(mktemp -d); trap 'rm -rf "$t"' EXIT -fail() { echo "FAIL: $1"; [ -n "${2:-}" ] && printf '%s\n' "$2" | tail -20; exit 1; } -mkdir -p "$t/src" -cat > "$t/mcpp.toml" <<'EOF' -[package] -name = "multi" -version = "0.1.0" - -[targets.alpha] -kind = "bin" -main = "src/alpha.cpp" - -[targets."COMMAND.COM"] -kind = "bin" -main = "src/command.cpp" -EOF -echo 'int main() { return 0; }' > "$t/src/alpha.cpp" -echo 'int main() { return 3; }' > "$t/src/command.cpp" -cd "$t" - -out=$("$MCPP" pack 2>&1) || fail "several programs, none named after the package, were not packed" "$out" -grep -q "packing COMMAND.COM, the first by name, as none is named after the package" <<<"$out" \ - || fail "the program packed is not said" "$out" -grep -q "(alpha)" <<<"$out" && grep -q "mcpp pack --all-programs" <<<"$out" \ - || fail "the others and the ways to choose are not said" "$out" -rm -rf target/dist -echo " ok several programs, none named after the package: the first by name, the others said" - -out=$("$MCPP" pack --all-programs 2>&1) || fail "--all-programs did not pack" "$out" -listing=$(tar tzf target/dist/multi-0.1.0-*.tar.gz) -grep -q '/bin/alpha$' <<<"$listing" && grep -q '/bin/COMMAND.COM$' <<<"$listing" \ - || fail "--all-programs did not ship both programs" "$listing" -echo " ok --all-programs ships every program" - -rm -rf target/dist -out=$("$MCPP" pack COMMAND.COM --pkg-format single-file 2>&1) || fail "the single file of COMMAND.COM was not made" "$out" -file=$(find target/dist -maxdepth 1 -type f -name 'multi-0.1.0-*' | head -1) -[ -n "$file" ] || fail "no single file under target/dist" "$(ls target/dist)" -set +e; "$file"; rc=$?; set -e -[ "$rc" = 3 ] || fail "the single file exited $rc, not 3" -echo " ok a program named with a dot is packed into one file and runs" - -sed -i 's/^name = "multi"/name = "alpha"/' mcpp.toml -out=$("$MCPP" pack 2>&1) || fail "the program named after the package was not packed" "$out" -grep -q "packing alpha, the program named after the package; its other programs are not packed (COMMAND.COM)" <<<"$out" \ - || fail "the programs left out are not said" "$out" -echo " ok the program named after the package is packed and the others are said" -echo "PASS: 1043 several programs and a dotted name are packed as stated" diff --git a/tests/e2e/1061_an_artifact_for_another_system_is_packed_without_running_it.sh b/tests/e2e/1061_an_artifact_for_another_system_is_packed_without_running_it.sh deleted file mode 100755 index a0fcf583b..000000000 --- a/tests/e2e/1061_an_artifact_for_another_system_is_packed_without_running_it.sh +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env bash -# requires: elf llvm -source "$(dirname "${BASH_SOURCE[0]}")/_toolchain_env.sh" -# 1061 -- an artifact for another system is packed without running it (design 2026-11-10 E7; -# mcpp#911 N8; docs/10). -# -# A row whose C library a platform describes and whose programs run on another system: the -# vendored closure is read from the program's DT_NEEDED rather than from the host's loader (`ldd` -# of a FreeBSD program exited 127). A library this build produced travels with the program; every -# other name is the target system's. `--mode static` links the program statically on a named row -# too, and the file is asked before it is packed under that name. -# -# Criteria: `mcpp pack` (vendored) packs the program and its library and runs no host loader; -# `--mode static` packs a program that names neither a loader nor a shared library. -set -e -t=$(mktemp -d); trap 'rm -rf "$t"' EXIT -fail() { echo "FAIL: $1"; [ -n "${2:-}" ] && printf '%s\n' "$2" | tail -20; exit 1; } -mkdir -p "$t/plat" "$t/app/src" "$t/awaylib/src" -cat > "$t/plat/mcpp.toml" < "$t/awaylib/mcpp.toml" <<'TOML' -[package] -name = "awaylib" -version = "0.1.0" - -[targets.awaylib] -kind = "shared" -TOML -echo 'int away_value(void) { return 7; }' > "$t/awaylib/src/away.c" -cat > "$t/app/mcpp.toml" <<'TOML' -[package] -name = "away" -version = "0.1.0" - -[dependencies] -awaylib = { path = "../awaylib" } - -[targets.away] -kind = "bin" -main = "src/main.c" - -[target.x86_64-awayos] -platform = { path = "../plat" } -TOML -cat > "$t/app/src/main.c" <<'C' -int away_value(void); -void _start(void) { (void)away_value(); for (;;) {} } -C -cd "$t/app" -out=$("$MCPP" build --target x86_64-awayos 2>&1) || fail "the row did not build" "$out" -exe=$(find target/x86_64-awayos -path '*/bin/away' -type f | head -1) -[ -n "$exe" ] || fail "no program" "$out" -readelf -d "$exe" | grep -q NEEDED || fail "the program names no shared library, so this test tests nothing" "$(readelf -d "$exe")" -out=$("$MCPP" pack --target x86_64-awayos --all-programs 2>&1) || fail "the vendored pack failed" "$out" -grep -qi 'ldd failed' <<<"$out" && fail "the host's ldd was run on the artifact" "$out" -listing=$(tar tzf target/dist/away-0.1.0-x86_64-awayos.tar.gz) -grep -q '/bin/away$' <<<"$listing" || fail "the program is not packed" "$listing" -grep -qE 'libawaylib[^/]*\.so' <<<"$listing" || fail "the library the build produced does not travel" "$listing" -echo " ok the closure is read from the file, and the build's library travels" -# `--mode static` is a static program on a named row as well (N8: the program was packed dynamic -# under the `-static` name); the library, a static archive here, links into it. -sed -i 's/^kind = "shared"$/kind = "lib"/' "$t/awaylib/mcpp.toml" -out=$("$MCPP" pack --target x86_64-awayos --mode static 2>&1) || fail "the static pack failed" "$out" -static_exe=$(tar tzf target/dist/away-0.1.0-x86_64-awayos-static.tar.gz | grep '/bin/away$' | head -1) -tar xzf target/dist/away-0.1.0-x86_64-awayos-static.tar.gz -C "$t" "$static_exe" -if readelf -d "$t/$static_exe" 2>/dev/null | grep -q NEEDED || readelf -l "$t/$static_exe" | grep -q INTERP; then - fail "--mode static packed a dynamic program" "$(readelf -dl "$t/$static_exe" | head -20)" -fi -echo " ok --mode static packs a static program" -echo "PASS: 1061 an artifact for another system is packed without running it" diff --git a/tests/e2e/215_pack_has_no_build_machine_paths.sh b/tests/e2e/215_pack_has_no_build_machine_paths.sh deleted file mode 100755 index 1483fd2e2..000000000 --- a/tests/e2e/215_pack_has_no_build_machine_paths.sh +++ /dev/null @@ -1,159 +0,0 @@ -#!/usr/bin/env bash -# requires: pack patchelf elf python3 -# 215_pack_has_no_build_machine_paths.sh — a bundle must not depend on the -# machine that built it, and its executable must carry DT_RPATH. -# -# TWO DEFECTS, ONE FILE. -# -# 1. Only the main executable's search path was rewritten. Every bundled .so -# kept the RUNPATH it was LINKED with, and in this ecosystem that is a list -# of absolute paths into the build machine's xlings store: -# -# /xim-x-glibc/2.44/lib64 : /xim-x-gcc/16.1.0/lib64 -# : /compat-x-glx-runtime/…/lib : $ORIGIN -# -# "Depends on the xlings ecosystem" would be a design choice; "depends on -# THIS machine's store" is a defect — and it is invisible, because the -# bundle runs perfectly where it was built. -# -# 2. `patchelf --set-rpath` writes DT_RUNPATH by default. For an executable -# that is the graphics defect one layer later: DT_RUNPATH is not consulted -# for a dlopen() performed by another object, and every dlopen in the GL -# stack is performed by another object. -# -# The assertion sweeps EVERY ELF in the bundle, not just the binary — the -# first defect lived precisely in the ones nobody looked at. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -export MCPP_HOME=$HOME/.mcpp - -# The ELF reader lives in _elf_tag.sh — 264 asks the same question of a -# LIBRARY package, and two copies of this parser would be two definitions of -# what "carries a build-machine path" means. -source "$(dirname "$0")/_elf_tag.sh" - -# The bundle must actually CONTAIN a library, or this test sweeps one -# executable and proves nothing about the defect it exists for — which lived -# entirely in the objects nobody looked at. -# -# `force_bundle` on libgcc_s is the cheapest way to get one, and it is not a -# contrivance: it resolves to a file inside the build machine's xlings store, -# carrying that store's paths in its own RUNPATH. That is the defect's exact -# shape. (The project's own `kind = "shared"` target does NOT work here — mcpp -# links module objects into the executable, so it is never a DT_NEEDED.) -cd "$TMP" -"$MCPP" new bundled > /dev/null -cd bundled -cat >> mcpp.toml <<'EOF' - -[pack.bundle-project] -force_bundle = ["libgcc_s.so.1"] -EOF - -"$MCPP" pack > "$TMP/pack.log" 2>&1 || { cat "$TMP/pack.log"; exit 1; } - -TARBALL="$(ls target/dist/*.tar.gz | head -1)" -[[ -n "$TARBALL" ]] || { echo "FAIL: no tarball"; cat "$TMP/pack.log"; exit 1; } -mkdir -p "$TMP/x" && tar -xzf "$TARBALL" -C "$TMP/x" - -# EVERYTHING under the mcpp home is machine-local, not just the store. -# -# This used to be `$MCPP_HOME/registry/data/xpkgs`, which is a SUBSET of what -# the sweep claims to cover — and the gap was load-bearing. The SubOS farm -# (`/registry/subos//lib`) now lands in the artifact's DT_RPATH by -# design, and it sits outside `data/xpkgs`, so a leaked farm path would have -# walked straight past this guard while the file's own title says no -# build-machine paths survive. A check narrower than its claim reports "clean" -# for the one thing it cannot see. -STORE="$(cd "$MCPP_HOME" 2>/dev/null && pwd || true)" - -fail=0 -found_exe=0 -found_lib=0 -while IFS= read -r obj; do - head -c4 "$obj" 2>/dev/null | grep -q $'\x7fELF' || continue - read -r form tag paths <<<"$(read_tag "$obj")" - [[ "$form" == "NOT-ELF64" ]] && continue - printf ' %-40s %-14s %s %s\n' "${obj#$TMP/x/}" "$form" "$tag" "$paths" - - if [[ -n "$STORE" && "$paths" == *"$STORE"* ]]; then - echo "FAIL: bundled object still points at the BUILD MACHINE's store" - echo " $obj" - echo " $paths" - fail=1 - fi - if [[ "$form" == "executable" ]]; then - found_exe=1 - case "$tag" in - RPATH|NONE) ;; - *) - echo "FAIL: bundled executable carries $tag, contract requires DT_RPATH" - echo " a bundled vendor library cannot be reached through DT_RUNPATH" - echo " when the dlopen is performed by something else" - fail=1 - ;; - esac - else - found_lib=1 - case "$tag" in - RUNPATH|NONE) ;; - *) - echo "FAIL: bundled library carries $tag, contract requires DT_RUNPATH" - fail=1 - ;; - esac - fi -done < <(find "$TMP/x" -type f) - -[[ "$found_exe" == "1" ]] || { echo "FAIL: swept no executable — the test proved nothing"; exit 1; } -# The load-bearing coverage guard: the defect this file exists for was in the -# BUNDLED LIBRARIES, so a run that found none has not tested it. -[[ "$found_lib" == "1" ]] || { - echo "FAIL: the bundle contains no shared library, so the defect this test" - echo " targets (bundled .so keeping build-machine paths) was not exercised" - find "$TMP/x" -type f | sed "s|$TMP/x/||" - exit 1 -} -[[ "$fail" == "0" ]] || exit 1 - -# A program with no host capabilities must NOT get a requirements file: an -# empty one would be read as a claim that nothing is needed. -if find "$TMP/x" -name HOST-REQUIREMENTS | grep -q .; then - echo "FAIL: a program with no host capabilities got a HOST-REQUIREMENTS file" - exit 1 -fi - -# ── the mode that bundles NOTHING ─────────────────────────────────────────── -# -# `--mode system` is the one mode that ships no libraries, and until now no -# test packed with it — so nothing checked the one thing it must get right: -# an artifact that carries no dependencies must also carry no addressing for -# them. It links against a private glibc with the SubOS farm in its DT_RPATH, -# and both of those are this machine's alone. -"$MCPP" pack --mode system > "$TMP/pack-system.log" 2>&1 || { - cat "$TMP/pack-system.log"; exit 1; } -SYSTAR="$(ls target/dist/*-system.tar.gz | head -1)" -[[ -n "$SYSTAR" ]] || { echo "FAIL: no system-mode tarball"; exit 1; } -mkdir -p "$TMP/xs" && tar -xzf "$SYSTAR" -C "$TMP/xs" - -sysfail=0 -sysfound=0 -while IFS= read -r obj; do - head -c4 "$obj" 2>/dev/null | grep -q $'\x7fELF' || continue - read -r form tag paths <<<"$(read_tag "$obj")" - [[ "$form" == "NOT-ELF64" ]] && continue - sysfound=1 - printf ' [system] %-30s %-14s %s %s\n' "${obj#$TMP/xs/}" "$form" "$tag" "$paths" - if [[ -n "$STORE" && "$paths" == *"$STORE"* ]]; then - echo "FAIL: --mode system artifact points at the BUILD MACHINE" - echo " $obj" - echo " $paths" - sysfail=1 - fi -done < <(find "$TMP/xs" -type f) -[[ "$sysfound" == "1" ]] || { echo "FAIL: --mode system swept no ELF"; exit 1; } -[[ "$sysfail" == "0" ]] || exit 1 - -echo "PASS: no build-machine paths survive packing (vendored or system), and every object carries its contract tag" diff --git a/tests/e2e/216_selfcontained_refuses_host_capability.sh b/tests/e2e/216_selfcontained_refuses_host_capability.sh deleted file mode 100755 index d1a6f2fd1..000000000 --- a/tests/e2e/216_selfcontained_refuses_host_capability.sh +++ /dev/null @@ -1,180 +0,0 @@ -#!/usr/bin/env bash -# requires: pack patchelf elf -# 216_selfcontained_refuses_host_capability.sh — a bundle that carries its own -# libc cannot consume a capability the host must satisfy. -# -# WHY BOTH MODES -# -# `static` has no libc to share; `self-contained` brings its own. For a library -# the target must supply, the consequence is the same: that .so arrives with -# its own requirements on the HOST's libc, and the process does not have that -# libc. Measured in both directions as mcpp#392 / mcpp#401 — a private glibc -# meeting host-loaded objects dies during relocation, before main. -# -# The graphics drivers are the everyday case: they cannot be bundled at all -# (version-locked to the running kernel module; redistribution not permitted), -# so they are always the host's. -# -# Today both modes link and then fail at startup, or silently fall back to -# software rendering — which is worse than not building, because nothing says -# so. -# -# THE REFUSAL MUST NAME A WAY FORWARD. A check that only says no is a check -# users route around; `vendored` is the mode that actually works here, and the -# test asserts the message says so. -set -e -source "$(dirname "$0")/_host_path.sh" - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -export MCPP_HOME=$HOME/.mcpp - -# ── the capability comes from a DEPENDENCY, not from this project ─────────── -# -# THIS IS THE SHAPE REAL PROJECTS HAVE, and getting it wrong is how the first -# version of this gate shipped half-working. Almost no application declares -# `capability:opengl.glx.driver` itself — it depends on glfw / an SDL wrapper / -# a GL runtime that does, and the resolver stamps each requirement with its -# requester. A gate that reads the ROOT manifest answers "did the author write -# it down" (nearly always no) instead of "does the resolved graph need it". -# -# Measured on a real imgui project: `mcpp why runtime` listed -# `capability:opengl.glx.driver [run] <- compat.glfw@3.4 (required)`, and -# `--mode self-contained` packaged it happily — while THIS test passed, because -# its fixture declared the capability at the root. The fixture had the one -# shape real projects do not. -INDEX_DIR="$TMP/local-index" -# The manifest is read by mcpp, not by the shell, so the path has to be in HOST -# spelling — an MSYS `/c/...` written into mcpp.toml is a path Windows cannot -# open. `00_fixture_path_hygiene` enforces this statically. -INDEX_DIR_HOST="$(host_path "$INDEX_DIR")" -mkdir -p "$INDEX_DIR/pkgs/g" -cat > "$INDEX_DIR/pkgs/g/gfx-runtime.lua" <<'EOF' -package = { - spec = "1", - name = "gfx-runtime", - description = "A dependency that needs the host to provide a driver", - licenses = {"MIT"}, - type = "package", - xpm = { - linux = { - ["1.0.0"] = { - url = "https://example.invalid/gfx-runtime-1.0.0.tar.gz", - sha256 = "0000000000000000000000000000000000000000000000000000000000000000", - }, - }, - }, - mcpp = { - language = "c++23", - import_std = true, - sources = { "src/**/*.cppm" }, - targets = { ["gfx-runtime"] = { kind = "lib" } }, - deps = {}, - runtime = { - requirements = { - { kind = "capability", value = "opengl.glx.driver", phase = "run", - required = true, discovery = "rpath-of-dispatch" }, - }, - }, - }, -} -EOF - -mkdir -p "$TMP/gfxapp/src" -mkdir -p "$TMP/gfxapp/.mcpp/.xlings/data/xpkgs/local-dev.gfx-runtime/1.0.0/src" -cd "$TMP/gfxapp" -cat > .mcpp/.xlings/data/xpkgs/local-dev.gfx-runtime/1.0.0/src/lib.cppm <<'EOF' -export module gfx.runtime; -export int gfx_ready() { return 1; } -EOF -cat > src/main.cpp <<'EOF' -import gfx.runtime; -int main() { return gfx_ready() == 1 ? 0 : 1; } -EOF -# The application itself declares NOTHING. That is the point. -cat > mcpp.toml < "$TMP/build.log" 2>&1 || { cat "$TMP/build.log"; exit 1; } -"$MCPP" why runtime > "$TMP/why.log" 2>&1 || { cat "$TMP/why.log"; exit 1; } -grep -q 'capability:opengl.glx.driver' "$TMP/why.log" || { - echo "FAIL: the dependency's capability never reached the resolved graph —" - echo " this test would then prove nothing about the gate" - cat "$TMP/why.log" - exit 1 -} -grep -q 'gfx-runtime' "$TMP/why.log" || { - echo "FAIL: the requirement is not attributed to the dependency" - cat "$TMP/why.log" - exit 1 -} -echo " requirement arrives from the dependency, not from this project" - -# ── the two modes that carry their own libc must refuse ───────────────────── -for mode in self-contained static; do - if "$MCPP" pack --mode "$mode" > "$TMP/$mode.log" 2>&1; then - echo "FAIL: --mode $mode packed a program that needs a host capability" - echo " it would link and then fail at startup on the user's machine" - tail -20 "$TMP/$mode.log" - exit 1 - fi - grep -q 'opengl.glx.driver' "$TMP/$mode.log" || { - echo "FAIL: --mode $mode refused without naming the capability" - cat "$TMP/$mode.log" - exit 1 - } - grep -q 'vendored' "$TMP/$mode.log" || { - echo "FAIL: --mode $mode refused without naming a way forward" - echo " a refusal with no next step is a refusal users disable" - cat "$TMP/$mode.log" - exit 1 - } - echo " $mode: refused, named the capability and the alternative" -done - -# ── vendored must WORK, and must state what the host has to provide ───────── -"$MCPP" pack --mode vendored > "$TMP/vendored.log" 2>&1 || { - echo "FAIL: --mode vendored refused a program it can package" - cat "$TMP/vendored.log" - exit 1 -} -TARBALL="$(ls target/dist/*.tar.gz | head -1)" -mkdir -p "$TMP/x" && tar -xzf "$TARBALL" -C "$TMP/x" -REQ="$(find "$TMP/x" -name HOST-REQUIREMENTS | head -1)" -[[ -n "$REQ" ]] || { - echo "FAIL: vendored bundle does not state what the host must provide" - find "$TMP/x" -type f | sed "s|$TMP/x/||" - exit 1 -} -cat "$REQ" - -grep -q 'capability=opengl.glx.driver' "$REQ" || { - echo "FAIL: HOST-REQUIREMENTS does not list the capability" - exit 1 -} -# The discovery mechanism is the actionable half. GLX is found through the -# dispatch library's own DT_RPATH, while EGL is found through a JSON file whose -# library_path is ABSOLUTE — so "copy the directory over" fixes one and not the -# other. A row without it is not actionable. -grep -q 'discovery=rpath-of-dispatch' "$REQ" || { - echo "FAIL: HOST-REQUIREMENTS states the capability but not how it is found" - cat "$REQ" - exit 1 -} - -echo "PASS: modes that carry their own libc refuse host capabilities; vendored packages and declares them" diff --git a/tests/e2e/242_pack_library_interface_and_headers.sh b/tests/e2e/242_pack_library_interface_and_headers.sh deleted file mode 100755 index a9b7652a0..000000000 --- a/tests/e2e/242_pack_library_interface_and_headers.sh +++ /dev/null @@ -1,138 +0,0 @@ -#!/usr/bin/env bash -# requires: -# (no capability: "a library package works on every target" has to be TESTED on -# every target. `# requires: gcc` would skip this on macOS and Windows — Apple -# Clang is not the gcc capability — leaving the claim unverified while the -# suite stayed green.) -# -# Deliberately NO implementation partition here. This test is about the two -# interface modes; 243 owns partitions, and they do not build on Windows/clang -# today (a pre-existing gap this work found, see 243's header). -# 242_pack_library_interface_and_headers.sh — `mcpp pack ` produces -# a package a consumer can use through EITHER interface mode, or both at once. -# -# Acceptance for §1.1 / §2 of -# .agents/docs/2026-08-17-library-distribution-design.md: a package carries -# `include/` (text, consumed by #include) and `interface/` (module units the -# consumer compiles), the two do not interfere, and neither needs a flag. -# -# Also pins the two lists `mcpp pack` prints. A closed-source publisher needs -# to see what is NOT travelling as much as what is. -set -e -source "$(dirname "$0")/_host_path.sh" - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -# ── the producer: a module interface AND a C header, over one library ── -mkdir -p mathkit/src mathkit/include -cat > mathkit/src/mathkit.cppm <<'EOF' -export module mathkit; -export import :api; -EOF -cat > mathkit/src/api.cppm <<'EOF' -export module mathkit:api; -export namespace mk { int add(int a, int b); } -EOF -cat > mathkit/src/impl.cpp <<'EOF' -module mathkit; -namespace mk { int add(int a, int b) { return a + b; } } -EOF -cat > mathkit/src/capi.c <<'EOF' -int mathkit_add(int a, int b) { return a + b; } -EOF -cat > mathkit/include/mathkit_c.h <<'EOF' -#ifdef __cplusplus -extern "C" { -#endif -int mathkit_add(int a, int b); -#ifdef __cplusplus -} -#endif -EOF -cat > mathkit/mcpp.toml <<'EOF' -[package] -name = "mathkit" -version = "0.1.0" -[build] -sources = ["src/*.cppm", "src/*.cpp", "src/*.c"] -include_dirs = ["include"] -[targets.mathkit] -kind = "lib" -EOF - -cd mathkit -"$MCPP" pack mathkit > pack.log 2>&1 || { cat pack.log; echo "pack failed"; exit 1; } - -# The container as stated, compressed by mcpp itself; a single file is a -# program's, not a library's. -"$MCPP" pack mathkit --pkg-format tar --compress zstd > pack-zst.log 2>&1 || { cat pack-zst.log; echo "pack --compress zstd failed"; exit 1; } -zst="$(find target/dist -maxdepth 1 -type f -name 'mathkit-0.1.0-*.tar.zst' | head -1)" -[[ -n "$zst" ]] || { cat pack-zst.log; ls target/dist; echo "no .tar.zst for --compress zstd"; exit 1; } -[[ "$(head -c 4 "$zst" | od -An -tx1 | tr -d ' \n')" == "28b52ffd" ]] || { echo "$zst is not a zstd frame"; exit 1; } -"$MCPP" pack mathkit --pkg-format zip > pack-zip.log 2>&1 || { cat pack-zip.log; echo "pack --pkg-format zip failed"; exit 1; } -ls target/dist/mathkit-0.1.0-*.zip > /dev/null 2>&1 || { cat pack-zip.log; ls target/dist; echo "no zip for --pkg-format zip"; exit 1; } -if "$MCPP" pack mathkit --pkg-format single-file > pack-sf.log 2>&1; then - cat pack-sf.log; echo "a library was packed as a single file"; exit 1 -fi -grep -q "is a library" pack-sf.log || { cat pack-sf.log; echo "the single-file refusal does not say why"; exit 1; } -"$MCPP" pack mathkit > pack.log 2>&1 || { cat pack.log; echo "pack failed"; exit 1; } - -pkg="$(find target/dist -maxdepth 1 -type d -name 'mathkit-0.1.0-*' | head -1)" -# The manifest below is FILE CONTENT: on Git Bash a shell-spelled -# /tmp/... path is read by a native mcpp.exe as "root of the current -# drive". host_path is the conversion (tests/e2e/_host_path.sh). -PKG_HOST="$(host_path "$TMP/mathkit/$pkg")" -[[ -n "$pkg" ]] || { cat pack.log; echo "no package directory"; exit 1; } - -# The layout is the contract: two interface modes, one artifact dir per triple. -for f in mcpp.toml interface/mathkit.cppm interface/api.cppm include/mathkit_c.h; do - [[ -f "$pkg/$f" ]] || { echo "package is missing $f"; find "$pkg" -type f; exit 1; } -done -# NOT a hard-coded `libmathkit.a`: the artifact's name follows the ENVIRONMENT, -# not the OS — MinGW writes libfoo.a where MSVC writes foo.lib. That is exactly -# why `lib/` is keyed by triple, and hard-coding the GNU spelling here made this -# test fail on Windows against a package that had been built correctly. -[[ -n "$(find "$pkg/lib" -type f \( -name 'libmathkit.*' -o -name 'mathkit.*' \) | head -1)" ]] || { - echo "package has no artifact under lib//"; find "$pkg" -type f; exit 1; } - -# Both lists are printed, and the implementation partition is on the right one. -grep -q 'Interface.*mathkit.cppm' pack.log || { cat pack.log; echo "no interface list"; exit 1; } -grep -q 'Withheld.*impl.cpp' pack.log || { cat pack.log; echo "no withheld list"; exit 1; } - -cd "$TMP" - -# ── three consumers: header only, module only, both ──────────────────── -consume() { # $1 = name, $2 = main.cpp body - mkdir -p "$1/src" - printf '%s' "$2" > "$1/src/main.cpp" - cat > "$1/mcpp.toml" < run.log 2>&1 ) || { cat "$1/run.log"; echo "$1 failed"; exit 1; } - grep -q 'ok=5' "$1/run.log" || { cat "$1/run.log"; echo "$1 printed the wrong answer"; exit 1; } -} - -consume c_hdr '#include -#include -int main(){ std::printf("ok=%d\n", mathkit_add(2,3)); return 0; } -' -consume c_mod '#include -import mathkit; -int main(){ std::printf("ok=%d\n", mk::add(2,3)); return 0; } -' -consume c_both '#include -#include -import mathkit; -int main(){ std::printf("ok=%d\n", mathkit_add(2,3) + mk::add(2,3) - 5); return 0; } -' - -echo "PASS: a library package serves header, module, and both-at-once consumers" diff --git a/tests/e2e/243_pack_library_interface_closure.sh b/tests/e2e/243_pack_library_interface_closure.sh deleted file mode 100755 index d9baffafc..000000000 --- a/tests/e2e/243_pack_library_interface_closure.sh +++ /dev/null @@ -1,146 +0,0 @@ -#!/usr/bin/env bash -# requires: -# (no capability: this must hold on every target.) -# -# This test is also the regression for a scanner bug it uncovered. An -# IMPLEMENTATION PARTITION (`module M:part;`, no `export`) had no coverage -# anywhere in mcpp, and the scanner recorded it as "requires M:part, provides -# nothing" — so the file required its own name, the graph held no edge from the -# unit importing the partition to the unit defining it, and build order was -# unconstrained. GCC and macOS clang recovered via their own scan; Windows clang -# failed with `failed to read compiled module`. The scanner now models it, and -# the warning it used to print on every platform ("imported but not provided in -# this build") is gone. -# 243_pack_library_interface_closure.sh — what travels is the module closure of -# the published root, and the archive keeps exactly what the closure does not. -# -# Two asymmetric failures are pinned here, and only one of them is loud in the -# wild: -# -# * publishing TOO LITTLE fails in the consumer's compile, naming the module; -# * publishing TOO MUCH silently ships a closed-source implementation -# partition's SOURCE. Nothing fails. That is what this test is for. -# -# The archive side is the same closure used the other way round, and getting it -# wrong was measured: dropping every `.m.o` also drops the implementation -# partition's object, and every target then fails to link with an undefined -# reference nowhere near its cause. -set -e -source "$(dirname "$0")/_host_path.sh" - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -mkdir -p mathkit/src -cat > mathkit/src/mathkit.cppm <<'EOF' -export module mathkit; -export import :api; -EOF -cat > mathkit/src/api.cppm <<'EOF' -export module mathkit:api; -export namespace mk { int answer(); } -EOF -# An implementation partition: it produces a BMI and a `.m.o`, and its source -# is the thing a closed-source publisher must not ship. -cat > mathkit/src/secret.cppm <<'EOF' -module mathkit:secret; -namespace mk { int secret_bias() { return 40; } } -EOF -cat > mathkit/src/impl.cpp <<'EOF' -module mathkit; -namespace mk { -int secret_bias(); -int answer() { return secret_bias() + 2; } -} -EOF -cat > mathkit/mcpp.toml <<'EOF' -[package] -name = "mathkit" -version = "0.1.0" -[build] -sources = ["src/*.cppm", "src/*.cpp"] -[targets.mathkit] -kind = "lib" -EOF - -cd mathkit -"$MCPP" pack mathkit > pack.log 2>&1 || { cat pack.log; echo "pack failed"; exit 1; } -pkg="$(find target/dist -maxdepth 1 -type d -name 'mathkit-0.1.0-*' | head -1)" -# The manifest below is FILE CONTENT: on Git Bash a shell-spelled -# /tmp/... path is read by a native mcpp.exe as "root of the current -# drive". host_path is the conversion (tests/e2e/_host_path.sh). -PKG_HOST="$(host_path "$TMP/mathkit/$pkg")" - -# ── the confidentiality criterion ────────────────────────────────────── -[[ -f "$pkg/interface/mathkit.cppm" ]] || { echo "root interface not published"; exit 1; } -[[ -f "$pkg/interface/api.cppm" ]] || { echo "interface partition not published"; exit 1; } -[[ ! -e "$pkg/interface/secret.cppm" ]] || { - echo "LEAK: the implementation partition's source was published"; exit 1; } -grep -RIl 'secret_bias' "$pkg/interface" "$pkg/include" 2>/dev/null | grep -q . && { - echo "LEAK: implementation source text found in the published interface"; exit 1; } - -# ── the archive criterion: published objects out, everything else in ──── -# A functional probe, not `command -v ar`: on macOS `ar` resolves to an xlings -# shim that reports "not installed" and exits non-zero, which under `set -e` -# kills the test instead of skipping the inspection. -archive="$(find "$pkg/lib" -type f \( -name 'libmathkit.*' -o -name 'mathkit.*' \) | head -1)" -if [[ -n "$archive" ]] && members="$(ar t "$archive" 2>/dev/null)"; then - echo "$members" | grep -q 'secret.m.o' || { - echo "the implementation partition's OBJECT was dropped; nothing would link" - echo "$members"; exit 1; } - echo "$members" | grep -q 'mathkit.m.o' && { - echo "a published interface unit's object is still in the archive" - echo "$members"; exit 1; } - echo "$members" | grep -q 'api.m.o' && { - echo "a published interface unit's object is still in the archive" - echo "$members"; exit 1; } -fi - -# ── the hazard case: an interface that reaches the partition ──────────── -# -# Legal, and the consumer needs that source to build the BMI at all — so it is -# published rather than refused. But for a closed-source library it is the one -# outcome nobody wants by accident, and `import :secret;` in an interface looks -# like any other import, so `mcpp pack` has to say it. -cd "$TMP/mathkit" -cp src/mathkit.cppm "$TMP/root.bak" -printf 'import :secret;\n' >> src/mathkit.cppm -rm -rf target -"$MCPP" pack mathkit > hazard.log 2>&1 || { cat hazard.log; echo "hazard pack failed"; exit 1; } -grep -q 'secret.cppm is an implementation partition' hazard.log || { - cat hazard.log - echo "FAIL: publishing an implementation partition's source was not reported" - exit 1; } -hazpkg="$(find target/dist -maxdepth 1 -type d -name 'mathkit-0.1.0-*' | head -1)" -[[ -e "$hazpkg/interface/secret.cppm" ]] || { - echo "the warning fired but the source was not actually published"; exit 1; } -cp "$TMP/root.bak" src/mathkit.cppm -rm -rf target -"$MCPP" pack mathkit > repack.log 2>&1 || { cat repack.log; echo "repack failed"; exit 1; } -pkg="$(find target/dist -maxdepth 1 -type d -name 'mathkit-0.1.0-*' | head -1)" -[[ ! -e "$pkg/interface/secret.cppm" ]] || { echo "restore did not withhold it again"; exit 1; } -PKG_HOST="$(host_path "$TMP/mathkit/$pkg")" - -# ── and it still links and runs ───────────────────────────────────────── -cd "$TMP" -mkdir -p app/src -cat > app/src/main.cpp <<'EOF' -#include -import mathkit; -int main(){ std::printf("ok=%d\n", mk::answer()); return 0; } -EOF -cat > app/mcpp.toml < run.log 2>&1 ) || { cat app/run.log; echo "consumer failed"; exit 1; } -grep -q 'ok=42' app/run.log || { cat app/run.log; echo "wrong answer"; exit 1; } - -echo "PASS: the closure decides both what is published and what is dropped" diff --git a/tests/e2e/244_pack_library_gate.sh b/tests/e2e/244_pack_library_gate.sh deleted file mode 100755 index 04c26e808..000000000 --- a/tests/e2e/244_pack_library_gate.sh +++ /dev/null @@ -1,125 +0,0 @@ -#!/usr/bin/env bash -# requires: -# (no capability: the refusals have to hold on every platform, so this test has -# to RUN on every platform.) -# 244_pack_library_gate.sh — the three refusals a prebuilt package must make. -# -# THE FIRST ONE IS WHY THIS FEATURE HAS A GATE AT ALL. Measured before it -# existed, on a real build: change one line of a shipped interface — swap two -# `int` members of a struct, which the Itanium ABI does not mangle — and the -# consumer compiles, links, runs, and prints transposed data. Exit code 0. No -# diagnostic at any stage, from any tool. -# -# 1. an edited interface → refuse (the silent-wrong-data case) -# 2. an artifact for another toolchain → refuse, and LIST the tags it has -# 3. `mcpp build` inside the package → refuse (it would "succeed" emptily) -set -e -source "$(dirname "$0")/_host_path.sh" - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -mkdir -p mathkit/src -cat > mathkit/src/mathkit.cppm <<'EOF' -export module mathkit; -export namespace mk { int answer(); } -EOF -cat > mathkit/src/impl.cpp <<'EOF' -module mathkit; -namespace mk { int answer() { return 42; } } -EOF -cat > mathkit/mcpp.toml <<'EOF' -[package] -name = "mathkit" -version = "0.1.0" -[build] -sources = ["src/*.cppm", "src/*.cpp"] -[targets.mathkit] -kind = "lib" -EOF - -cd mathkit -"$MCPP" pack mathkit > pack.log 2>&1 || { cat pack.log; echo "pack failed"; exit 1; } -pkg="$TMP/mathkit/$(find target/dist -maxdepth 1 -type d -name 'mathkit-0.1.0-*' | head -1)" -# The manifest below is FILE CONTENT: on Git Bash a shell-spelled -# /tmp/... path is read by a native mcpp.exe as "root of the current -# drive". host_path is the conversion (tests/e2e/_host_path.sh). -PKG_HOST="$(host_path "$pkg")" -cd "$TMP" - -mkdir -p app/src -cat > app/src/main.cpp <<'EOF' -#include -import mathkit; -int main(){ std::printf("ok=%d\n", mk::answer()); return 0; } -EOF -cat > app/mcpp.toml < ok.log 2>&1 ) || { cat app/ok.log; echo "baseline failed"; exit 1; } -grep -q 'ok=42' app/ok.log || { cat app/ok.log; echo "baseline wrong answer"; exit 1; } - -# ── 1. an edited interface is refused ────────────────────────────────── -cp "$pkg/interface/mathkit.cppm" "$TMP/interface.bak" -printf '\n// tampered\n' >> "$pkg/interface/mathkit.cppm" -rm -rf app/target -if ( cd app && "$MCPP" build > tamper.log 2>&1 ); then - cat app/tamper.log - echo "FAIL: an edited interface built anyway — this is the silent-wrong-data case" - exit 1 -fi -grep -q 'does not match what was packaged' app/tamper.log || { - cat app/tamper.log; echo "refused, but not for the interface digest"; exit 1; } -cp "$TMP/interface.bak" "$pkg/interface/mathkit.cppm" - -# ── 2. a foreign toolchain tag is refused, and the real tags are shown ── -cp "$pkg/mcpp.toml" "$TMP/manifest.bak" -# Forge the STDLIB segment, whatever it is called. Hard-coding `gcc` would have -# made this a no-op on macOS (llvm) and Windows (msvc) — the tag would stay -# valid, the package would be correctly accepted, and the test would report -# "a package built for another compiler was accepted" against a product that -# did nothing wrong. -sed -i.bak -E 's/(abi[[:space:]]*=[[:space:]]*"[^"]*-)[a-z]+[0-9]+(-)/\1forgedstl99\2/' "$pkg/mcpp.toml" -rm -rf app/target -if ( cd app && "$MCPP" build > tag.log 2>&1 ); then - cat app/tag.log - echo "FAIL: a package built for another compiler was accepted" - exit 1 -fi -grep -q 'no prebuilt artifact matches this toolchain' app/tag.log || { - cat app/tag.log; echo "refused, but not for the abi tag"; exit 1; } -# The diagnostic has to say what IS available — a refusal the reader cannot act -# on sends them looking for a package that is right in front of them. -grep -q 'published tags' app/tag.log || { - cat app/tag.log; echo "the refusal did not list the published tags"; exit 1; } -grep -q 'forgedstl99' app/tag.log || { - cat app/tag.log; echo "the refusal did not name the tag it found"; exit 1; } -cp "$TMP/manifest.bak" "$pkg/mcpp.toml" - -# ── 3. building INSIDE the package is refused ────────────────────────── -if ( cd "$pkg" && "$MCPP" build > "$TMP/inside.log" 2>&1 ); then - cat "$TMP/inside.log" - echo "FAIL: building inside a distribution package 'succeeded' — it compiles" - echo " declarations, links nothing, and reports Finished" - exit 1 -fi -grep -q 'distribution package produced by' "$TMP/inside.log" || { - cat "$TMP/inside.log"; echo "refused, but not as a distribution package"; exit 1; } - -# ── and the restored package still builds ────────────────────────────── -rm -rf app/target -( cd app && "$MCPP" run > final.log 2>&1 ) || { cat app/final.log; echo "restore failed"; exit 1; } -grep -q 'ok=42' app/final.log || { cat app/final.log; echo "restore wrong answer"; exit 1; } - -echo "PASS: interface tamper, tag mismatch, and in-package build are all refused" diff --git a/tests/e2e/245_pack_library_fat_target_selection.sh b/tests/e2e/245_pack_library_fat_target_selection.sh deleted file mode 100755 index b337aee76..000000000 --- a/tests/e2e/245_pack_library_fat_target_selection.sh +++ /dev/null @@ -1,122 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc musl -# 245_pack_library_fat_target_selection.sh — one package, several targets, and -# each build picks exactly its own leg. -# -# `musl` is now declared as well as `gcc`. It always needed both — the pack below -# asks for a musl leg outright — and CI was green only because the payload -# happens to be warm there. A requirement the test does not declare is one that -# turns into a confusing failure the day it stops being true. -# -# The Windows counterpart (msvc + mingw legs in one package) is 256; `# requires:` -# cannot express "gcc+musl OR msvc+mingw", and a host that has neither should -# skip rather than half-run. -# -# THE PREDICATE IS THE POINT. Each leg is selected by a generated -# `cfg(all(arch=…, os=…, env=…))` block and NOT by a bare `[target.'']` -# key. The bare form only matched when `--target` was passed: a plain -# `mcpp build` resolved the host and compared it against an empty string, so -# the section was silently inert. That shape is the worst kind — CI passes -# `--target` and is green, the developer's own build drops the flags, and the -# failure arrives at the linker naming a symbol instead of a predicate. -# -# So this test builds the NATIVE case as well as the explicit ones, and asserts -# the selected directory each time. -# -# gnu + musl and not gnu + windows: CI warms both of those toolchains, so this -# test RUNS rather than skipping. A `# requires: mingw-cross` here would have -# made the whole fat-package mechanism unverified on every ordinary CI run -# while still reporting a green suite. The PE leg is covered by 248. -set -e -source "$(dirname "$0")/_host_path.sh" - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -mkdir -p mathkit/src -cat > mathkit/src/mathkit.cppm <<'EOF' -export module mathkit; -export namespace mk { int answer(); } -EOF -cat > mathkit/src/impl.cpp <<'EOF' -module mathkit; -namespace mk { int answer() { return 42; } } -EOF -cat > mathkit/mcpp.toml <<'EOF' -[package] -name = "mathkit" -version = "0.1.0" -[build] -sources = ["src/*.cppm", "src/*.cpp"] -[targets.mathkit] -kind = "lib" -EOF - -cd mathkit -"$MCPP" pack mathkit \ - --target x86_64-linux-gnu \ - --target x86_64-linux-musl > pack.log 2>&1 \ - || { cat pack.log; echo "fat pack failed"; exit 1; } - -pkg="$TMP/mathkit/target/dist/mathkit-0.1.0" -# The manifest below is FILE CONTENT: on Git Bash a shell-spelled -# /tmp/... path is read by a native mcpp.exe as "root of the current -# drive". host_path is the conversion (tests/e2e/_host_path.sh). -PKG_HOST="$(host_path "$pkg")" -for t in x86_64-linux-gnu x86_64-linux-musl; do - [[ -n "$(find "$pkg/lib/$t" -name 'libmathkit.a' | head -1)" ]] || { - echo "no artifact for $t"; find "$pkg" -type f; exit 1; } -done - -# The generated blocks must be cfg(...), never a bare triple. -grep -q "target\.'cfg(" "$pkg/mcpp.toml" || { - cat "$pkg/mcpp.toml"; echo "legs are not selected by cfg() predicates"; exit 1; } -grep -qE "^\[target\.'x86_64-" "$pkg/mcpp.toml" && { - cat "$pkg/mcpp.toml" - echo "a leg is selected by a BARE TRIPLE, which is inert on a native build" - exit 1; } - -cd "$TMP" -mkdir -p app/src -cat > app/src/main.cpp <<'EOF' -#include -import mathkit; -int main(){ std::printf("ok=%d\n", mk::answer()); return 0; } -EOF -cat > app/mcpp.toml < "$TMP/$label.log" 2>&1 ) \ - || { cat "$TMP/$label.log"; echo "$label build failed"; exit 1; } - local nj; nj="$(find app/target -name build.ninja | head -1)" - # Separator-agnostic and anchored on the PACKAGE name: a native mcpp.exe - # writes native separators into build.ninja, so a `dist/…/lib/…` pattern - # matches nothing there and the failure reads like a packaging bug. Same - # helper as 256, which is where that was measured. - grep -oE "mathkit-0\.1\.0[\\/]lib[\\/][A-Za-z0-9_-]+" "$nj" \ - | sed 's|.*[\\/]||' | sort -u > "$TMP/$label.legs" - [[ "$(wc -l < "$TMP/$label.legs")" -eq 1 ]] || { - echo "$label saw $(wc -l < "$TMP/$label.legs") leg(s), expected exactly 1:" - cat "$TMP/$label.legs"; exit 1; } - grep -qx "$want" "$TMP/$label.legs" || { - echo "$label picked the wrong leg:"; cat "$TMP/$label.legs"; exit 1; } -} - -# The native build is the one the bare-triple form used to get wrong. -check native x86_64-linux-gnu -check gnu x86_64-linux-gnu --target x86_64-linux-gnu -check musl x86_64-linux-musl --target x86_64-linux-musl - -echo "PASS: a fat package selects one leg per target, native build included" diff --git a/tests/e2e/249_pack_workspace_root_unchanged.sh b/tests/e2e/249_pack_workspace_root_unchanged.sh deleted file mode 100755 index c2a982830..000000000 --- a/tests/e2e/249_pack_workspace_root_unchanged.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bash -# requires: -# (no capability: a library package is claimed to work on every target, so this -# test has to RUN on every platform. `# requires: gcc` would have skipped it on -# macOS and Windows — Apple Clang is not the gcc capability — leaving the claim -# unverified while the suite stayed green.) -# 249_pack_workspace_root_unchanged.sh — `mcpp pack` in a workspace root still -# packs the member's program. -# -# Routing `mcpp pack` through `[targets.].kind` means something has to read -# the manifest before the build does. A workspace root has no targets of its -# own — a virtual one has no `[package]` either — so the first version of that -# routing read the root's empty target list and concluded there was nothing to -# pack, turning a working command into an error. -# -# Caught by running the previous release against examples/04-workspace and -# comparing. This test is that comparison, made permanent. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -mkdir -p ws/apps/hello/src ws/libs/mathcore/src - -cat > ws/mcpp.toml <<'EOF' -[workspace] -members = ["libs/mathcore", "apps/hello"] -EOF - -cat > ws/libs/mathcore/mcpp.toml <<'EOF' -[package] -name = "mathcore" -version = "0.1.0" -EOF -cat > ws/libs/mathcore/src/mathcore.cppm <<'EOF' -export module mathcore; -export int core_answer() { return 42; } -EOF - -cat > ws/apps/hello/mcpp.toml <<'EOF' -[package] -name = "hello" -version = "0.1.0" -[dependencies] -mathcore = { path = "../../libs/mathcore" } -EOF -cat > ws/apps/hello/src/main.cpp <<'EOF' -#include -import mathcore; -int main() { std::printf("ok=%d\n", core_answer()); return 0; } -EOF - -cd ws - -# The same legs on every host. A Mach-O program packs as the others do -# (2026.9.14.2+, its closure read from its load commands); the macOS branch -# that asserted the earlier refusal naming the member's program is gone. - -"$MCPP" pack --mode system > pack.log 2>&1 || { - cat pack.log - echo "FAIL: packing from a virtual workspace root stopped working" - exit 1 -} -grep -q 'Packed' pack.log || { cat pack.log; echo "no archive reported"; exit 1; } -# .tar.gz everywhere except a Windows target, which produces a .zip — the -# archive format follows the artifact, not the packer. -[[ -n "$(find . -name 'hello-0.1.0-*.tar.gz' -o -name 'hello-0.1.0-*.zip' | head -1)" ]] || { - cat pack.log; echo "the member's archive was not produced"; find . -name 'hello-0.1.0-*'; exit 1; } - -echo "PASS: a workspace root still packs its member's program" diff --git a/tests/e2e/250_pack_names_the_target.sh b/tests/e2e/250_pack_names_the_target.sh deleted file mode 100755 index bb7c4264a..000000000 --- a/tests/e2e/250_pack_names_the_target.sh +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env bash -# requires: -# (no capability: a library package is claimed to work on every target, so this -# test has to RUN on every platform. `# requires: gcc` would have skipped it on -# macOS and Windows — Apple Clang is not the gcc capability — leaving the claim -# unverified while the suite stayed green.) -# 250_pack_names_the_target.sh — `mcpp pack ` packs the target it was -# given, and refuses a name it cannot pack. -# -# The positional is what decides application-bundle vs library-package, so it -# has to reach BOTH pipelines. It did not reach the application one at first: -# a project with two `bin` targets accepted `mcpp pack app2` and bundled app1, -# which is the shape where the command succeeds and the answer is wrong. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -mkdir -p two/src -cat > two/src/alpha.cpp <<'EOF' -#include -int main() { std::printf("alpha\n"); return 0; } -EOF -cat > two/src/beta.cpp <<'EOF' -#include -int main() { std::printf("beta\n"); return 0; } -EOF -cat > two/mcpp.toml <<'EOF' -[package] -name = "two" -version = "0.1.0" - -[targets.alpha] -kind = "bin" -main = "src/alpha.cpp" - -[targets.beta] -kind = "bin" -main = "src/beta.cpp" -EOF - -cd two - -# The same legs on every host. A Mach-O program packs as the others do -# (2026.9.14.2+), so the named program is looked for in the staged tree on -# macOS too, rather than in the name an earlier refusal carried. - -# ── the named program is the one that gets bundled ───────────────────── -"$MCPP" pack beta --mode system > beta.log 2>&1 || { cat beta.log; echo "pack beta failed"; exit 1; } -staged="$(find target/dist -maxdepth 1 -type d -name 'two-0.1.0*' | head -1)" -[[ -n "$staged" ]] || { cat beta.log; echo "no staging dir"; exit 1; } -find "$staged" -type f -name 'beta*' | grep -q . || { - echo "FAIL: 'mcpp pack beta' did not bundle beta"; find "$staged" -type f; exit 1; } -find "$staged" -type f -name 'alpha*' | grep -q . && { - echo "FAIL: 'mcpp pack beta' bundled alpha instead"; find "$staged" -type f; exit 1; } - -# ── and the other one, to prove the first result was not the default ─── -rm -rf target/dist -"$MCPP" pack alpha --mode system > alpha.log 2>&1 || { cat alpha.log; echo "pack alpha failed"; exit 1; } -staged="$(find target/dist -maxdepth 1 -type d -name 'two-0.1.0*' | head -1)" -find "$staged" -type f -name 'alpha*' | grep -q . || { - echo "FAIL: 'mcpp pack alpha' did not bundle alpha"; find "$staged" -type f; exit 1; } - -# ── an unknown name is refused, and says what there is ───────────────── -if "$MCPP" pack nosuch --mode system > bad.log 2>&1; then - cat bad.log; echo "FAIL: an unknown target name was accepted"; exit 1 -fi -grep -q "no target named 'nosuch'" bad.log || { cat bad.log; echo "wrong refusal"; exit 1; } -grep -q 'alpha' bad.log && grep -q 'beta' bad.log || { - cat bad.log; echo "the refusal did not list the available targets"; exit 1; } - -echo "PASS: mcpp pack packs the target it is given" diff --git a/tests/e2e/251_pack_library_shared.sh b/tests/e2e/251_pack_library_shared.sh deleted file mode 100755 index 26ca02880..000000000 --- a/tests/e2e/251_pack_library_shared.sh +++ /dev/null @@ -1,124 +0,0 @@ -#!/usr/bin/env bash -# requires: -# 251_pack_library_shared.sh — a `kind = "shared"` package is either produced -# CORRECTLY or refused CLEARLY. Never silently wrong. -# -# Both halves, because testing only the working one cannot tell "the gate is -# handled" from "there is no gate". Which half applies is now a property of the -# TARGET, not of "is it Linux": -# -# ELF, Mach-O, PE/MinGW, PE/MSVC produced — the package carries every name -# the platform needs to link it and to find -# it later -# -# Every format mcpp targets now produces one. MSVC was the last holdout, and -# what it was missing was never the linker: it exports nothing from a DLL -# without `__declspec(dllexport)` or a `.def`, so mcpp generates the `.def` from -# the objects (258 pins that path in detail, 259 pins Mach-O relocatability, 257 -# the PE/MinGW one). What this file adds is that one fixture and one command -# produce a usable package on whichever platform it runs. -# -# A shared library is LINKED by `lib.so` and FOUND at run time by its -# SONAME, and those are two different filenames. The first version of this -# packer shipped only the built file: the consumer linked, and then mcpp's own -# runtime-closure check reported -# -# libmathkit.so.1 not found on the search path this artifact will actually use -# -# which is the good outcome only because that check exists. Without it the -# program would have failed to start with a loader error naming a file the user -# never asked for. -set -e -source "$(dirname "$0")/_host_path.sh" - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -mkdir -p mathkit/src -cat > mathkit/src/mathkit.cppm <<'EOF' -export module mathkit; -export namespace mk { int answer(); } -EOF -cat > mathkit/src/impl.cpp <<'EOF' -module mathkit; -namespace mk { int answer() { return 42; } } -EOF -cat > mathkit/mcpp.toml <<'EOF' -[package] -name = "mathkit" -version = "0.1.0" -[build] -sources = ["src/*.cppm", "src/*.cpp"] -[targets.mathkit-shared] -kind = "shared" -soname = "libmathkit.so.1" -EOF - -cd mathkit - -# ── Windows and macOS: it is produced; the deep claims live in 258 / 259 ── -if [[ "$(uname -s)" != "Linux" ]]; then - "$MCPP" pack mathkit-shared > pack.log 2>&1 \ - || { cat pack.log; echo "FAIL: shared pack failed off ELF"; exit 1; } - nonelf="$(find target/dist -maxdepth 1 -type d -name 'mathkit-0.1.0-*' | head -1)" - # `.dylib` on macOS, `.dll` on Windows — asked for by shape rather than by - # `uname`, so the assertion is about the artifact and not about the host. - [[ -n "$(find "$nonelf" \( -name '*.dylib' -o -name '*.dll' \) | head -1)" ]] || { - find "$nonelf" \( -type f -o -type l \) - echo "FAIL: no shared library in the package"; exit 1; } - echo "PASS: a shared library package is produced off ELF too" - exit 0 -fi - -# ── the ELF side: produce it, with both names ────────────────────────── -"$MCPP" pack mathkit-shared > pack.log 2>&1 || { cat pack.log; echo "shared pack failed"; exit 1; } -pkg="$TMP/mathkit/$(find target/dist -maxdepth 1 -type d -name 'mathkit-0.1.0-*' | head -1)" -# The manifest below is FILE CONTENT: on Git Bash a shell-spelled -# /tmp/... path is read by a native mcpp.exe as "root of the current -# drive". host_path is the conversion (tests/e2e/_host_path.sh). -PKG_HOST="$(host_path "$pkg")" - -libdir="$(dirname "$(find "$pkg/lib" -name 'libmathkit-shared.so' | head -1)")" -[[ -n "$libdir" ]] || { echo "no .so in the package"; find "$pkg" -type f; exit 1; } -# Both names. The SONAME one may be a symlink or a copy — either is fine, its -# absence is not. -[[ -e "$libdir/libmathkit.so.1" ]] || { - echo "FAIL: the package does not carry the SONAME the loader will ask for" - ls -l "$libdir"; exit 1; } - -# The manifest declares it as a shared library and gives a runtime search dir: -# link_library_dirs is not rpath, and a shared package needs both. -grep -q 'role *= *"shared-library"' "$pkg/mcpp.toml" || { - cat "$pkg/mcpp.toml"; echo "artifact is not recorded as a shared library"; exit 1; } -grep -q 'runtime_search_dirs' "$pkg/mcpp.toml" || { - cat "$pkg/mcpp.toml"; echo "no runtime_search_dirs — the consumer could not find it"; exit 1; } - -cd "$TMP" -mkdir -p app/src -cat > app/src/main.cpp <<'EOF' -#include -import mathkit; -int main(){ std::printf("ok=%d\n", mk::answer()); return 0; } -EOF -cat > app/mcpp.toml < run.log 2>&1 ) || { cat app/run.log; echo "consumer failed to run"; exit 1; } -grep -q 'ok=42' app/run.log || { cat app/run.log; echo "wrong answer"; exit 1; } - -exe="$(find app/target -name app -type f | head -1)" -readelf -d "$exe" 2>/dev/null | grep -q 'libmathkit.so.1' || { - readelf -d "$exe"; echo "the consumer does not NEED the soname"; exit 1; } - -echo "PASS: a shared library package carries both names and the consumer starts" diff --git a/tests/e2e/252_pack_library_old_client.sh b/tests/e2e/252_pack_library_old_client.sh deleted file mode 100755 index f220c7bc0..000000000 --- a/tests/e2e/252_pack_library_old_client.sh +++ /dev/null @@ -1,139 +0,0 @@ -#!/usr/bin/env bash -# requires: -# 252_pack_library_old_client.sh — an mcpp that predates library packaging must -# still BUILD against a package produced by one that has it. -# -# That claim is the reason the generated manifest introduces no section and no -# key: everything in it — `sources`, `include_dirs`, `[modules] exports`, a -# `cfg(...)` block per leg, `[[runtime.artifacts]]` — was already parsed before -# this feature existed. An older client reads the package and links it; what it -# does not do is run the gates, because it has no way to know that -# `provenance = "mcpp-pack …"` means anything. -# -# Two halves, because only one of them can run everywhere: -# -# 1. STATIC — the generated manifest's top-level sections are a subset of the -# vocabulary that predates this feature. Portable, and it is the actual -# invariant rather than a proxy for it. -# 2. REAL — consume the package with $MCPP_BOOT, the released mcpp each CI job -# bootstraps from. Skipped with a loud note when that is not available, -# never silently. -set -e -source "$(dirname "$0")/_host_path.sh" - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -mkdir -p mathkit/src -cat > mathkit/src/mathkit.cppm <<'EOF' -export module mathkit; -export namespace mk { int answer(); } -EOF -cat > mathkit/src/impl.cpp <<'EOF' -module mathkit; -namespace mk { int answer() { return 42; } } -EOF -cat > mathkit/mcpp.toml <<'EOF' -[package] -name = "mathkit" -version = "0.1.0" -[build] -sources = ["src/*.cppm", "src/*.cpp"] -[targets.mathkit] -kind = "lib" -EOF - -cd mathkit -"$MCPP" pack mathkit > pack.log 2>&1 || { cat pack.log; echo "pack failed"; exit 1; } -pkg="$TMP/mathkit/$(find target/dist -maxdepth 1 -type d -name 'mathkit-0.1.0-*' | head -1)" -PKG_HOST="$(host_path "$pkg")" - -# ── 1. no section outside the pre-existing vocabulary ────────────────── -# -# Listed literally rather than derived: the point is that this set was frozen -# before the feature, so a new entry has to be added here deliberately — and -# adding one is exactly the moment to ask whether older clients can still read -# the package. -# `\[\[?` covers both a table and an array-of-tables header: `[[runtime.artifacts]]` -# is the same section as `[runtime]` for this purpose, and the first version of -# this pattern matched only the single-bracket form — so it flagged the very -# section the design deliberately reuses. -known='^\[\[?(package|build|modules|targets\.|target\.|dependencies|dev-dependencies|runtime|profile\.|features|lib|pack|workspace|indices|resources|xlings|capabilities|tools)' -bad="$(grep -E '^\[' "$pkg/mcpp.toml" | grep -Ev "$known" || true)" -[[ -z "$bad" ]] || { - echo "FAIL: the generated manifest uses sections an older mcpp cannot read:" - printf '%s\n' "$bad" - echo " Either express the fact with an existing key, or accept that packages" - echo " need a version floor — and say so in docs/12." - exit 1; } - -# ── 2. the released client actually builds against it ────────────────── -mkdir -p "$TMP/app/src" -cat > "$TMP/app/src/main.cpp" <<'EOF' -#include -import mathkit; -int main(){ std::printf("ok=%d\n", mk::answer()); return 0; } -EOF -cat > "$TMP/app/mcpp.toml" < new.log 2>&1 ) \ - || { cat "$TMP/app/new.log"; echo "the PR binary could not consume its own package"; exit 1; } -grep -q 'ok=42' "$TMP/app/new.log" || { cat "$TMP/app/new.log"; echo "wrong answer"; exit 1; } - -# The boot entry each CI job bootstraps from is an xvm SHIM, and a shim -# resolves against the home it is asked in — under the e2e suite's environment -# it answers `xlings: 'mcpp' is not installed` and prints NOTHING for -# `--version`. The first version of this guard compared that empty string -# against the PR binary's version, found them "different", and concluded it had -# found an old client — then reported a compatibility failure against a package -# that is perfectly readable. So the guard demands a version-SHAPED answer; -# anything else means "no usable old binary here", which is a note, not a -# verdict. -boot_ver="" -new_ver="$("$MCPP" --version 2>/dev/null || true)" -if [[ -n "${MCPP_BOOT:-}" && -x "${MCPP_BOOT}" ]]; then - boot_ver="$("$MCPP_BOOT" --version 2>/dev/null || true)" -fi -usable=0 -case "$boot_ver" in - mcpp\ [0-9]*) usable=1 ;; -esac - -if [[ "$usable" == 1 && "$boot_ver" != "$new_ver" ]]; then - echo "old client: $boot_ver" - rm -rf "$TMP/app/target" - ( cd "$TMP/app" && "$MCPP_BOOT" run > old.log 2>&1 ) || { - cat "$TMP/app/old.log" - echo "FAIL: the released mcpp cannot build against a package this one produced." - echo " The compatibility claim in docs/12 is then false: such packages" - echo " need a version floor, and publishing one without it bricks older" - echo " clients rather than degrading them." - exit 1; } - grep -q 'ok=42' "$TMP/app/old.log" || { - cat "$TMP/app/old.log"; echo "the old client built it but ran it wrong"; exit 1; } - echo "PASS: a released mcpp builds and runs against a package from this one" -else - if [[ -n "${MCPP_BOOT:-}" && "$usable" != 1 ]]; then - echo "NOTE: \$MCPP_BOOT=${MCPP_BOOT} does not answer --version with a version" - echo " (got: '${boot_ver}'), so it is not a usable old client here — a" - echo " shim resolves against the home it is asked in. The REAL old-client" - echo " check therefore did not run; run it with MCPP_BOOT pointing at a" - echo " released mcpp binary directly." - else - echo "NOTE: \$MCPP_BOOT is unset or identical to \$MCPP — the real old-client" - echo " check did not run here." - fi - echo "PASS: the generated manifest introduces no section an older mcpp cannot read" -fi diff --git a/tests/e2e/253_pack_library_undetermined_partition.sh b/tests/e2e/253_pack_library_undetermined_partition.sh deleted file mode 100755 index 2869412bc..000000000 --- a/tests/e2e/253_pack_library_undetermined_partition.sh +++ /dev/null @@ -1,146 +0,0 @@ -#!/usr/bin/env bash -# requires: -# (no capability: nothing here is toolchain-specific — the fixture uses a local -# module rather than `import std`, and both assertions read mcpp's own output.) -# -# 253_pack_library_undetermined_partition.sh — when mcpp CANNOT TELL whether a -# published partition is an interface partition or an implementation one, it has -# to say so. -# -# `mcpp pack` publishes the module closure of the lib root as SOURCE. Whether a -# partition's source may travel depends on one keyword: -# -# export module M:api; interface partition — publishing it is the point -# module M:impl; implementation partition — publishing it is a leak -# -# The text scanner reads that keyword. Two other paths do not: -# -# * `[scan_overrides.""]` names the modules a file provides and has -# nowhere to say whether the declaration carries `export`; -# * a P1689 scanner may omit `is-interface` (the key is optional). -# -# Both used to arrive as "interface" — the value that produces NO warning. So an -# implementation partition declared in `[scan_overrides]` was published in -# silence, which is the one failure mode the whole closure design exists to -# prevent. -# -# PINNED FROM BOTH SIDES, on purpose. Asserting only that the override case -# warns cannot distinguish "mcpp models three states" from "mcpp warns about -# every partition it publishes". So the same fixture is packed twice — once -# scanned, once overridden — and the two must produce DIFFERENT sentences: -# -# scanned → "secret.cppm is an implementation partition" (it knows) -# overridden → "cannot tell which kind" (it doesn't) -set -e -source "$(dirname "$0")/_host_path.sh" - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -mkdir -p mathkit/src -# The root interface reaches the partition, so the partition is published either -# way — this test is about WHAT MCPP SAYS while publishing it, not about whether -# it does (243 pins that). -cat > mathkit/src/mathkit.cppm <<'EOF' -export module mathkit; -import :secret; -export namespace mk { int answer(); } -EOF -cat > mathkit/src/secret.cppm <<'EOF' -module mathkit:secret; -namespace mk { int secret_bias() { return 40; } } -EOF -cat > mathkit/src/impl.cpp <<'EOF' -module mathkit; -namespace mk { -int secret_bias(); -int answer() { return secret_bias() + 2; } -} -EOF - -manifest() { # $1 = extra manifest body (the override, or nothing) - # Absolute: this is called from inside mathkit/ the second time, and a - # relative path there writes a manifest nobody reads (or nothing at all). - cat > "$TMP/mathkit/mcpp.toml" < scanned.log 2>&1 || { cat scanned.log; echo "pack failed"; exit 1; } -grep -q 'secret.cppm is an implementation partition' scanned.log || { - cat scanned.log - echo "FAIL: the scanned case lost its known-implementation-partition warning" - exit 1; } -grep -q 'cannot tell which kind' scanned.log && { - cat scanned.log - echo "FAIL: mcpp read \`module mathkit:secret;\` and still claims it cannot tell." - echo " Then the undetermined state is not a state, it is every partition," - echo " and the warning carries no information." - exit 1; } - -# ── side 2: overridden. Nobody told mcpp whether it is exported. ──────── -# -# The override declares exactly what the scanner would have found, EXCEPT the -# one thing the schema cannot express — so the graph is identical apart from the -# unknown, and any difference in output is attributable to it alone. -manifest ' -[scan_overrides."src/secret.cppm"] -provides = ["mathkit:secret"]' -rm -rf target -"$MCPP" pack mathkit > overridden.log 2>&1 || { cat overridden.log; echo "override pack failed"; exit 1; } -grep -q 'cannot tell which kind' overridden.log || { - cat overridden.log - echo "FAIL: a partition declared in [scan_overrides] was published without a word." - echo " That is the silent half of the asymmetry: too FEW published sources" - echo " fails the consumer's compile, too MANY ships private source and" - echo " nothing fails. Unknown must warn." - exit 1; } -grep -q 'secret.cppm is an implementation partition' overridden.log && { - cat overridden.log - echo "FAIL: mcpp asserted it IS an implementation partition. Nothing told it so —" - echo " the override cannot say, and stating it anyway is a guess wearing" - echo " a diagnostic's clothes." - exit 1; } - -# It is still published: the consumer cannot build the root's BMI without it. -pkg="$(find target/dist -maxdepth 1 -type d -name 'mathkit-0.1.0-*' | head -1)" -[[ -e "$pkg/interface/secret.cppm" ]] || { - echo "FAIL: the warning fired but the source was not published — the consumer" - echo " would fail to compile the interface it was shipped." - exit 1; } -PKG_HOST="$(host_path "$TMP/mathkit/$pkg")" - -# ── and the package a warned-about pack produces still works ──────────── -cd "$TMP" -mkdir -p app/src -cat > app/src/main.cpp <<'EOF' -#include -import mathkit; -int main(){ std::printf("ok=%d\n", mk::answer()); return 0; } -EOF -cat > app/mcpp.toml < run.log 2>&1 ) || { cat app/run.log; echo "consumer failed"; exit 1; } -grep -q 'ok=42' app/run.log || { cat app/run.log; echo "wrong answer"; exit 1; } - -echo "PASS: an undetermined partition is published loudly, and it says so differently" diff --git a/tests/e2e/254_pack_library_platform_coverage.sh b/tests/e2e/254_pack_library_platform_coverage.sh deleted file mode 100755 index 45b0dc1db..000000000 --- a/tests/e2e/254_pack_library_platform_coverage.sh +++ /dev/null @@ -1,138 +0,0 @@ -#!/usr/bin/env bash -# requires: -# (no capability: every assertion reads mcpp's own output about a manifest key.) -# -# 254_pack_library_platform_coverage.sh — `[package] platforms` is a support -# CLAIM, and `mcpp pack` is where it first becomes checkable against evidence: -# the legs in the package are the platforms it can actually serve. -# -# THE HARD PART IS NOT FINDING THE GAP, IT IS NOT SHOUTING ABOUT IT. -# -# Four comparisons exist and only two may be printed: -# -# packed, not declared always actionable — the manifest disclaims a -# platform the package demonstrably serves. -# declared, not packed actionable ONLY IF THIS HOST COULD HAVE BUILT IT. -# The normal release flow is one `mcpp pack` per -# platform in CI, so a Linux runner producing no -# macos leg is not an omission, it is every single -# run. A warning that fires on every run is one -# nobody reads, and then the real one is invisible -# too. -# -# So the silence is as much the feature as the warning, and both are asserted. -# -# THE PER-HOST PICKS BELOW MIRROR THE PROVIDERS' `hosts` (data/builtin.toml, -# which `host_can_serve` reads since 2026.11.12.1). That is deliberate and it -# is a tripwire: when a provider's hosts change, this test starts failing — -# which is the correct outcome, because the table it encodes will have changed -# and the expectations here have to be re-derived rather than assumed. It -# tripped once, as predicted: the `mingw-llvm` provider (2026.11.10.1) serves -# the GNU-ABI Windows rows on every host, a macOS host included, and the -# function this test mirrored then still said a macOS host builds for macOS -# alone. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -mkdir -p mathkit/src -cat > mathkit/src/mathkit.cppm <<'EOF' -export module mathkit; -export namespace mk { int answer(); } -EOF -cat > mathkit/src/impl.cpp <<'EOF' -module mathkit; -namespace mk { int answer() { return 42; } } -EOF - -manifest() { # $1 = the platforms array body, e.g. '"macos"' - cat > "$TMP/mathkit/mcpp.toml" < probe.log 2>&1 || { cat probe.log; echo "pack failed"; exit 1; } -host_triple="$(find target -mindepth 1 -maxdepth 1 -type d ! -name dist -exec basename {} \; | head -1)" -case "$host_triple" in - *-linux-*|*-linux) HOST_OS=linux ;; - *-macos*) HOST_OS=macos ;; - *-windows-*) HOST_OS=windows ;; - *) echo "FAIL: could not read the host platform out of target/$host_triple"; exit 1 ;; -esac -echo "host platform: $HOST_OS (from target/$host_triple)" - -# Per host: one platform this host CANNOT serve, and one it can but did not pack. -case "$HOST_OS" in - linux) UNSERVABLE=macos SERVABLE_UNPACKED=windows ;; - windows) UNSERVABLE=macos SERVABLE_UNPACKED=linux ;; - # macOS has no Linux-targeting payload (a Linux identity is the graph's - # there, which is not this question), and it builds PE through the - # MinGW-w64 base package as every host does. - macos) UNSERVABLE=linux SERVABLE_UNPACKED=windows ;; -esac - -# ── 1. packed, not declared ───────────────────────────────────────────── -manifest "\"$UNSERVABLE\"" -rm -rf target -"$MCPP" pack mathkit > undeclared.log 2>&1 || { cat undeclared.log; echo "pack failed"; exit 1; } -grep -q "ships a $HOST_OS binary" undeclared.log || { - cat undeclared.log - echo "FAIL: the package ships a $HOST_OS artifact while [package] platforms lists" - echo " only $UNSERVABLE, and mcpp said nothing. The manifest disclaims a" - echo " platform the package serves, which is the claim consumers resolve against." - exit 1; } - -# ── 2. declared and unservable here: SILENCE ──────────────────────────── -# -# The half that keeps the warning worth reading. Asserted on its own manifest so -# a stray match from case 1 cannot satisfy it. -manifest "\"$HOST_OS\", \"$UNSERVABLE\"" -rm -rf target -"$MCPP" pack mathkit > quiet.log 2>&1 || { cat quiet.log; echo "pack failed"; exit 1; } -grep -q "claims $UNSERVABLE" quiet.log && { - cat quiet.log - echo "FAIL: mcpp asked for a $UNSERVABLE leg on a $HOST_OS host, which cannot" - echo " build one. That warning would fire on every release run of every" - echo " cross-platform package, and a warning that always fires hides the" - echo " one that matters." - exit 1; } -grep -q "ships a $HOST_OS binary" quiet.log && { - cat quiet.log - echo "FAIL: $HOST_OS is declared AND packed, and mcpp still complained about it" - exit 1; } - -# ── 3. declared, servable here, not packed ────────────────────────────── -if [[ -n "$SERVABLE_UNPACKED" ]]; then - manifest "\"$HOST_OS\", \"$SERVABLE_UNPACKED\"" - rm -rf target - "$MCPP" pack mathkit > gap.log 2>&1 || { cat gap.log; echo "pack failed"; exit 1; } - grep -q "claims $SERVABLE_UNPACKED" gap.log || { - cat gap.log - echo "FAIL: [package] platforms claims $SERVABLE_UNPACKED, this host can build" - echo " for it, no such leg was packed, and mcpp said nothing. Consumers" - echo " on $SERVABLE_UNPACKED resolve this package and find no artifact." - exit 1; } - echo "PASS: coverage gaps are reported, and unservable platforms are not" -else - echo "NOTE: a $HOST_OS host serves exactly one target, so there is no" - echo " servable-but-unpacked platform to assert here. Case 3 did not run." - echo "PASS: an undeclared platform is reported, an unservable one stays quiet" -fi diff --git a/tests/e2e/261_pack_follows_the_module_extension.sh b/tests/e2e/261_pack_follows_the_module_extension.sh deleted file mode 100755 index 423defcf0..000000000 --- a/tests/e2e/261_pack_follows_the_module_extension.sh +++ /dev/null @@ -1,150 +0,0 @@ -#!/usr/bin/env bash -# requires: -# (no capability: nothing here is toolchain-specific.) -# -# 261_pack_follows_the_module_extension.sh — `mcpp pack` follows the project's -# module extension on its own, and the package it writes states that extension -# for its consumers. -# -# The principle: a project says `module_extensions = [".ixx"]` ONCE. Packing it -# and consuming the package must both work with nothing further declared on -# either side. Anything else makes the knob a knob plus two things to remember. -# -# TWO DEFECTS SAT BEHIND THIS, and the first was silent in both directions. -# -# 1. The lib-root convention hard-coded `.cppm`: `src/.cppm`. For an -# `.ixx` project that file does not exist, so the closure started nowhere: -# -# $ mcpp pack mathkit -# Interface (headers only) ← the module interface, gone -# Withheld (nothing) -# Packed …-x86_64-linux-gnu ← the C-SURFACE tag -# -# Both halves wrong without a word. No consumer could `import mathkit`; and -# an empty published set is precisely how the packer recognises a C surface, -# so the package ALSO stopped constraining the C++ ABI and the compatibility -# gate stopped checking compiler and stdlib. A package that claims less than -# it should is the failure this whole design exists to prevent. -# -# 2. The generated manifest listed `sources = ["interface/mathkit.ixx"]` and did -# not say what an `.ixx` is, so the consumer was refused by the classifier. -# Now the package declares it — computed from the published FILES, so it -# cannot disagree with `sources`. -set -e -source "$(dirname "$0")/_host_path.sh" - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -mkdir -p mathkit/src -cat > mathkit/src/mathkit.ixx <<'EOF' -export module mathkit; -export import :api; -EOF -cat > mathkit/src/api.ixx <<'EOF' -export module mathkit:api; -export namespace mk { int answer(); } -EOF -cat > mathkit/src/impl.cpp <<'EOF' -module mathkit; -namespace mk { int answer() { return 42; } } -EOF -cat > mathkit/mcpp.toml <<'EOF' -[package] -name = "mathkit" -version = "0.1.0" -[build] -sources = ["src/*.ixx", "src/*.cpp"] -module_extensions = [".ixx"] -[targets.mathkit] -kind = "lib" -EOF - -cd mathkit -"$MCPP" pack mathkit --pkg-format dir > pack.log 2>&1 || { cat pack.log; echo "pack failed"; exit 1; } -pkg="$TMP/mathkit/$(find target/dist -maxdepth 1 -type d -name 'mathkit-0.1.0-*' | head -1)" - -# ── the closure actually started ──────────────────────────────────────── -[[ -f "$pkg/interface/mathkit.ixx" && -f "$pkg/interface/api.ixx" ]] || { - find "$pkg" -type f - cat pack.log - echo "FAIL: the module interface was not published. 'Interface (headers only)'" - echo " in the log above means the lib root resolved to a file that does" - echo " not exist — the convention looked for .cppm." - exit 1; } -[[ ! -e "$pkg/interface/impl.cpp" ]] || { echo "FAIL: the implementation unit was published"; exit 1; } - -# ── and the package is tagged as the C++ surface it is ────────────────── -# -# Asserted separately because it fails INDEPENDENTLY: an empty published set is -# how a C surface is recognised, so losing the interface also silently downgrades -# the compatibility tag, and a package that constrains nothing is accepted by -# every toolchain. -grep -qE '^abi +=.*-c\+\+[0-9]+"' "$pkg/mcpp.toml" || { - grep -n 'abi' "$pkg/mcpp.toml" - echo "FAIL: the leg carries a C-surface tag. A package that publishes a C++" - echo " module interface constrains the C++ ABI, and this one now says" - echo " it does not — the gate will accept any compiler." - exit 1; } - -# ── the package tells consumers what an .ixx is ───────────────────────── -grep -q 'module_extensions' "$pkg/mcpp.toml" || { - cat "$pkg/mcpp.toml" - echo "FAIL: the generated manifest lists .ixx sources without declaring the" - echo " extension, so every consumer is refused by the classifier." - exit 1; } - -# ── a consumer that declares NOTHING builds and runs ──────────────────── -PKG_HOST="$(host_path "$pkg")" -cd "$TMP" -mkdir -p app/src -cat > app/src/main.cpp <<'EOF' -#include -import mathkit; -int main(){ std::printf("pack-ixx=%d\n", mk::answer()); return 0; } -EOF -cat > app/mcpp.toml < run.log 2>&1 ) || { - cat app/run.log - echo "FAIL: the consumer had to be told about .ixx. The package should carry it." - exit 1; } -grep -q 'pack-ixx=42' app/run.log || { cat app/run.log; echo "FAIL: wrong answer"; exit 1; } - -# ── a .cppm package's manifest is unchanged ───────────────────────────── -# -# The new key must appear only when the published set needs it, or every -# existing package's manifest changes shape for no reason. -cd "$TMP" -mkdir -p plain/src -cat > plain/src/plain.cppm <<'EOF' -export module plain; -export namespace pl { int v() { return 1; } } -EOF -cat > plain/mcpp.toml <<'EOF' -[package] -name = "plain" -version = "0.1.0" -[build] -sources = ["src/*.cppm"] -[targets.plain] -kind = "lib" -EOF -( cd plain && "$MCPP" pack plain --pkg-format dir > pack.log 2>&1 ) || { - cat plain/pack.log; echo "plain pack failed"; exit 1; } -plainpkg="$(find plain/target/dist -maxdepth 1 -type d -name 'plain-0.1.0-*' | head -1)" -grep -q 'module_extensions' "$plainpkg/mcpp.toml" && { - cat "$plainpkg/mcpp.toml" - echo "FAIL: a .cppm-only package gained a module_extensions key it does not need" - exit 1; } - -echo "PASS: pack follows the project's module extension, and the package carries it" diff --git a/tests/e2e/264_pack_library_is_relocatable.sh b/tests/e2e/264_pack_library_is_relocatable.sh deleted file mode 100755 index 5130020e0..000000000 --- a/tests/e2e/264_pack_library_is_relocatable.sh +++ /dev/null @@ -1,155 +0,0 @@ -#!/usr/bin/env bash -# requires: pack python3 -# 264_pack_library_is_relocatable.sh — a packed `kind = "shared"` library must -# not carry the BUILD MACHINE's loader search path (issue #460). -# -# WHAT WAS WRONG, AND WHY NOBODY SAW IT -# -# `run_library_pack` copied the built `.so` into the package and did nothing -# else, so it kept the DT_RUNPATH the link gave it: -# -# /registry/data/xpkgs/xim-x-glibc/2.44/lib64 -# : /registry/data/xpkgs/xim-x-gcc/16.1.0/lib64 -# : /registry/subos/default/lib -# -# On another machine those directories do not exist, and — this is the part -# that makes it fatal rather than untidy — a shared object that carries ANY -# DT_RUNPATH makes the loader skip the whole inherited DT_RPATH chain when -# resolving that object's own dependencies. So the consumer's carefully -# computed DT_RPATH (payload + package dir + SubOS farm) is not consulted, and -# the program dies with -# -# error while loading shared libraries: libstdc++.so.6: cannot open shared object file -# -# AND WHY `$ORIGIN` IS NOT THE FIX. Measured: a DT_RUNPATH of `$ORIGIN`, and -# a DT_RUNPATH of the empty string, both fail exactly the same way. It is the -# TAG's presence that disables inheritance, not its contents. The criterion is -# therefore "there is no tag", never "the tag is relative". -# -# AND WHY THIS TEST DELIBERATELY BREAKS THE PACKAGE HALF-WAY THROUGH. -# Asserting only that the fixed package runs cannot tell "the defect is fixed" -# from "this machine happens to satisfy the stale path" — which is precisely -# what made the pre-existing e2e (251) green throughout the bug's life: it -# consumes the package on the machine that built it, where those directories -# are right there. So the defect is put BACK with patchelf, the consumer is -# required to FAIL, and only then is it restored. A guard that cannot observe -# the defect is not a guard. -set -e -source "$(dirname "$0")/_host_path.sh" -source "$(dirname "$0")/_elf_tag.sh" - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -mkdir -p mathkit/src -cat > mathkit/src/mathkit.cppm <<'EOF' -export module mathkit; -export namespace mk { int answer(); } -EOF -cat > mathkit/src/impl.cpp <<'EOF' -module mathkit; -namespace mk { int answer() { return 42; } } -EOF -cat > mathkit/mcpp.toml <<'EOF' -[package] -name = "mathkit" -version = "0.1.0" -[build] -sources = ["src/*.cppm", "src/*.cpp"] -[targets.mathkit-shared] -kind = "shared" -soname = "libmathkit.so.1" -EOF - -cd mathkit -"$MCPP" pack mathkit-shared > pack.log 2>&1 || { cat pack.log; echo "FAIL: pack"; exit 1; } -pkg="$TMP/mathkit/$(find target/dist -maxdepth 1 -type d -name 'mathkit-0.1.0-*' | head -1)" -PKG_HOST="$(host_path "$pkg")" -libdir="$pkg/lib/x86_64-linux-gnu" -[[ -d "$libdir" ]] || libdir="$(dirname "$(find "$pkg/lib" -name 'libmathkit-shared.so' | head -1)")" -so="$libdir/libmathkit-shared.so" -[[ -f "$so" ]] || { find "$pkg" -type f; echo "FAIL: no .so in the package"; exit 1; } - -# ── 1. static: EVERY ELF in the package, and both of the .so's names ──────── -# -# The SONAME alias is checked too, and that is not belt-and-braces: it is a -# symlink here, but on a filesystem where `create_symlink` fails the packer -# falls back to a COPY — and that copy used to be made from the BUILD TREE's -# file rather than from the staged one, i.e. from the unrelocated original, -# under the exact name the loader asks for. -fail=0 -swept=0 -while IFS= read -r obj; do - head -c4 "$obj" 2>/dev/null | grep -q $'\x7fELF' || continue - read -r form tag paths <<<"$(read_tag "$obj")" - [[ "$form" == "NOT-ELF64" ]] && continue - swept=$((swept + 1)) - printf ' %-34s %-14s %s %s\n' "$(basename "$obj")" "$form" "$tag" "$paths" - if [[ "$tag" != "NONE" ]]; then - echo "FAIL: a packaged library carries $tag ($paths)" - echo " A shipped .so must carry NO loader search path: any DT_RUNPATH —" - echo " including \$ORIGIN and the empty string — stops the consumer's" - echo " DT_RPATH from being inherited for this object's dependencies." - fail=1 - fi -done < <(find "$libdir" \( -type f -o -type l \)) -[[ "$swept" -ge 1 ]] || { echo "FAIL: swept no ELF — the test proved nothing"; exit 1; } -[[ "$fail" == "0" ]] || exit 1 - -# ── 2. a consumer of the package starts ──────────────────────────────────── -cd "$TMP" -mkdir -p app/src -cat > app/src/main.cpp <<'EOF' -#include -import mathkit; -int main(){ std::printf("ok=%d\n", mk::answer()); return 0; } -EOF -cat > app/mcpp.toml < build.log 2>&1 ) || { cat app/build.log; echo "FAIL: consumer build"; exit 1; } -exe="$(find app/target -path '*/bin/app' -type f | head -1)" -[[ -x "$exe" ]] || { echo "FAIL: no consumer binary"; exit 1; } - -run_bare() { "$exe" 2>&1; } # NOT `mcpp run`: no injected environment - -out="$(run_bare)" && rc=0 || rc=$? -[[ "$rc" == "0" && "$out" == *"ok=42"* ]] || { - echo "FAIL: the consumer does not start against the packed library" - echo " rc=$rc out=$out"; exit 1; } - -# ── 3. put the defect BACK, and require the consumer to fail ─────────────── -# -# `/nonexistent-machine/...` stands in for "the publisher's store, on somebody -# else's computer". Nothing is rebuilt: only the shipped library's dynamic -# section changes, which is exactly the difference between the two machines. -cp "$so" "$so.packed" -patchelf --set-rpath '/nonexistent-machine/.mcpp/registry/data/xpkgs/xim-x-gcc/16.1.0/lib64' "$so" -out="$(run_bare)" && rc=0 || rc=$? -if [[ "$rc" == "0" ]]; then - echo "FAIL: the consumer still ran with a stale RUNPATH restored on the library." - echo " That means this test cannot observe the defect it exists for —" - echo " the consumer is finding libstdc++ some other way, so a regression" - echo " in the packer would pass unnoticed." - exit 1 -fi -[[ "$out" == *"cannot open shared object file"* ]] || { - echo "FAIL: the restored defect produced an unexpected failure: $out"; exit 1; } -echo " defect restored → consumer fails as expected (rc=$rc)" - -# ── 4. …and restore, so the pass is not an artifact of step 3 ────────────── -cp "$so.packed" "$so" -out="$(run_bare)" && rc=0 || rc=$? -[[ "$rc" == "0" && "$out" == *"ok=42"* ]] || { - echo "FAIL: restoring the packed library did not restore the behaviour" - echo " rc=$rc out=$out"; exit 1; } - -echo "PASS: a packed shared library carries no build-machine search path, and the guard can see the defect" diff --git a/tests/e2e/265_pack_strips_but_stays_usable.sh b/tests/e2e/265_pack_strips_but_stays_usable.sh deleted file mode 100755 index ccf8b2b6f..000000000 --- a/tests/e2e/265_pack_strips_but_stays_usable.sh +++ /dev/null @@ -1,189 +0,0 @@ -#!/usr/bin/env bash -# requires: pack python3 -# 265_pack_strips_but_stays_usable.sh — `mcpp pack` ships stripped artifacts, -# and stripping must not make them unusable. -# -# THE ONE THAT BITES: A STATIC ARCHIVE CANNOT BE `--strip-all`ed. -# -# `strip` with no shape argument defaults to strip-all, and on a `.a` that -# removes the ARCHIVE SYMBOL INDEX. The package then fails at the CONSUMER's -# link with -# -# ld: libmathkit.a: error adding symbols: archive has no index; run ranlib to add one -# -# — a message that names neither `strip` nor the publisher, arriving on a -# machine the publisher does not have. Measured: `--strip-debug` keeps the -# index (2988 → 1244 bytes) and the consumer links and runs. So the packer -# uses dh_strip's division, and this test consumes BOTH shapes rather than -# inspecting them: "the archive still has an index" is a proxy, "a program -# linked against it prints the right number" is the thing. -# -# The three configuration channels are pinned from BOTH sides — a test that -# only checks the default cannot tell "the switch works" from "there is no -# switch". -set -e -source "$(dirname "$0")/_host_path.sh" - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -mkdir -p mathkit/src -cat > mathkit/src/mathkit.cppm <<'EOF' -export module mathkit; -export namespace mk { int answer(); } -EOF -cat > mathkit/src/impl.cpp <<'EOF' -module mathkit; -namespace mk { int answer() { return 42; } } -EOF -cat > mathkit/mcpp.toml <<'EOF' -[package] -name = "mathkit" -version = "0.1.0" -[build] -sources = ["src/*.cppm", "src/*.cpp"] -[targets.mathkit] -kind = "lib" -[targets.mathkit-shared] -kind = "shared" -soname = "libmathkit.so.1" -EOF - -# `file` is not required by the harness; ask the ELF itself instead. -has_symtab() { -python3 - "$1" <<'PY' -import struct, sys -d = open(sys.argv[1], 'rb').read() -if d[:4] != b'\x7fELF' or d[4] != 2: - print("no"); raise SystemExit -shoff, = struct.unpack_from(' — build a program against it and run - local pkg="$1" want="$2" tag="$3" - # Through a named *_HOST variable, not interpolated inline: the manifest - # below is FILE CONTENT, and on Git Bash a shell-spelled /tmp/... path is - # read by a native mcpp.exe as "root of the current drive". 00_fixture_path - # _hygiene enforces the naming so the conversion is visible at the use site. - local PKG_HOST - PKG_HOST="$(host_path "$pkg")" - rm -rf "$TMP/consumer" - mkdir -p "$TMP/consumer/src" - cat > "$TMP/consumer/src/main.cpp" <<'EOF' -#include -import mathkit; -int main(){ std::printf("ok=%d\n", mk::answer()); return 0; } -EOF - cat > "$TMP/consumer/mcpp.toml" < run.log 2>&1 ) || { - cat "$TMP/consumer/run.log" - echo "FAIL[$tag]: a consumer cannot use the stripped package"; exit 1; } - grep -q "$want" "$TMP/consumer/run.log" || { - cat "$TMP/consumer/run.log"; echo "FAIL[$tag]: wrong answer"; exit 1; } -} - -cd mathkit - -# ── 1. static: stripped, and STILL LINKABLE ──────────────────────────────── -rm -rf target/dist -"$MCPP" pack mathkit > pack-a.log 2>&1 || { cat pack-a.log; echo "FAIL: static pack"; exit 1; } -grep -q "Stripped" pack-a.log || { cat pack-a.log; echo "FAIL: the static leg was not stripped"; exit 1; } -pkg_a="$TMP/mathkit/$(find target/dist -maxdepth 1 -type d -name 'mathkit-0.1.0-*' | head -1)" -consume "$pkg_a" "ok=42" "static" -echo " static archive: stripped and still linkable" - -# ── 2. shared: stripped, and STILL LOADABLE ──────────────────────────────── -rm -rf target/dist -"$MCPP" pack mathkit-shared > pack-b.log 2>&1 || { cat pack-b.log; echo "FAIL: shared pack"; exit 1; } -grep -q "Stripped" pack-b.log || { cat pack-b.log; echo "FAIL: the shared leg was not stripped"; exit 1; } -pkg_b="$TMP/mathkit/$(find target/dist -maxdepth 1 -type d -name 'mathkit-0.1.0-*' | head -1)" -so="$(find "$pkg_b/lib" -name 'libmathkit-shared.so' -type f | head -1)" -[[ -n "$so" ]] || { echo "FAIL: no .so"; exit 1; } -# `--strip-unneeded` must leave `.dynsym` — it IS the export list. If this -# regressed to `--strip-all` the link below would still work (ld reads -# `.dynsym`), so the run is what proves it. -[[ "$(has_symtab "$so")" == "no" ]] || { - echo "FAIL: the shared library still carries a symbol/debug table after stripping"; exit 1; } -consume "$pkg_b" "ok=42" "shared" -echo " shared library: stripped and still loadable" - -# ── 3. the OTHER side of every switch ────────────────────────────────────── -rm -rf target/dist -"$MCPP" pack mathkit-shared --no-strip > pack-c.log 2>&1 || { cat pack-c.log; echo "FAIL: --no-strip pack"; exit 1; } -grep -q "Stripped" pack-c.log && { cat pack-c.log; echo "FAIL: --no-strip stripped anyway"; exit 1; } -pkg_c="$TMP/mathkit/$(find target/dist -maxdepth 1 -type d -name 'mathkit-0.1.0-*' | head -1)" -so_c="$(find "$pkg_c/lib" -name 'libmathkit-shared.so' -type f | head -1)" -[[ "$(has_symtab "$so_c")" == "yes" ]] || { - echo "FAIL: --no-strip produced an artifact with no symbols — the flag did nothing" - exit 1; } -echo " --no-strip: symbols kept" - -rm -rf target/dist -cp mcpp.toml mcpp.toml.bak -printf '\n[pack]\nstrip = false\n' >> mcpp.toml -"$MCPP" pack mathkit-shared > pack-d.log 2>&1 || { cat pack-d.log; echo "FAIL: [pack] strip pack"; exit 1; } -grep -q "Stripped" pack-d.log && { cat pack-d.log; echo "FAIL: [pack] strip = false was ignored"; exit 1; } -mv mcpp.toml.bak mcpp.toml -echo " [pack] strip = false: honoured" - -# ── 4. --debug-symbols keeps the symbols, beside the artifact ────────────── -rm -rf target/dist -"$MCPP" pack mathkit-shared --debug-symbols dbg > pack-e.log 2>&1 || { - cat pack-e.log; echo "FAIL: --debug-symbols pack"; exit 1; } -# PER TRIPLE, mirroring `lib//`. A fat package's legs share an artifact -# NAME (`libmathkit-shared.so` for both a gnu and a musl leg is the normal case), -# so a flat debug directory would have the second leg overwrite the first — and -# the first artifact's `.gnu_debuglink` would then resolve to the other target's -# symbols, silently. Asserting the layout is what keeps that from regressing. -dbgfile="$(find dbg -name 'libmathkit-shared.so.debug' | head -1)" -[[ -n "$dbgfile" ]] || { ls -R dbg 2>&1; echo "FAIL: no separated debug file"; exit 1; } -[[ "$dbgfile" == dbg/*/libmathkit-shared.so.debug ]] || { - echo "FAIL: the debug file is not under a per-triple directory: $dbgfile" - echo " A fat package's legs share an artifact name; a flat layout loses one." - exit 1; } -[[ "$(has_symtab "$dbgfile")" == "yes" ]] || { - echo "FAIL: the separated debug file carries no debug information"; exit 1; } -pkg_e="$TMP/mathkit/$(find target/dist -maxdepth 1 -type d -name 'mathkit-0.1.0-*' | head -1)" -so_e="$(find "$pkg_e/lib" -name 'libmathkit-shared.so' -type f | head -1)" -[[ "$(has_symtab "$so_e")" == "no" ]] || { - echo "FAIL: --debug-symbols left the debug information in the shipped artifact too" - exit 1; } -# The link back. Without it a debugger has the file and no way to find it. -python3 - "$so_e" <<'PY' || { echo "FAIL: no .gnu_debuglink in the stripped artifact"; exit 1; } -import struct, sys -d = open(sys.argv[1], 'rb').read() -shoff, = struct.unpack_from(' /dev/null -cd myapp -# Keep Mode C independent from whatever global default toolchain earlier -# e2e tests wrote into ~/.mcpp/config.toml. -cat >> mcpp.toml <<'EOF' - -[toolchain] -linux = "gcc@16.1.0" -EOF - -# ─── Mode C (default = bundle-project) ───────────────────────────────── -"$MCPP" pack > "$TMP/pack-c.log" 2>&1 || { - cat "$TMP/pack-c.log"; echo "Mode C pack failed"; exit 1; } - -tarball="target/dist/myapp-0.1.0-x86_64-linux-gnu.tar.gz" -[[ -f "$tarball" ]] || { echo "Mode C tarball not produced at $tarball"; exit 1; } - -# Extract + verify binary + RUNPATH/INTERP rewrite + runs from new location. -mkdir -p "$TMP/c" -tar -xzf "$tarball" -C "$TMP/c" -[[ -x "$TMP/c/myapp-0.1.0-x86_64-linux-gnu/bin/myapp" ]] || { echo "missing bin/myapp"; exit 1; } - -# Mode C: PT_INTERP must be repointed to the LSB-standard path. Use -# `file` rather than `readelf` because some sandboxes route readelf -# through an xvm shim that needs a per-tool version pin we can't rely -# on in CI. -file_out=$(file "$TMP/c/myapp-0.1.0-x86_64-linux-gnu/bin/myapp") -echo "$file_out" | grep -q 'interpreter /lib64/ld-linux-x86-64.so.2' || { - echo "Mode C: PT_INTERP not repointed to /lib64/ld-linux-x86-64.so.2" - echo "$file_out" - exit 1 -} - -# Mode C with no third-party deps: lib/ should be empty (or absent). -if [[ -d "$TMP/c/myapp-0.1.0-x86_64-linux-gnu/lib" ]]; then - extra=$(ls "$TMP/c/myapp-0.1.0-x86_64-linux-gnu/lib" 2>/dev/null | wc -l) - [[ "$extra" -eq 0 ]] || { - echo "Mode C: project has no third-party deps but lib/ has $extra entries:" - ls "$TMP/c/myapp-0.1.0-x86_64-linux-gnu/lib"; exit 1; } -fi - -# Run from new location. Should work because PT_INTERP is system-standard. -"$TMP/c/myapp-0.1.0-x86_64-linux-gnu/bin/myapp" > "$TMP/c-out.log" 2>&1 || { - cat "$TMP/c-out.log"; echo "Mode C extracted binary failed to run"; exit 1; } -grep -q 'Hello' "$TMP/c-out.log" || { - cat "$TMP/c-out.log"; echo "Mode C runtime output missing"; exit 1; } - -# Mode C should also drop a top-level entry-point script so users -# can do `./myapp` from the bundle root without typing bin/. -[[ -x "$TMP/c/myapp-0.1.0-x86_64-linux-gnu/myapp" ]] || { - ls "$TMP/c/myapp-0.1.0-x86_64-linux-gnu"; echo "Mode C: missing top-level entry"; exit 1; } -"$TMP/c/myapp-0.1.0-x86_64-linux-gnu/myapp" > "$TMP/c-name.log" 2>&1 || { - cat "$TMP/c-name.log"; echo "Mode C: top-level entry failed to run"; exit 1; } -grep -q 'Hello' "$TMP/c-name.log" || { - cat "$TMP/c-name.log"; echo "Mode C: top-level entry output missing"; exit 1; } - -# ─── Mode A (static, musl) ───────────────────────────────────────────── -musl_gcc="$MUSL_GCC_ROOT/bin/x86_64-linux-musl-g++" -if [[ -x "$musl_gcc" ]]; then - "$MCPP" pack --mode static > "$TMP/pack-a.log" 2>&1 || { - cat "$TMP/pack-a.log"; echo "Mode A pack failed"; exit 1; } - - tarball_a="target/dist/myapp-0.1.0-x86_64-linux-musl-static.tar.gz" - [[ -f "$tarball_a" ]] || { echo "Mode A tarball missing at $tarball_a"; exit 1; } - - mkdir -p "$TMP/a" - tar -xzf "$tarball_a" -C "$TMP/a" - [[ -x "$TMP/a/myapp-0.1.0-x86_64-linux-musl-static/bin/myapp" ]] \ - || { echo "Mode A: missing bin/myapp"; exit 1; } - file "$TMP/a/myapp-0.1.0-x86_64-linux-musl-static/bin/myapp" | grep -q 'statically linked' || { - file "$TMP/a/myapp-0.1.0-x86_64-linux-musl-static/bin/myapp" - echo "Mode A: not statically linked" - exit 1 - } - "$TMP/a/myapp-0.1.0-x86_64-linux-musl-static/bin/myapp" > "$TMP/a-out.log" 2>&1 || { - cat "$TMP/a-out.log"; echo "Mode A extracted binary failed to run"; exit 1; } - grep -q 'Hello' "$TMP/a-out.log" || { echo "Mode A runtime output missing"; exit 1; } - - # Mode A also gets a top-level entry-point wrapper. - [[ -x "$TMP/a/myapp-0.1.0-x86_64-linux-musl-static/myapp" ]] || { - echo "Mode A: missing top-level entry"; exit 1; } - "$TMP/a/myapp-0.1.0-x86_64-linux-musl-static/myapp" > "$TMP/a-name.log" 2>&1 || { - echo "Mode A: top-level entry failed to run"; exit 1; } - grep -q 'Hello' "$TMP/a-name.log" || { - echo "Mode A: top-level entry output missing"; exit 1; } -else - echo "(skip Mode A: musl-gcc not installed)" -fi - -# ─── Mode B (bundle-all + wrapper) ───────────────────────────────────── -"$MCPP" pack --mode bundle-all > "$TMP/pack-b.log" 2>&1 || { - cat "$TMP/pack-b.log"; echo "Mode B pack failed"; exit 1; } - -tarball_b="target/dist/myapp-0.1.0-x86_64-linux-gnu-bundle-all.tar.gz" -[[ -f "$tarball_b" ]] || { echo "Mode B tarball missing at $tarball_b"; exit 1; } - -mkdir -p "$TMP/b" -tar -xzf "$tarball_b" -C "$TMP/b" -[[ -x "$TMP/b/myapp-0.1.0-x86_64-linux-gnu-bundle-all/bin/myapp" ]] || { echo "Mode B: missing bin/myapp"; exit 1; } -[[ -x "$TMP/b/myapp-0.1.0-x86_64-linux-gnu-bundle-all/run.sh" ]] || { echo "Mode B: missing run.sh wrapper"; exit 1; } -[[ -d "$TMP/b/myapp-0.1.0-x86_64-linux-gnu-bundle-all/lib" ]] || { echo "Mode B: missing lib/"; exit 1; } - -# Mode B must include the dynamic linker + libc — those are the libs the -# wrapper relies on to detach from the host's glibc. -ls "$TMP/b/myapp-0.1.0-x86_64-linux-gnu-bundle-all/lib" | grep -q 'ld-linux' \ - || { echo "Mode B: ld-linux not bundled"; exit 1; } -ls "$TMP/b/myapp-0.1.0-x86_64-linux-gnu-bundle-all/lib" | grep -q '^libc\.so' \ - || { echo "Mode B: libc.so not bundled"; exit 1; } - -# Wrapper script must run via both run.sh AND the program-name entry. -[[ -x "$TMP/b/myapp-0.1.0-x86_64-linux-gnu-bundle-all/myapp" ]] || { - ls "$TMP/b" - echo "Mode B: missing top-level entry alongside run.sh" - exit 1 -} -"$TMP/b/myapp-0.1.0-x86_64-linux-gnu-bundle-all/run.sh" > "$TMP/b-out.log" 2>&1 || { - cat "$TMP/b-out.log"; echo "Mode B run.sh failed"; exit 1; } -grep -q 'Hello' "$TMP/b-out.log" || { - cat "$TMP/b-out.log"; echo "Mode B runtime output missing"; exit 1; } -"$TMP/b/myapp-0.1.0-x86_64-linux-gnu-bundle-all/myapp" > "$TMP/b-name.log" 2>&1 || { - cat "$TMP/b-name.log"; echo "Mode B entry failed"; exit 1; } -grep -q 'Hello' "$TMP/b-name.log" || { - cat "$TMP/b-name.log"; echo "Mode B -entry output missing"; exit 1; } - -# Launching through the loader makes the kernel set /proc/self/exe to the -# LOADER, so every "find my resources next to the executable" path silently -# resolves against lib/ instead. The wrapper must therefore hand the program -# an answer that survives — and it must hand the same one from both entry -# points, since they are documented as interchangeable. -for entry in run.sh myapp; do - grep -q 'MCPP_BUNDLE_DIR' \ - "$TMP/b/myapp-0.1.0-x86_64-linux-gnu-bundle-all/$entry" || { - echo "Mode B: $entry does not export MCPP_BUNDLE_DIR — an application" - echo " cannot locate its own resources under the bundled loader" - exit 1; } -done - -# The value must be the bundle ROOT (`$here`), not the loader's directory and -# not a path baked in at pack time — the bundle is relocatable, so anything -# absolute would be wrong the moment it is extracted somewhere else. -grep -q 'MCPP_BUNDLE_DIR="\$here"' \ - "$TMP/b/myapp-0.1.0-x86_64-linux-gnu-bundle-all/run.sh" || { - echo "Mode B: MCPP_BUNDLE_DIR is not derived from the bundle root (\$here)" - grep MCPP_BUNDLE_DIR "$TMP/b/myapp-0.1.0-x86_64-linux-gnu-bundle-all/run.sh" - exit 1; } - -# ─── Mode `system` (Mode::None — depend on OS for all .so) ───────────── -"$MCPP" pack --mode system > "$TMP/pack-sys.log" 2>&1 || { - cat "$TMP/pack-sys.log"; echo "system pack failed"; exit 1; } - -tarball_sys="target/dist/myapp-0.1.0-x86_64-linux-gnu-system.tar.gz" -[[ -f "$tarball_sys" ]] || { echo "system tarball missing at $tarball_sys"; exit 1; } - -mkdir -p "$TMP/sys" -tar -xzf "$tarball_sys" -C "$TMP/sys" -root_sys="$TMP/sys/myapp-0.1.0-x86_64-linux-gnu-system" -[[ -x "$root_sys/bin/myapp" ]] || { echo "system: missing bin/myapp"; exit 1; } - -# system mode bundles NOTHING — lib/ must be empty or absent even though the -# binary still links libc/libstdc++ (those come from the host). -if [[ -d "$root_sys/lib" ]]; then - n=$(ls "$root_sys/lib" 2>/dev/null | wc -l) - [[ "$n" -eq 0 ]] || { echo "system: lib/ should be empty, has $n"; ls "$root_sys/lib"; exit 1; } -fi - -# PT_INTERP repointed to the LSB-standard loader (host glibc). -file "$root_sys/bin/myapp" | grep -q 'interpreter /lib64/ld-linux-x86-64.so.2' || { - file "$root_sys/bin/myapp"; echo "system: PT_INTERP not repointed to LSB loader"; exit 1; } - -# Runs on this (glibc) host. -"$root_sys/bin/myapp" > "$TMP/sys-out.log" 2>&1 || { - cat "$TMP/sys-out.log"; echo "system: extracted binary failed to run"; exit 1; } -grep -q 'Hello' "$TMP/sys-out.log" || { echo "system: runtime output missing"; exit 1; } - -# Old alias must still parse identically (back-compat). -"$MCPP" pack --mode bundle-project > "$TMP/pack-alias.log" 2>&1 || { - cat "$TMP/pack-alias.log"; echo "alias bundle-project failed"; exit 1; } - -echo "OK" diff --git a/tests/e2e/638_pack_format_dispatch.sh b/tests/e2e/638_pack_format_dispatch.sh deleted file mode 100755 index 17ebe3e1b..000000000 --- a/tests/e2e/638_pack_format_dispatch.sh +++ /dev/null @@ -1,369 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 638_pack_format_dispatch.sh — `mcpp pack --pkg-format ` dispatches to a -# package, and no distribution format lives in the engine. -# -# `tar` and `dir` answer the same question `msi` and `appimage` answer -- what -# shape does the output take -- so they are values of ONE flag, and everything -# past the two the engine owns comes from the resolved graph. What the engine -# adds is the mechanism: a staged tree an artifact action can consume, the rest -# of `[package]` in the build program, and the dispatch itself. -# -# The four properties this holds, each with the wrong answer it excludes: -# -# 1. DECLARE UNCONDITIONALLY, SUBMIT CONDITIONALLY. A build that asks for no -# format still declares one, so `--format bogus` can name what IS -# available. A member that declared only when asked works for its author -# and makes the set unknowable for everyone else. -# 2. THE STAGED TREE IS REAL WHEN THE ACTION RUNS. Asserted from INSIDE the -# action, by listing the tree into the output -- not by the action's exit -# code, because a command that writes nothing and exits 0 is the measured -# failure this whole mechanism exists to prevent. -# 3. A PLAIN BUILD HAS NO DISTRIBUTION EDGE, before or after a pack. -# 4. THE PLACEHOLDER REFUSES OUTSIDE A PACKAGING PASS, rather than expanding -# to an empty string that the tool would accept. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -# ── The provider: one build program, both halves of the contract ─────────── -mkdir -p app/src -cd app - -cat > mcpp.toml <<'EOF' -[package] -name = "app" -version = "2.5.0" -description = "a program that ships" -license = "Apache-2.0" -authors = ["Ada "] -repo = "https://example.org/app" - -[targets.app] -kind = "bin" -main = "src/main.cpp" -EOF - -cat > src/main.cpp <<'EOF' -#include -int main() { std::puts("app"); return 0; } -EOF - -# The dist step. A separate script because an action's command is an argv with -# no shell assumed, which is the same reason 188_build_actions.sh writes one. -# -# It writes the package VERSION and the staged tree's top level into its -# output, so the assertions below read what the action actually saw rather than -# whether it exited 0. -cat > dist.sh <<'EOF' -#!/usr/bin/env bash -set -e -version="$1"; stage="$2"; out="$3"; minplat="$4" -{ - echo "version=$version" - # #622 A11: MCPP_TARGET_MIN_PLATFORM_VERSION, unset for every other build - # program below (they pass 3 args), so this line reads "min_platform_version=" - # for them too -- harmless, since the assertion below matches it by exact - # line rather than by file content as a whole. - echo "min_platform_version=$minplat" - echo "staged:" - ls -1 "$stage" | sort -} > "$out" -EOF -chmod +x dist.sh - -cat > build.mcpp <<'EOF' -import mcpp; -#include -#include -#include -int main() { - // Half one: unconditional. This is what makes the set knowable on a build - // that asked for nothing. - mcpp::provides_pack_format("zap"); - - // Half two: conditional. A plain build must have no such edge. - if (std::string_view(mcpp::pack_format()) != "zap") return 0; - - const std::string root = mcpp::manifest_dir(); - const std::string out = std::string(mcpp::out_dir()) + "/app.zap"; - mcpp::action a; - a.id = "zap"; - a.role = "artifact"; - a.description = "zap"; - a.arg((root + "/dist.sh").c_str()) - // The VERSION comes from `[package]`, not from an option the project - // restates: a second copy drifts with nothing able to detect it. - .arg(mcpp::package_version()) - .arg("${mcpp.stage_dir}") - .arg(out.c_str()) - // #622 A11: the platform floor for this triple, empty on Linux. Read - // here rather than restated, exactly as the version above is. - .arg(mcpp::min_platform_version()) - .input("${mcpp.target_file:app}") - .output(out.c_str()) - .submit(); - return 0; -} -EOF - -MCPP="${MCPP:-mcpp}" - -graph_dist() { sed -n '2p' "$1" | sed 's/.*;dist=//;s/;.*//'; } -find_graph() { find target -name build.ninja -not -path '*/.build-mcpp/*' | head -1; } - -# ── 1. a plain build declares, and submits nothing ───────────────────────── -"$MCPP" build --release > b1.log 2>&1 || { cat b1.log; echo "FAIL: plain build failed"; exit 1; } -G=$(find_graph) -[ -n "$G" ] || { echo "FAIL: no build.ninja"; exit 1; } -[ "$(graph_dist "$G")" = "none" ] \ - || { sed -n '2p' "$G"; echo "FAIL: a plain build's graph is not dist=none"; exit 1; } -[ -z "$(find target -name 'app.zap' 2>/dev/null)" ] \ - || { echo "FAIL: a plain build produced a distributable"; exit 1; } - -# ── 2. an unknown format is refused, BEFORE the build, naming what exists ── -set +e -"$MCPP" pack --pkg-format bogus > b2.log 2>&1 -rc=$? -set -e -[ "$rc" -ne 0 ] || { cat b2.log; echo "FAIL: --format bogus was accepted"; exit 1; } -grep -q "unknown --pkg-format 'bogus'" b2.log \ - || { cat b2.log; echo "FAIL: the refusal does not name the value"; exit 1; } -# THE LIST IS THE POINT. A refusal that named a fixed list would be the -# coupling this mechanism removes; this one names the graph's own answer. -grep -q "available in this build: tar, zip, dir, single-file, polyglot, zap" b2.log \ - || { cat b2.log; echo "FAIL: the refusal does not name what is available"; exit 1; } -# Nothing was compiled to find that out. -grep -q "Compiling app" b2.log \ - && { cat b2.log; echo "FAIL: the refusal arrived after a compile"; exit 1; } -# AND NOTHING REACHED STDOUT. This is the path a client hits when it probes an -# mcpp for a capability, so the machine-output contract -# (202_machine_output_contract.sh) requires an empty stdout, a non-empty stderr -# and exit 2. It is asserted here as well because the tension is local to this -# feature: the valid set is a property of the resolved graph, so the refusal -# cannot be decided until prepare has run -- and prepare narrates what it -# resolves. Deciding it later is what broke the contract once. -set +e -so=$("$MCPP" pack --pkg-format bogus 2>/dev/null); rc=$? -se=$("$MCPP" pack --pkg-format bogus 2>&1 >/dev/null) -set -e -[ -z "$so" ] || { echo "FAIL: the refusal wrote to stdout: $(echo "$so" | head -1)"; exit 1; } -[ -n "$se" ] || { echo "FAIL: the refusal said nothing on stderr"; exit 1; } -[ "$rc" = 2 ] || { echo "FAIL: the refusal exited $rc, expected 2"; exit 1; } - -# ── 3. the dispatched format produces a file, and it saw the staged tree ─── -"$MCPP" pack --pkg-format zap > b3.log 2>&1 || { cat b3.log; echo "FAIL: pack --pkg-format zap failed"; exit 1; } -Z=$(find target -name 'app.zap' | head -1) -[ -n "$Z" ] || { cat b3.log; echo "FAIL: --format zap produced nothing"; exit 1; } -# The engine handed the build program the rest of `[package]`. -grep -qx "version=2.5.0" "$Z" \ - || { cat "$Z"; echo "FAIL: the action was not told the package version"; exit 1; } -# #622 A11: MCPP_TARGET_MIN_PLATFORM_VERSION is empty on a Linux host/target, -# read through mcpp::min_platform_version() rather than restated. -grep -qx "min_platform_version=" "$Z" \ - || { cat "$Z"; echo "FAIL: MCPP_TARGET_MIN_PLATFORM_VERSION was not empty on Linux"; exit 1; } -# The action ran with a staged tree that already held the program. This is the -# assertion the ordering exists for: an artifact edge is scheduled by ninja and -# the tree is staged by mcpp after the link, so a single-pass design would run -# this against a directory that does not exist. -grep -qx "bin" "$Z" \ - || { cat "$Z"; echo "FAIL: the staged tree had no bin/ when the action ran"; exit 1; } -grep -q "Packed" b3.log \ - || { cat b3.log; echo "FAIL: the produced file was not reported"; exit 1; } -# The staged tree is described by a SIBLING of itself, never a member: a file -# inside it would ship inside every format that packages the directory. -[ -n "$(find target/dist -maxdepth 1 -name '*.stage-manifest' 2>/dev/null)" ] \ - || { echo "FAIL: no stage manifest beside the staged tree"; exit 1; } -[ -z "$(find target/dist -mindepth 2 -name '*.stage-manifest' 2>/dev/null)" ] \ - || { echo "FAIL: the stage manifest landed inside the staged tree"; exit 1; } - -# ── 4. a plain build AFTER the pack still has no distribution edge ───────── -rm -f "$Z" -"$MCPP" build --release > b4.log 2>&1 || { cat b4.log; echo "FAIL: build after pack failed"; exit 1; } -[ "$(graph_dist "$(find_graph)")" = "none" ] \ - || { echo "FAIL: the graph still says dist= after a plain build"; exit 1; } -[ ! -f "$Z" ] \ - || { echo "FAIL: a plain build regenerated the distributable"; exit 1; } - -# ── 5. the placeholder outside a packaging pass is refused ───────────────── -# The same action, ungated. An empty expansion would give the tool the build -# directory root, which exists -- so the mistake would produce a plausible -# artifact instead of a diagnostic. -cd "$TMP" -cp -r app ungated -cd ungated -cat > build.mcpp <<'EOF' -import mcpp; -#include -int main() { - const std::string root = mcpp::manifest_dir(); - const std::string out = std::string(mcpp::out_dir()) + "/app.zap"; - mcpp::action a; - a.id = "zap"; a.role = "artifact"; - a.arg((root + "/dist.sh").c_str()).arg("x").arg("${mcpp.stage_dir}").arg(out.c_str()) - .input("${mcpp.target_file:app}").output(out.c_str()).submit(); - return 0; -} -EOF -set +e -"$MCPP" build --release > b5.log 2>&1 -rc=$? -set -e -[ "$rc" -ne 0 ] || { cat b5.log; echo "FAIL: an ungated stage_dir built"; exit 1; } -grep -q "this build is not packaging" b5.log \ - || { cat b5.log; echo "FAIL: the refusal does not say why"; exit 1; } -grep -q "provides_pack_format" b5.log \ - || { cat b5.log; echo "FAIL: the refusal does not say what to do instead"; exit 1; } - -# ── 6. a role other than artifact cannot reach the staged tree ───────────── -# GATED, so this is a role refusal and not the one above. An ungated action is -# already refused in the FIRST pass of `mcpp pack`, before there is a staged -# tree, which is why the two cases need different fixtures rather than a -# one-word edit: only a gated action gets far enough for its role to matter. -cd "$TMP" -cp -r app wrongrole -cd wrongrole -cat > build.mcpp <<'EOF' -import mcpp; -#include -#include -int main() { - mcpp::provides_pack_format("zap"); - if (std::string_view(mcpp::pack_format()) != "zap") return 0; - const std::string root = mcpp::manifest_dir(); - const std::string out = std::string(mcpp::out_dir()) + "/app.zap"; - mcpp::action a; - a.id = "zap"; a.role = "source"; - a.arg((root + "/dist.sh").c_str()).arg("x").arg("${mcpp.stage_dir}").arg(out.c_str()) - .output(out.c_str()).submit(); - return 0; -} -EOF -set +e -"$MCPP" pack --pkg-format zap > b6.log 2>&1 -rc=$? -set -e -[ "$rc" -ne 0 ] || { cat b6.log; echo "FAIL: a source action reached the staged tree"; exit 1; } -grep -q 'other than "artifact"' b6.log \ - || { cat b6.log; echo "FAIL: the role refusal does not name the role"; exit 1; } - -# ── 7. declared and never submitted is a refusal, not a success ──────────── -# THE HALF A MEMBER AUTHOR IS MOST LIKELY TO GET WRONG. A gate that never -# opens leaves a pass that succeeds and produces no package, which reads as -# "packaging is not implemented yet" rather than as a defect in the member. -cd "$TMP" -cp -r app silent -cd silent -cat > build.mcpp <<'EOF' -import mcpp; -int main() { mcpp::provides_pack_format("zap"); return 0; } -EOF -set +e -"$MCPP" pack --pkg-format zap > b7.log 2>&1 -rc=$? -set -e -[ "$rc" -ne 0 ] || { cat b7.log; echo "FAIL: a format nothing claimed reported success"; exit 1; } -grep -q "no action claimed --pkg-format 'zap'" b7.log \ - || { cat b7.log; echo "FAIL: the refusal does not name the unclaimed format"; exit 1; } - -# ── 8. an artifact action that predates the request is not the package ───── -# THE CRITERION IS "WHAT THE REQUEST INTRODUCED", and this is what distinguishes -# it from "any artifact action". A codesign stamp or a size budget is also an -# artifact action and is present whether or not a format was asked for; naming -# one as the distributable would be a wrong answer that looks like a right one. -# -# It is also what an earlier revision got wrong from the other side: the check -# collected only actions naming ${mcpp.stage_dir}, which refused a member that -# packages ONE NAMED PROGRAM and never reads the tree -- the shape the design -# record recommends, after a bind path that resolved to nothing produced a -# valid, empty, 52 KB installer. So this fixture submits both shapes: an -# ungated stamp that must be ignored, and a gated action that names no staged -# tree at all and must still be reported. -cd "$TMP" -cp -r app twoshapes -cd twoshapes -cat > build.mcpp <<'EOF' -import mcpp; -#include -#include -int main() { - mcpp::provides_pack_format("zap"); - const std::string root = mcpp::manifest_dir(); - - // Ungated: present in both passes, so it is not the distributable. - const std::string stamp = std::string(mcpp::out_dir()) + "/size.stamp"; - mcpp::action s; - s.id = "size-budget"; s.role = "artifact"; - s.arg((root + "/dist.sh").c_str()).arg("stamp").arg(root.c_str()).arg(stamp.c_str()) - .input("${mcpp.target_file:app}").output(stamp.c_str()); - s.submit(); - - if (std::string_view(mcpp::pack_format()) != "zap") return 0; - // Gated, and it names NO staged tree: the program arrives through - // ${mcpp.target_file:...} exactly as an MSI's one File row does. - const std::string out = std::string(mcpp::out_dir()) + "/app.zap"; - mcpp::action a; - a.id = "zap"; a.role = "artifact"; - a.arg((root + "/dist.sh").c_str()).arg("named").arg(root.c_str()).arg(out.c_str()) - .input("${mcpp.target_file:app}").output(out.c_str()); - a.submit(); - return 0; -} -EOF -"$MCPP" pack --pkg-format zap > b8.log 2>&1 || { cat b8.log; echo "FAIL: a member that reads no staged tree was refused"; exit 1; } -grep -q "app.zap" b8.log \ - || { cat b8.log; echo "FAIL: the gated action was not reported as the package"; exit 1; } -grep -q "size.stamp" b8.log \ - && { cat b8.log; echo "FAIL: an action predating the request was reported as the package"; exit 1; } -# Both files exist -- the stamp was built, it was simply not the answer. -[ -n "$(find target -name 'size.stamp' 2>/dev/null)" ] \ - || { echo "FAIL: the ungated artifact action did not run at all"; exit 1; } - -# ── 9. a format that names no staged tree survives a staging refusal ────── -# THE CASE macOS FOUND, HELD ON EVERY PLATFORM. -# -# `mcpp pack` refuses to bundle a Mach-O PROGRAM: the built-in closure walk is -# `LD_TRACE_LOADED_OBJECTS`, glibc's mechanism, and dyld ignores it and runs the -# program instead. That refusal is right about the built-in archive and says -# nothing about whether a `.app` bundler can work -- one that names a single -# program needs no closure walk at all. Before this, the refusal happened before -# the dispatch, so EVERY dispatched format was unreachable on that target. -# -# Staging is now a service to the provider rather than a precondition. This leg -# cannot reproduce the Mach-O refusal on Linux, so it holds the property the fix -# rests on instead: a provider that reads no staged tree is reported, and the -# reason travels far enough to reach the placeholder's refusal. -cd "$TMP" -cp -r app nostage -cd nostage -cat > build.mcpp <<'EOF' -import mcpp; -#include -#include -int main() { - mcpp::provides_pack_format("zap"); - if (std::string_view(mcpp::pack_format()) != "zap") return 0; - // Reads NOTHING from the staged tree: the program arrives through - // ${mcpp.target_file:...}, which is what an `.msi` of one program does. - const std::string root = mcpp::manifest_dir(); - const std::string out = std::string(mcpp::out_dir()) + "/app.zap"; - mcpp::action a; - a.id = "zap"; a.role = "artifact"; - a.arg((root + "/dist.sh").c_str()).arg("named").arg(root.c_str()).arg(out.c_str()) - .input("${mcpp.target_file:app}").output(out.c_str()); - a.submit(); - return 0; -} -EOF -"$MCPP" pack --pkg-format zap > b9.log 2>&1 || { cat b9.log; echo "FAIL: a provider that reads no staged tree was refused"; exit 1; } -grep -q "app.zap" b9.log \ - || { cat b9.log; echo "FAIL: the provider was not reported as the package"; exit 1; } -# It really did not consume the tree: no stage manifest edge was added, so the -# action's inputs are the program alone. -grep -q "stage-manifest" b9.log \ - && { cat b9.log; echo "FAIL: a provider that names no tree gained a stage dependency"; exit 1; } -echo "ok: a provider that reads no staged tree is dispatched and reported" - -echo "PASS: 638_pack_format_dispatch" diff --git a/tests/e2e/649_pack_carries_runtime_deploy_files.sh b/tests/e2e/649_pack_carries_runtime_deploy_files.sh deleted file mode 100755 index 2d49a3758..000000000 --- a/tests/e2e/649_pack_carries_runtime_deploy_files.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env bash -# requires: pack -# 649_pack_carries_runtime_deploy_files.sh -- `mcpp pack` stages what the build -# placed relative to the executable (`runtime.deploy_files` and -# `runtime.deploy`, #615) at the same relative path beside the packed -# executable. Pack read neither list before, so a program that found its driver -# manifest in the build directory could not find it once packed. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -export MCPP_HOME=$HOME/.mcpp - -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } - -cd "$TMP" -"$MCPP" new app > /dev/null -cd app -mkdir -p share/vulkan/icd.d -printf 'icd manifest\n' > share/vulkan/icd.d/lvp_icd.json -printf 'notes\n' > share/notes.txt -cat >> mcpp.toml <<'TOML' - -[toolchain] -linux = "gcc@16.1.0" - -[runtime] -deploy_files = ["share/notes.txt"] -deploy = [ { from = "share/vulkan/icd.d/lvp_icd.json", to = "vulkan/icd.d" } ] -TOML - -"$MCPP" pack > pack.log 2>&1 || fail "mcpp pack failed" pack.log -tarball=$(ls target/dist/app-0.1.0-*.tar.gz 2>/dev/null | head -1) -[ -n "$tarball" ] || fail "no tarball under target/dist" pack.log -mkdir -p "$TMP/x" -tar -xzf "$tarball" -C "$TMP/x" -root=$(ls -d "$TMP"/x/app-0.1.0-*/ | head -1) -[ -x "${root}bin/app" ] || fail "the bundle has no bin/app" pack.log -grep -q 'icd manifest' "${root}bin/vulkan/icd.d/lvp_icd.json" 2>/dev/null \ - || fail "runtime.deploy did not reach bin/vulkan/icd.d in the bundle" pack.log -grep -q 'notes' "${root}bin/notes.txt" 2>/dev/null \ - || fail "runtime.deploy_files did not reach bin/ in the bundle" pack.log -echo "pack carries runtime files OK" diff --git a/tests/e2e/652_an_application_is_a_kind.sh b/tests/e2e/652_an_application_is_a_kind.sh deleted file mode 100755 index ad1decb8f..000000000 --- a/tests/e2e/652_an_application_is_a_kind.sh +++ /dev/null @@ -1,106 +0,0 @@ -#!/usr/bin/env bash -# requires: elf gcc -# 652_an_application_is_a_kind.sh -- `kind = "app"` (design record #622 A3, -# 2026-09-12-622-a-ui-framework-on-android-ios-and-web.md §2.3). -# -# An application is "the thing a user launches", on every row. Its link form -# is a property of the ROW, not of the manifest: on every row this script can -# exercise directly (an ELF host) it is identical to `bin`; the Android row, -# where it is a shared object instead, is 652b (`# requires: android-ndk`). -# -# OLDER-ENGINE COMPATIBILITY, BY READING RATHER THAN BY RUNNING (§1 rule 6, -# T3 step 7): `mcpp 2026.9.12.2`'s `toml.cppm:1110` reads -# -# targets..kind must be 'bin', 'lib' or 'shared'; got '' -# -# -- three kinds. Verified by hand against that exact binary -# (~/.xlings/data/xpkgs/xim-x-mcpp/2026.9.12.2/bin/mcpp) while writing this -# script: `kind = "app"` is refused, naming three kinds, which is correct -- -# an application manifest is a root, and a root that names a form an older -# engine cannot produce must not build. The refusal lists every kind: -# 'bin', 'app', 'lib', 'shared' or 'entry' (2026.10.26.2) -- asserted below on -# the engine under test. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT - -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } - -cd "$TMP" -mkdir -p src - -cat > mcpp.toml <<'TOML' -[package] -name = "myapp" -version = "0.1.0" - -[targets.myapp] -kind = "app" -main = "src/main.cpp" - -[targets.tool] -kind = "bin" -main = "src/tool.cpp" -TOML - -cat > src/main.cpp <<'CPP' -#include -int main() { std::puts("APP-RUN"); return 0; } -CPP -cat > src/tool.cpp <<'CPP' -#include -int main() { std::puts("TOOL-RUN"); return 0; } -CPP - -# ── 1. The refusal, on the engine under test, names every kind ──────────── -cat > bad.toml <<'TOML' -[package] -name = "p" -version = "0.1.0" - -[targets.t] -kind = "framework" -TOML -mkdir -p bad/src -cp bad.toml bad/mcpp.toml -printf 'int main(){}\n' > bad/src/main.cpp -out=$( cd bad && "$MCPP" build 2>&1 ) && fail "an unrecognized kind must be refused" <(echo "$out") -grep -q "must be 'bin', 'app', 'lib', 'shared' or 'entry'" <<<"$out" \ - || fail "the refusal does not list every kind" <(echo "$out") -echo "unrecognized kind lists every kind OK" - -# ── 2. The host build: both targets link, `app` == `bin` off Android ─────── -"$MCPP" build > build.log 2>&1 || fail "the host build failed" build.log -outdir=$(dirname "$(ls -t target/*/*/build.ninja | head -1)") -[ -x "$outdir/bin/myapp" ] || fail "bin/myapp was not linked" build.log -[ -x "$outdir/bin/tool" ] || fail "bin/tool was not linked" build.log -file "$outdir/bin/myapp" | grep -qi "executable" \ - || fail "bin/myapp is not an executable (an app's host form must be one)" build.log -echo "host build links app and bin as executables OK" - -# ── 3. `mcpp run` of an app whose form here IS an executable just runs ───── -out=$("$MCPP" run myapp 2>&1) || fail "mcpp run myapp failed on the host" <(echo "$out") -grep -q "APP-RUN" <<<"$out" || fail "mcpp run myapp did not print its marker" <(echo "$out") -echo "mcpp run myapp OK" - -# Negative-direction control for (3): a target that is genuinely absent is -# still refused as before -- the app-aware run path must not swallow that. -out=$("$MCPP" run nosuchtarget 2>&1) && fail "mcpp run of a nonexistent target must fail" <(echo "$out") -grep -q "no binary target 'nosuchtarget' found" <<<"$out" \ - || fail "the generic no-binary-target refusal changed wording" <(echo "$out") -echo "mcpp run of a nonexistent target still refused generically OK" - -# ── 4. `bin` is unaffected: it still runs like it always has ─────────────── -out=$("$MCPP" run tool 2>&1) || fail "mcpp run tool failed" <(echo "$out") -grep -q "TOOL-RUN" <<<"$out" || fail "mcpp run tool did not print its marker" <(echo "$out") -echo "mcpp run tool (bin) unaffected OK" - -# ── 5. `mcpp pack` treats `app` as the program route, same as `bin` ──────── -"$MCPP" pack --pkg-format dir > pack.log 2>&1 || fail "mcpp pack failed" pack.log -staged=$(ls -d target/dist/myapp-0.1.0-*/ 2>/dev/null | head -1) -[ -n "$staged" ] || fail "no staged tree under target/dist" pack.log -[ -x "${staged}bin/myapp" ] || fail "the staged tree has no bin/myapp" pack.log -echo "mcpp pack routes an app as a program OK" - -echo "652: kind = \"app\" OK" diff --git a/tests/e2e/662_pack_stages_declared_files_before_the_closure.sh b/tests/e2e/662_pack_stages_declared_files_before_the_closure.sh deleted file mode 100755 index baad1bcfb..000000000 --- a/tests/e2e/662_pack_stages_declared_files_before_the_closure.sh +++ /dev/null @@ -1,160 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 662_pack_stages_declared_files_before_the_closure.sh -- `mcpp pack` stages -# the program and its declared runtime files BEFORE it asks whether this -# host can walk the artifact's dependency closure, and records the outcome -# of that closure walk on the stage manifest. -# -# See .agents/docs/2026-09-13-630-what-a-framework-still-hits-in-the-engine.md -# §3. Before this, a program whose closure could not be walked (a Mach-O on -# any host, a non-PE artifact on a Windows host) was refused BEFORE anything -# was staged, so a dispatched format that names one program and never reads -# the closure (an `.app`/`.msi` bundler) had no tree to work from either. -# Reordering the steps gives every dispatched format a tree regardless, and -# leaves the two ELF-native formats (`tar`, `dir`) exactly as they were: the -# staged/archived tree IS the product for those two, so an unavailable -# closure remains the command failing. -# -# TWO LEGS ON THIS (LINUX) HOST, EACH WITH ITS OWN CLAIM: -# -# A. `--format dir` of an ordinary ELF program: the reorder must not change -# the ELF product. Asserted as an exact structure -- `bin/`, -# the declared runtime file, an empty `lib/` (this fixture bundles -# nothing), and a stage manifest that says `closure = walked` -- rather -# than "the command exited 0", which a wrong reorder also satisfies. -# B. A DISPATCHED format (`--format zap`, in the shape 638 uses) whose -# action reads `${mcpp.stage_dir}`: the tree it sees carries the -# declared runtime file, and the manifest beside it says -# `closure = walked` too -- on Linux the ELF closure is always walked, -# so this leg's job is to prove the field REACHES a provider, not that -# it ever reads "not-walked" here. -# -# THE "NOT-WALKED" CASE CANNOT BE PRODUCED ON LINUX. It needs a Mach-O -# program (refused on every host, by format) or a non-PE artifact packed -# from a Windows host -- neither is reachable from a `gcc`-only Linux runner. -# It is covered instead by: -# - a unit test of the pure decision function -# (`PackClosureUnavailableOutcome` in test_pack_modes.cpp) and of the -# manifest writer/reader round-trip for both values -# (`PackStageTree.*Closure*` in test_pack_stage_tree.cpp); -# - 266_pack_refuses_a_macho_program.sh (`# requires: macos`), which holds -# the negative direction end to end: `mcpp pack` (bare, i.e. `--format -# tar`) of a Mach-O program still exits non-zero. -# The macOS half of THIS fixture -- a dispatched format on a Mach-O program -# staging without its closure, with `closure = not-walked` on the manifest -# and the reason readable by the provider -- is a follow-up for whoever runs -# this suite's iOS/macOS CI job; it is not asserted by any step here. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -export MCPP_HOME=$HOME/.mcpp - -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } - -# ── Leg A: `--format dir`, byte-for-byte the same ELF product ────────────── -cd "$TMP" -"$MCPP" new app > /dev/null -cd app -mkdir -p share -printf 'notes\n' > share/notes.txt -cat >> mcpp.toml <<'EOF' - -[toolchain] -linux = "gcc@16.1.0" - -[runtime] -deploy_files = ["share/notes.txt"] -EOF - -"$MCPP" pack --pkg-format dir > dir.log 2>&1 || fail "mcpp pack --pkg-format dir failed" dir.log - -tree=$(find target/dist -maxdepth 1 -type d -name 'app-0.1.0-*' | head -1) -[ -n "$tree" ] || fail "no staged directory under target/dist" dir.log -[ -x "$tree/bin/app" ] || fail "the staged tree has no executable bin/app" dir.log -[ -f "$tree/bin/notes.txt" ] || fail "the declared runtime file did not reach bin/" dir.log -grep -q 'notes' "$tree/bin/notes.txt" || fail "the deployed file's content is wrong" dir.log -# No third-party dependency was declared, so nothing should be bundled -- -# the reorder must not manufacture a lib/ entry that was not there before it. -if [ -d "$tree/lib" ]; then - n=$(find "$tree/lib" -mindepth 1 | wc -l) - [ "$n" -eq 0 ] || { find "$tree/lib"; fail "lib/ has entries but nothing was declared" dir.log; } -fi -"$tree/bin/app" > run.log 2>&1 || fail "the staged program does not run" run.log dir.log - -manifest="$(dirname "$tree")/$(basename "$tree").stage-manifest" -[ -f "$manifest" ] || fail "no stage manifest beside the staged tree" dir.log -[ "$(sed -n '1p' "$manifest")" = "closure = walked" ] \ - || fail "the manifest's first line is not 'closure = walked'" "$manifest" dir.log -grep -q '^reason = ' "$manifest" \ - && fail "a walked closure must carry no reason line" "$manifest" -grep -q ' bin/notes.txt$' "$manifest" || fail "the deploy file is not in the manifest" "$manifest" -echo " leg A: --format dir stages bin/app + the deploy file, closure = walked" - -# ── Leg B: a dispatched format reads the same tree + the same field ──────── -cd "$TMP" -"$MCPP" new zapapp > /dev/null -cd zapapp -mkdir -p share -printf 'notes\n' > share/notes.txt -cat >> mcpp.toml <<'EOF' - -[toolchain] -linux = "gcc@16.1.0" - -[runtime] -deploy_files = ["share/notes.txt"] -EOF - -cat > dist.sh <<'EOF' -#!/usr/bin/env bash -set -e -stage="$1"; manifest="$2"; out="$3" -{ - echo "staged:" - ls -1 "$stage/bin" | sort - echo "manifest-first-line:" - sed -n '1p' "$manifest" -} > "$out" -EOF -chmod +x dist.sh - -cat > build.mcpp <<'EOF' -import mcpp; -#include -#include -int main() { - // Unconditional half: this build knows a "zap" format exists whether or - // not one was requested (638 states why). - mcpp::provides_pack_format("zap"); - if (std::string_view(mcpp::pack_format()) != "zap") return 0; - - const std::string root = mcpp::manifest_dir(); - const std::string stage = std::string("${mcpp.stage_dir}"); - const std::string out = std::string(mcpp::out_dir()) + "/app.zap"; - mcpp::action a; - a.id = "zap"; - a.role = "artifact"; - a.description = "zap"; - a.arg((root + "/dist.sh").c_str()) - .arg(stage.c_str()) - .arg((stage + ".stage-manifest").c_str()) - .arg(out.c_str()) - .input("${mcpp.target_file:zapapp}") - .output(out.c_str()) - .submit(); - return 0; -} -EOF - -"$MCPP" pack --pkg-format zap > zap.log 2>&1 || fail "mcpp pack --pkg-format zap failed" zap.log -Z=$(find target -name 'app.zap' | head -1) -[ -n "$Z" ] || fail "--format zap produced nothing" zap.log - -grep -qx "notes.txt" "$Z" \ - || fail "the action's own view of the staged tree lacks the deploy file" "$Z" zap.log -grep -qx "closure = walked" "$Z" \ - || fail "the manifest the action read does not say closure = walked" "$Z" zap.log -echo " leg B: --format zap's action sees the deploy file and closure = walked" - -echo "PASS: 662_pack_stages_declared_files_before_the_closure" diff --git a/tests/e2e/669_an_elf_host_pack_states_its_closure.sh b/tests/e2e/669_an_elf_host_pack_states_its_closure.sh deleted file mode 100755 index 78c4e7b3c..000000000 --- a/tests/e2e/669_an_elf_host_pack_states_its_closure.sh +++ /dev/null @@ -1,126 +0,0 @@ -#!/usr/bin/env bash -# requires: elf gcc -# 669 -- #634 A3 and A4 on the ELF host row. -# -# A4: an ELF shared library built without a declared `soname` records its file -# name as DT_SONAME; its consumer's DT_NEEDED is the same name it was before, -# and a declared `soname` keeps its value. -# -# A3: the host row keeps its loader-traced closure, and the stage manifest -# gains the `needs` lines every other row writes: a bundled library at its -# staged path, a library the target provides as `platform`. A name the loader -# finds no file for used to be dropped, so an archive that could not start -# said `closure = walked`; now `dir` refuses naming it. -set -e - -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -export MCPP_HOME=${MCPP_HOME:-$HOME/.mcpp} - -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } -TAB=$(printf '\t') -soname_of() { readelf -d "$1" | sed -n 's/.*(SONAME).*\[\(.*\)\].*/\1/p'; } - -cd "$TMP" -mkdir -p fw/src named/src app/src -cat > fw/mcpp.toml <<'EOF' -[package] -namespace = "demo" -name = "fw" -version = "0.1.0" -[targets.fw] -kind = "lib" -EOF -printf 'export module fw;\nexport int fw_anchor() { return 41; }\n' > fw/src/fw.cppm - -cat > named/mcpp.toml <<'EOF' -[package] -namespace = "demo" -name = "named" -version = "0.1.0" -[targets.named] -kind = "shared" -soname = "libnamed.so.1" -EOF -printf 'export module named;\nexport int named_anchor() { return 1; }\n' > named/src/named.cppm - -cat > app/mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" -[toolchain] -linux = "gcc@16.1.0" -[dependencies] -demo.fw = { path = "../fw", linkage = "shared" } -demo.named = { path = "../named" } -EOF -cat > app/src/main.cpp <<'EOF' -#include -import fw; -import named; -int main() { std::puts("1-2-3"); return fw_anchor() == 41 && named_anchor() == 1 ? 0 : 1; } -EOF -cd app - -# ── 1. A4: the default SONAME, and the declared one ─────────────────────── -"$MCPP" build > build.log 2>&1 || fail "mcpp build failed" build.log -bin=$(dirname "$(find target -path '*/bin/app' -type f | head -1)") -[ -f "$bin/libfw.so" ] || fail "no bin/libfw.so" build.log -[ "$(soname_of "$bin/libfw.so")" = "libfw.so" ] \ - || fail "libfw.so carries SONAME '$(soname_of "$bin/libfw.so")', not its file name" build.log -readelf -d "$bin/app" | grep -q 'NEEDED.*\[libfw.so\]' \ - || fail "the consumer no longer needs libfw.so by its file name" build.log -named=$(find "$bin" -maxdepth 1 -name 'libnamed.so' -type f | head -1) -[ -n "$named" ] || fail "no bin/libnamed.so" build.log -[ "$(soname_of "$named")" = "libnamed.so.1" ] \ - || fail "the declared soname became '$(soname_of "$named")'" build.log -echo " ok: libfw.so records its file name; a declared soname keeps libnamed.so.1" - -# ── 2. A3: the manifest states the closure the tree carries ─────────────── -"$MCPP" pack --pkg-format dir > dir.log 2>&1 || fail "mcpp pack --pkg-format dir failed" dir.log -tree=$(find target/dist -mindepth 1 -maxdepth 1 -type d -name 'app-0.1.0-*' | head -1) -[ -n "$tree" ] || fail "no staged tree" dir.log -manifest="$tree.stage-manifest" -[ "$(sed -n '1p' "$manifest")" = "closure = walked" ] \ - || fail "the manifest's first line is not 'closure = walked'" "$manifest" -[ -f "$tree/lib/libfw.so" ] || fail "lib/libfw.so is not bundled" dir.log -grep -qxF "needs${TAB}libfw.so${TAB}lib/libfw.so" "$manifest" \ - || fail "the manifest does not state libfw.so at lib/libfw.so" "$manifest" -grep -qxF "needs${TAB}libc.so.6${TAB}platform" "$manifest" \ - || fail "the manifest does not state libc.so.6 as the platform's" "$manifest" -# The needs block sits between the header and the file list. -awk 'NR==1{next} /^needs\t/{ if (files) bad=1; next } { files=1 } END{ exit bad }' "$manifest" \ - || fail "a needs line follows a file line" "$manifest" -"$tree/bin/app" > run.log 2>&1 || fail "the staged program does not run" run.log -grep -q '^1-2-3$' run.log || fail "the staged program did not print 1-2-3" run.log -echo " ok: needs lines for the bundled and the platform libraries; the tree runs" - -# ── 3. a library the loader cannot find refuses the archive ─────────────── -cd "$TMP" -mkdir -p ext/lib extapp/src -printf 'int ext_value() { return 1; }\n' > ext/ext.cpp -g++ -shared -fPIC -Wl,-soname,libext.so -o ext/lib/libext.so ext/ext.cpp -cat > extapp/mcpp.toml < extapp/src/main.cpp -cd extapp -"$MCPP" pack --pkg-format dir > ext.log 2>&1 || fail "the pack with a prebuilt library failed" ext.log -etree=$(find target/dist -mindepth 1 -maxdepth 1 -type d -name 'extapp-0.1.0-*' | head -1) -grep -qxF "needs${TAB}libext.so${TAB}lib/libext.so" "$etree.stage-manifest" \ - || fail "libext.so is not stated as bundled" "$etree.stage-manifest" -rm "$TMP/ext/lib/libext.so" -if "$MCPP" pack --pkg-format dir > gone.log 2>&1; then - fail "--format dir succeeded with libext.so gone" gone.log -fi -grep -q "libext.so: the loader finds no file for it" gone.log \ - || fail "the refusal does not name libext.so" gone.log -echo " ok: dir refuses, naming the library the loader cannot find" - -echo "669: an ELF host pack states its closure OK" diff --git a/tests/e2e/689_pack_takes_features.sh b/tests/e2e/689_pack_takes_features.sh deleted file mode 100644 index 0bc4f0f0b..000000000 --- a/tests/e2e/689_pack_takes_features.sh +++ /dev/null @@ -1,124 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc -# 689 -- `mcpp pack --features ` activates root-package features for the -# build passes the pack performs (#641, item 4). A host tool needed only for one -# distribution is declared under `[feature-deps.]` with `tools = [...]`, and -# `pack` is the command that asks for it; the option did not exist, so the -# declaration had to be unconditional and every plain build compiled the tool. -# -# Three readings, each against the other two: the pack with the feature builds -# the tool and ships the program the tool generated; the pack without it builds -# no tool; a plain build without it builds no tool either. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -# An isolated store, so that a tool built by an earlier run cannot answer. -export MCPP_HOME="$TMP/mcpphome" -mkdir -p "$MCPP_HOME" -if [ -d "$HOME/.mcpp/registry" ]; then - ln -s "$HOME/.mcpp/registry" "$MCPP_HOME/registry" -fi - -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } - -mkdir -p toolpkg/src app/src -cat > toolpkg/mcpp.toml <<'EOF' -[package] -name = "toolpkg" -version = "0.1.0" - -[targets.gen] -kind = "bin" -main = "src/gen.cpp" -EOF -cat > toolpkg/src/gen.cpp <<'EOF' -#include -int main(int argc, char** argv) { - if (argc < 2) return 2; - FILE* f = std::fopen(argv[1], "w"); - if (!f) return 3; - std::fprintf(f, "int generated_answer() { return 41; }\n"); - std::fclose(f); - return 0; -} -EOF - -cat > app/mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" - -[targets.app] -kind = "bin" -main = "src/main.cpp" - -[features] -installer = {} - -[feature-deps.installer] -toolpkg = { path = "../toolpkg", tools = ["gen"] } -EOF -cat > app/src/main.cpp <<'EOF' -#include -int generated_answer(); -int main() { std::printf("ANSWER=%d\n", generated_answer()); } -EOF -# The build program asks for the tool only when the feature is active, which -# is the use the issue describes: without the feature, `dep_bin` is not reached. -cat > app/build.mcpp <<'EOF' -#include -#include -#include -import mcpp; -int main() { - std::string out = std::string(mcpp::out_dir()) + "/gen.cpp"; - if (mcpp::has_feature("installer")) { - const char* tool = mcpp::dep_bin("toolpkg", "gen"); - if (!tool || !*tool) { std::fprintf(stderr, "no tool path\n"); return 1; } - std::string cmd = std::string("\"") + tool + "\" \"" + out + "\""; - if (std::system(cmd.c_str()) != 0) { std::fprintf(stderr, "tool failed\n"); return 1; } - } else { - FILE* f = std::fopen(out.c_str(), "w"); - if (!f) return 1; - std::fprintf(f, "int generated_answer() { return 0; }\n"); - std::fclose(f); - } - mcpp::generated(out.c_str()); -} -EOF -cd app - -packed_answer() { # $1 = log; runs the program the last pack staged - local exe - exe=$(find target/dist -type f -name app -perm -u+x 2>/dev/null | head -1) - [ -n "$exe" ] || fail "the pack staged no program" "$1" - "$exe" -} -tool_entries() { find "$MCPP_HOME/build-cache" -path '*/tool/*' -name 'gen*' -type f 2>/dev/null | wc -l; } - -# ── The pack without the feature: no tool, the fallback source ───────────── -"$MCPP" pack --pkg-format dir > p0.log 2>&1 || fail "pack without the feature failed" p0.log -[ "$(packed_answer p0.log)" = "ANSWER=0" ] || fail "pack without the feature: expected 0" p0.log -[ "$(tool_entries)" -eq 0 ] || fail "a pack without the feature built the host tool" p0.log -grep -q "host tool" p0.log && fail "a pack without the feature mentioned the host tool" p0.log -echo "ok: a pack without --features builds no host tool" - -# ── The pack with the feature: the tool is built and its output shipped ──── -rm -rf target -"$MCPP" pack --pkg-format dir --features installer > p1.log 2>&1 \ - || fail "mcpp pack --features installer failed" p1.log -grep -q "unknown option" p1.log && fail "pack did not accept --features" p1.log -grep -q "Building.*host tool" p1.log || fail "the pack with the feature did not build the tool" p1.log -[ "$(packed_answer p1.log)" = "ANSWER=41" ] || fail "pack with the feature: expected 41" p1.log -echo "ok: mcpp pack --features activates the feature-gated host tool" - -# ── A plain build without the feature: no tool ─────────────────────────── -rm -rf target "$MCPP_HOME/build-cache" -"$MCPP" build > b0.log 2>&1 || fail "the plain build failed" b0.log -[ "$(tool_entries)" -eq 0 ] || fail "a plain build without the feature built the host tool" b0.log -echo "ok: a plain build without the feature builds no host tool" - -echo "PASS: mcpp pack --features reaches the build passes of the pack" diff --git a/tests/e2e/722_pack_strips_what_the_graph_built.sh b/tests/e2e/722_pack_strips_what_the_graph_built.sh deleted file mode 100755 index 77dbe5919..000000000 --- a/tests/e2e/722_pack_strips_what_the_graph_built.sh +++ /dev/null @@ -1,210 +0,0 @@ -#!/usr/bin/env bash -# requires: pack python3 -# 722 -- `mcpp pack` strips what the graph built on every row that strips, and -# says so only when it does (#649 E5). -# -# `strip_program` used to strip the staged PROGRAM and nothing else, on the ELF -# and PE paths only. A shared library the same graph compiled from source -# shipped its `.symtab` and `.debug_*`, and the Android row, which `run` -# dispatches before either path, stripped nothing at all while the `Packing` -# line said "stripped". A member that stages libraries of its own had no way to -# learn the decision, so `--no-strip` did not reach its files. -# -# Legs: -# A. Linux desktop, `bin` over a shared dependency: `bin/hostapp` and -# `lib/libdep.so` carry no `.symtab` and no `.debug_*`; `libdep.so` still -# exports `dep_answer` in `.dynsym`; the unpacked program runs. -# B. `--no-strip`: `lib/libdep.so` keeps its `.symtab`, and the `Packing` -# line does not say "stripped". -# C. `--debug-symbols DIR`: `DIR/libdep.so.debug` exists and the packed -# library names it through `.gnu_debuglink`. -# D. A dispatched format's build program reads `pack_strip()` = "1" and -# `pack_debug_symbols_dir()` = DIR, and "0" under `--no-strip`. -# E. When the NDK is installed: `--target x86_64-linux-android` gives -# `lib/libapp.so`, `lib/libdep.so` and `lib/libc++_shared.so` with no -# `.symtab`, and `.dynsym` still names `app_entry` and `dep_answer`. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } - -# Sections and dynamic symbols, read from the ELF itself. -elf_facts() { # elf_facts -> "symtab=N debug=N dynsym=" -python3 - "$1" <<'PY' -import struct, sys -d = open(sys.argv[1], 'rb').read() -assert d[:4] == b'\x7fELF' and d[4] == 2, "not ELF64" -shoff, = struct.unpack_from(' dep/mcpp.toml <<'EOF' -[package] -name = "dep" -namespace = "spike" -version = "0.1.0" -standard = "c++20" - -[targets.dep] -kind = "shared" - -[build] -sources = ["src/*.cpp"] -EOF -cat > dep/src/dep.cpp <<'EOF' -#include -[[gnu::visibility("default")]] int dep_answer() { return static_cast(std::to_string(42).size()) + 40; } -EOF - -cat > hostapp/mcpp.toml <<'EOF' -[package] -name = "hostapp" -version = "0.1.0" -standard = "c++20" - -[dependencies] -spike.dep = { path = "../dep" } - -[targets.hostapp] -kind = "bin" -main = "src/main.cpp" -EOF -printf 'int dep_answer();\nint main() { return dep_answer() == 42 ? 0 : 1; }\n' > hostapp/src/main.cpp -cd hostapp - -# ── A ─────────────────────────────────────────────────────────────────────── -"$MCPP" pack --pkg-format tar > a.log 2>&1 || fail "A: pack failed" a.log -grep -q "Packing hostapp v0.1.0 (vendored, stripped)" a.log || fail "A: the Packing line" a.log -rm -rf xa && mkdir xa && tar -xzf target/dist/hostapp-0.1.0-x86_64-linux-gnu.tar.gz -C xa -lib=$(ls xa/*/lib/libdep.so); prog=$(ls xa/*/bin/hostapp) -fa=$(elf_facts "$lib"); fp=$(elf_facts "$prog") -echo "reading A: libdep.so $fa" -echo "reading A: hostapp $fp" -case "$fa" in "symtab=0 debug=0 "*) ;; *) fail "A: lib/libdep.so is not stripped: $fa" ;; esac -case "$fa" in *dep_answer*) ;; *) fail "A: lib/libdep.so lost its export: $fa" ;; esac -case "$fp" in "symtab=0 debug=0 "*) ;; *) fail "A: bin/hostapp is not stripped: $fp" ;; esac -"$(ls -d xa/*)/hostapp" || fail "A: the unpacked program does not run" a.log -echo "ok: A, the program and the graph-built library are stripped and run" - -# ── B ─────────────────────────────────────────────────────────────────────── -"$MCPP" pack --pkg-format tar --no-strip > b.log 2>&1 || fail "B: pack failed" b.log -grep -q "Packing hostapp v0.1.0 (vendored)" b.log || fail "B: the Packing line" b.log -rm -rf xb && mkdir xb && tar -xzf target/dist/hostapp-0.1.0-x86_64-linux-gnu.tar.gz -C xb -fb=$(elf_facts "$(ls xb/*/lib/libdep.so)") -echo "reading B: libdep.so $fb" -case "$fb" in "symtab=1 "*) ;; *) fail "B: --no-strip did not reach lib/libdep.so: $fb" ;; esac -echo "ok: B, --no-strip reaches the graph-built library" - -# ── C ─────────────────────────────────────────────────────────────────────── -"$MCPP" pack --pkg-format tar --debug-symbols "$TMP/dbg" > c.log 2>&1 || fail "C: pack failed" c.log -[ -s "$TMP/dbg/libdep.so.debug" ] || fail "C: no libdep.so.debug" c.log -rm -rf xc && mkdir xc && tar -xzf target/dist/hostapp-0.1.0-x86_64-linux-gnu.tar.gz -C xc -fc=$(elf_facts "$(ls xc/*/lib/libdep.so)") -case "$fc" in *"debuglink=1"*) ;; *) fail "C: lib/libdep.so names no debug file: $fc" ;; esac -echo "ok: C, --debug-symbols separates the graph-built library's debug information" - -# ── D ─────────────────────────────────────────────────────────────────────── -# Written only now, so the legs above measure the strip itself on any engine. -# D's provider: records what a dispatched format's program is told. -cat > build.mcpp <<'EOF' -import mcpp; -#include -#include -#include -int main() { - mcpp::provides_pack_format("probe"); - if (std::string_view(mcpp::pack_format()) != "probe") return 0; - const std::string out = std::string(mcpp::out_dir()) + "/probe.txt"; - const std::string text = std::string("strip=") + mcpp::pack_strip() - + " debug=" + mcpp::pack_debug_symbols_dir(); - mcpp::action a; - a.id = "probe"; - a.role = "artifact"; - a.description = "probe"; - a.arg("/bin/sh").arg("-c").arg(("printf '%s' '" + text + "' > " + out).c_str()) - .input("${mcpp.target_file:hostapp}") - .output(out.c_str()) - .submit(); - return 0; -} -EOF - -"$MCPP" pack --pkg-format probe --debug-symbols "$TMP/dbg2" > d.log 2>&1 || fail "D: pack failed" d.log -probe=$(find target -name probe.txt | head -1) -[ "$(cat "$probe")" = "strip=1 debug=$TMP/dbg2" ] || fail "D: the program read '$(cat "$probe" 2>/dev/null)'" d.log -"$MCPP" pack --pkg-format probe --no-strip > d2.log 2>&1 || fail "D: pack --no-strip failed" d2.log -[ "$(cat "$probe")" = "strip=0 debug=" ] || fail "D: under --no-strip the program read '$(cat "$probe")'" d2.log -echo "ok: D, a dispatched format's program reads the strip decision" - -# ── E ─────────────────────────────────────────────────────────────────────── -ndk=$(ls -d "${MCPP_HOME:-$HOME/.mcpp}"/registry/data/xpkgs/xim-x-android-ndk/*/ 2>/dev/null | head -1 || true) -if [ -z "$ndk" ]; then - echo "skip: E, xim:android-ndk is not installed on this machine" -else - cd "$TMP" - cat > app/mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" -standard = "c++20" - -[dependencies] -spike.dep = { path = "../dep" } - -[targets.app] -kind = "app" -main = "src/main.cpp" - -[target.x86_64-linux-android] -min_api_level = 23 -EOF - cat > app/src/main.cpp <<'EOF' -#include -int dep_answer(); -[[gnu::visibility("default")]] int app_entry() { return dep_answer() + static_cast(std::to_string(1).size()); } -int main() { return app_entry() == 43 ? 0 : 1; } -EOF - cd app - "$MCPP" pack --target x86_64-linux-android --pkg-format tar > e.log 2>&1 || fail "E: pack failed" e.log - grep -q "Packing app v0.1.0 (vendored, stripped)" e.log || fail "E: the Packing line" e.log - rm -rf xe && mkdir xe && tar -xzf target/dist/app-0.1.0-x86_64-linux-android.tar.gz -C xe - for f in libapp.so libdep.so libc++_shared.so; do - fe=$(elf_facts "$(ls xe/*/lib/$f)") - echo "reading E: $f $(echo "$fe" | cut -d' ' -f1-3)" - case "$fe" in "symtab=0 debug=0 "*) ;; *) fail "E: lib/$f is not stripped: $fe" e.log ;; esac - done - case "$(elf_facts "$(ls xe/*/lib/libapp.so)")" in *app_entry*) ;; *) fail "E: libapp.so lost app_entry" ;; esac - case "$(elf_facts "$(ls xe/*/lib/libdep.so)")" in *dep_answer*) ;; *) fail "E: libdep.so lost dep_answer" ;; esac - echo "ok: E, the Android row strips the program, the graph's library and the NDK runtime" -fi - -echo "PASS: 722" diff --git a/tests/e2e/723_pack_reports_what_it_produced.sh b/tests/e2e/723_pack_reports_what_it_produced.sh deleted file mode 100755 index 61e12a1ba..000000000 --- a/tests/e2e/723_pack_reports_what_it_produced.sh +++ /dev/null @@ -1,118 +0,0 @@ -#!/usr/bin/env bash -# requires: pack python3 -# 723 -- `mcpp pack --message-format json` reports what it produced, and `pack` -# takes the profile shorthands `build` and `run` take, with one precedence -# (#649 E9). -# -# A driver that publishes a pack's artifacts used to parse the human `Packed` -# lines, whose paths are shortened (`@mcpp/`, `~/`, project-relative), and a -# driver mapping one profile switch onto three verbs found `--release` refused -# on the third. `mcpp run --profile dev --release` built `release` while -# `mcpp build` given the same line built `dev`. -# -# Legs: -# A. `pack --pkg-format tar --message-format json`: stdout is one `mcpp.pack` -# envelope and nothing else; the artifact path is absolute and exists, its -# type is `file`, its format `tar`, its target the host triple; the staged -# tree and its manifest exist, and the closure is `walked`. -# B. `pack --pkg-format dir --message-format json`: the artifact is a directory. -# C. `pack --release` and `pack --dev` are accepted; `--dev` builds `dev`. -# D. `run --profile dev --release` builds `dev`, as `build` does. -# E. `pack --message-format yaml` is refused with exit 2. -# F. `--protocol-version` lists the `mcpp.pack` kind and the `pack` command. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } - -mkdir -p app/src -cat > app/mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.3.0" - -[targets.app] -kind = "bin" -main = "src/main.cpp" -EOF -printf '#include \nint main() { std::puts("app ran"); return 0; }\n' > app/src/main.cpp -cd app - -# ── A ─────────────────────────────────────────────────────────────────────── -"$MCPP" pack --pkg-format tar --message-format json > a.json 2> a.err || fail "A: pack failed" a.err a.json -python3 - a.json <<'EOF' || fail "A: the envelope" a.json a.err -import json, os, sys -text = open(sys.argv[1]).read() -e = json.loads(text) # the whole of stdout is one document -assert e["kind"] == "mcpp.pack" and e["kindVersion"] == 1, (e["kind"], e.get("kindVersion")) -assert "write-project" in e["effects"], e["effects"] -arts = e["data"]["artifacts"] -assert len(arts) == 1, arts -a = arts[0] -assert os.path.isabs(a["path"]) and os.path.isfile(a["path"]), a -assert a["path"].endswith(".tar.gz") and a["type"] == "file" and a["format"] == "tar", a -assert len(a["targets"]) == 1 and a["targets"][0].endswith("linux-gnu"), a["targets"] -st = e["data"]["stage"] -assert os.path.isdir(st["dir"]) and os.path.isfile(st["manifest"]), st -assert st["closure"] == "walked", st -assert e["diagnostics"] == [], e["diagnostics"] -EOF -grep -q "Packed" a.err || fail "A: the human lines did not go to stderr" a.err -echo "ok: A, one envelope on stdout names the archive, its legs and the staged tree" - -# ── B ─────────────────────────────────────────────────────────────────────── -"$MCPP" pack --pkg-format dir --message-format json > b.json 2> b.err || fail "B: pack failed" b.err -python3 - b.json <<'EOF' || fail "B: the envelope" b.json -import json, os, sys -a = json.load(open(sys.argv[1]))["data"]["artifacts"][0] -assert a["type"] == "directory" and os.path.isdir(a["path"]) and a["format"] == "dir", a -EOF -echo "ok: B, a directory artifact is reported as one" - -# ── C ─────────────────────────────────────────────────────────────────────── -# `pack` narrates no `Finished` line, so the profile is read from the graph the -# pack built: `dev` compiles at `-O0`, `release` does not. Each sub-leg starts -# from an empty `target/`, so the one graph there is the one this pack wrote. -built_dev() { grep -q -- " -O0" "$(ls target/*-linux-gnu/*/build.ninja | head -1)"; } -rm -rf target -"$MCPP" pack --release --pkg-format tar > c1.log 2>&1 || fail "C: pack --release was refused" c1.log -built_dev && fail "C: pack --release built dev" c1.log -rm -rf target -"$MCPP" pack --dev --pkg-format tar > c2.log 2>&1 || fail "C: pack --dev was refused" c2.log -built_dev || fail "C: pack --dev did not build dev" c2.log -rm -rf target -"$MCPP" pack --profile release --dev --pkg-format tar > c3.log 2>&1 || fail "C: pack --profile --dev failed" c3.log -built_dev && fail "C: --dev won over --profile release on pack" c3.log -echo "ok: C, pack takes --release and --dev, and --profile wins" - -# ── D ─────────────────────────────────────────────────────────────────────── -"$MCPP" run --profile dev --release > d.log 2>&1 || fail "D: run failed" d.log -grep -q "Finished dev" d.log || fail "D: run --profile dev --release did not build dev" d.log -"$MCPP" build --profile dev --release > d2.log 2>&1 || fail "D: build failed" d2.log -grep -q "Finished dev" d2.log || fail "D: build --profile dev --release did not build dev" d2.log -echo "ok: D, run and build resolve --profile over the shorthands alike" - -# ── E ─────────────────────────────────────────────────────────────────────── -set +e -"$MCPP" pack --pkg-format tar --message-format yaml > e.out 2> e.err -rc=$? -set -e -[ "$rc" -eq 2 ] || fail "E: --message-format yaml exited $rc" e.err -grep -q "unknown --message-format 'yaml'" e.err || fail "E: the refusal does not name the value" e.err -echo "ok: E, an unknown message format is refused" - -# ── F ─────────────────────────────────────────────────────────────────────── -"$MCPP" --protocol-version > f.json -python3 - f.json <<'EOF' || fail "F: the protocol document" f.json -import json, sys -p = json.load(open(sys.argv[1])) -assert p["kinds"]["mcpp.pack"] == 1, p["kinds"] -fx = p["commands"]["pack"]["effects"] -assert "write-project" in fx and "exec-build-script" in fx, fx -EOF -echo "ok: F, the protocol document declares the kind and the command" - -echo "PASS: 723" diff --git a/tests/e2e/801_a_dependency_program_is_shipped_with_the_consumer.sh b/tests/e2e/801_a_dependency_program_is_shipped_with_the_consumer.sh deleted file mode 100755 index 7c07eac25..000000000 --- a/tests/e2e/801_a_dependency_program_is_shipped_with_the_consumer.sh +++ /dev/null @@ -1,122 +0,0 @@ -#!/usr/bin/env bash -# requires: gcc elf -# 801_a_dependency_program_is_shipped_with_the_consumer.sh — mcpp#711. -# -# `x = { path = "...", artifacts = ["updater"] }` asks for the dependency's -# `bin` target built for the CONSUMER's target and profile, as a link unit of -# the consumer's own plan, beside the consumer's programs in `bin/`. The only -# edge that exposed another package's executable before was `tools`, which -# builds it for the build machine in a nested sub-build: a cross build shipped -# a program for the wrong architecture, built a second time. -# -# Criteria: -# 1. the dependency's program is built into `bin/` and runs; -# 2. none of its code is linked into the consumer (an artifact edge takes the -# program, not the package's code); -# 3. `${mcpp.artifact:updater/updater}` names it in an action; -# 4. `mcpp pack` stages it beside the program; -# 5. under `--target x86_64-linux-musl` it is built for that target (static, -# no PT_INTERP) -- run where a musl toolchain is available. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" - -mkdir -p updater/src app/src -cat > updater/mcpp.toml <<'EOF' -[package] -name = "updater" -version = "0.1.0" - -[targets.updater] -kind = "bin" -main = "src/main.cpp" -EOF -cat > updater/src/helper.cpp <<'EOF' -int updater_only_helper() { return 7; } -EOF -cat > updater/src/main.cpp <<'EOF' -#include -int updater_only_helper(); -int main() { std::printf("UPDATER=%d\n", updater_only_helper()); } -EOF - -cat > app/mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" - -[dependencies] -updater = { path = "../updater", artifacts = ["updater"] } - -[targets.app] -kind = "bin" -main = "src/main.cpp" -EOF -cat > app/src/main.cpp <<'EOF' -#include -int main() { std::printf("APP_OK\n"); } -EOF -cat > app/build.mcpp <<'EOF' -#include -import mcpp; -int main() { - const std::string out = std::string(mcpp::out_dir()) + "/updater.copy"; - mcpp::action a; - a.id = "copy-updater"; - a.role = mcpp::roles::artifact; - a.arg("cp").arg("${mcpp.artifact:updater/updater}").arg(out.c_str()) - .input("${mcpp.artifact:updater/updater}") - .output(out.c_str()) - .submit(); -} -EOF - -cd app -"$MCPP" build > b1.log 2>&1 || { cat b1.log; echo "FAIL: build failed"; exit 1; } - -# ── 1 ── -upd="$(find target -path '*/bin/updater' -type f | head -1)" -[[ -n "$upd" ]] || { cat b1.log; find target -name 'updater*'; echo "FAIL: 1: no bin/updater"; exit 1; } -[[ "$("$upd")" == "UPDATER=7" ]] || { echo "FAIL: 1: the artifact does not run"; exit 1; } -out="$("$MCPP" run 2>&1 | tail -1)" -[[ "$out" == "APP_OK" ]] || { echo "FAIL: 1: mcpp run chose '$out', not the package's program"; exit 1; } -echo "ok: 1" - -# ── 2 ── -app="$(dirname "$upd")/app" -if nm "$app" 2>/dev/null | grep -q updater_only_helper; then - echo "FAIL: 2: the dependency's code was linked into the consumer"; exit 1 -fi -echo "ok: 2" - -# ── 3 ── -copy="$(find target -name updater.copy -type f | head -1)" -[[ -n "$copy" ]] && cmp -s "$copy" "$upd" || { - echo "FAIL: 3: \${mcpp.artifact:} did not reach the action"; exit 1; } -echo "ok: 3" - -# ── 4 ── -"$MCPP" pack > p.log 2>&1 || { cat p.log; echo "FAIL: 4: pack failed"; exit 1; } -tarball="$(find target/dist -name '*.tar.gz' | head -1)" -[[ -n "$tarball" ]] || { cat p.log; echo "FAIL: 4: no archive"; exit 1; } -tar -tzf "$tarball" | grep -q '/updater$' || { - tar -tzf "$tarball"; echo "FAIL: 4: the artifact is not in the archive"; exit 1; } -echo "ok: 4" - -# ── 5 ── -if [[ "$(uname -m)" == x86_64 ]] && { command -v x86_64-linux-musl-g++ >/dev/null 2>&1 \ - || ls "${MCPP_HOME:-$HOME/.mcpp}"/registry/data/xpkgs/xim-x-musl-gcc/*/bin/x86_64-linux-musl-g++ >/dev/null 2>&1; }; then - "$MCPP" build --target x86_64-linux-musl > b5.log 2>&1 || { cat b5.log; echo "FAIL: 5: cross build failed"; exit 1; } - cu="$(find target/x86_64-linux-musl -path '*/bin/updater' -type f | head -1)" - [[ -n "$cu" ]] || { echo "FAIL: 5: no cross-built updater"; exit 1; } - if readelf -l "$cu" | grep -q INTERP; then - echo "FAIL: 5: the artifact was not built for the musl target"; exit 1 - fi - echo "ok: 5" -else - echo "skip: 5 (no musl toolchain on this machine)" -fi - -echo "PASS: 801_a_dependency_program_is_shipped_with_the_consumer" diff --git a/tests/e2e/828_a_library_states_its_interface.sh b/tests/e2e/828_a_library_states_its_interface.sh deleted file mode 100755 index 436502494..000000000 --- a/tests/e2e/828_a_library_states_its_interface.sh +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env bash -# requires: unix-shell -# 828_a_library_states_its_interface.sh -- #734 E6, SPEC-008 phase 1. -# -# A library's interface is its root module and what the root re-exports with -# `export import`, transitively. `mcpp pack` ships that closure. Phase 1 warns -# and never refuses: a library may implement itself in modules and publish only -# headers, and only its author can say which it means. -# -# I1 a library exporting `Alpha` and `Beta` with no root: the build states -# W1 with its impact line; the pack exits 0, names both modules (W2), and -# its "Withheld" row lists both units instead of "(nothing)"; -# I2 the same library with a facade root re-exporting both: both are shipped -# and no W2 appears; -# I3 a consumer importing `Beta` from a library whose root re-exports only -# `Alpha` is warned (W3); importing `Alpha` is not. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } -MCPP="${MCPP:-mcpp}" - -mkdir -p lib/src -cd lib -cat > mcpp.toml <<'EOF' -[package] -namespace = "probe" -name = "flatlib" -version = "0.1.0" - -[build] -sources = ["src/alpha.cppm", "src/beta.cppm"] - -[targets.flatlib] -kind = "lib" -EOF -printf 'export module Alpha;\nexport int alpha() { return 1; }\n' > src/alpha.cppm -printf 'export module Beta;\nimport Alpha;\nexport int beta() { return alpha() + 1; }\n' > src/beta.cppm - -# I1 -"$MCPP" build > i1b.log 2>&1 || fail "I1: the build failed" i1b.log -grep -q "without conventional lib root" i1b.log || fail "I1: W1 is missing" i1b.log -grep -q "impact: \`mcpp pack\` publishes this library without a module interface" i1b.log \ - || fail "I1: W1 has no impact line" i1b.log -"$MCPP" pack flatlib > i1.log 2>&1 || fail "I1: the pack failed" i1.log -grep -q "exports modules Alpha, Beta that the packed form does not ship" i1.log \ - || grep -q "exports modules Beta, Alpha that the packed form does not ship" i1.log \ - || fail "I1: W2 does not name both modules" i1.log -grep -q "Withheld.*alpha.cppm" i1.log && grep -q "Withheld.*beta.cppm" i1.log \ - || fail "I1: the Withheld row does not list both units" i1.log -grep -q "Withheld (nothing)" i1.log && fail "I1: the Withheld row still reads (nothing)" i1.log - -# I2 -cat >> mcpp.toml <<'EOF' - -[lib] -path = "src/flatlib.cppm" -EOF -sed 's|sources = \["src/alpha.cppm", "src/beta.cppm"\]|sources = ["src/flatlib.cppm", "src/alpha.cppm", "src/beta.cppm"]|' mcpp.toml > mcpp.toml.new && mv mcpp.toml.new mcpp.toml # BSD sed has no bare -i -printf 'export module probe.flatlib;\nexport import Alpha;\nexport import Beta;\n' > src/flatlib.cppm -"$MCPP" pack flatlib > i2.log 2>&1 || fail "I2: the pack failed" i2.log -grep -q "does not ship" i2.log && fail "I2: W2 appeared with a facade" i2.log -grep -q "Interface.*alpha.cppm" i2.log && grep -q "Interface.*beta.cppm" i2.log \ - || fail "I2: the facade's modules are not both shipped" i2.log - -# I3 -printf 'export module probe.flatlib;\nexport import Alpha;\nimport Beta;\n' > src/flatlib.cppm -cd "$TMP" -mkdir -p app/src -cat > app/mcpp.toml <<'EOF' -[package] -name = "app828" -version = "0.1.0" - -[dependencies] -"probe.flatlib" = { path = "../lib" } -EOF -printf 'import Alpha;\nint main() { return alpha() == 1 ? 0 : 1; }\n' > app/src/main.cpp -cd app -"$MCPP" build > i3a.log 2>&1 || fail "I3: the build importing a public module failed" i3a.log -grep -q "not one of that package's public modules" i3a.log && fail "I3: a public module was warned about" i3a.log -printf 'import Beta;\nint main() { return beta() == 2 ? 0 : 1; }\n' > src/main.cpp -"$MCPP" build > i3b.log 2>&1 || fail "I3: the build importing a non-public module failed" i3b.log -grep -q "imports 'Beta' of 'probe.flatlib', which is not one of that package's public modules" i3b.log \ - || fail "I3: W3 is missing" i3b.log -grep -q "import a public module of 'probe.flatlib': Alpha" i3b.log || fail "I3: W3 does not name the public modules" i3b.log - -echo "OK" diff --git a/tests/e2e/829_pack_takes_a_member.sh b/tests/e2e/829_pack_takes_a_member.sh deleted file mode 100755 index 61e1ae143..000000000 --- a/tests/e2e/829_pack_takes_a_member.sh +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env bash -# requires: unix-shell -# 829_pack_takes_a_member.sh -- #734 E3. -# -# `build`, `run` and `test` select a workspace member with `-p`, and so does -# `pack`: the member is resolved by the resolver every `-p` shares, and the pack -# runs in its directory. -# -# P1 `mcpp pack -p lib` at the workspace root produces the archive that -# `mcpp pack` in `lib/` produces, with the same contents; -# P2 a relative `-o` keeps meaning the directory the command was typed in; -# P3 a name that is no member is refused, naming the members. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } -MCPP="${MCPP:-mcpp}" - -mkdir -p ws/lib/src ws/app/src -cat > ws/mcpp.toml <<'EOF' -[workspace] -members = ["lib", "app"] -EOF -cat > ws/lib/mcpp.toml <<'EOF' -[package] -namespace = "probe" -name = "packlib" -version = "0.1.0" - -[targets.packlib] -kind = "lib" -EOF -printf 'export module probe.packlib;\nexport int one() { return 1; }\n' > ws/lib/src/packlib.cppm -printf '[package]\nname = "app829"\nversion = "0.1.0"\n' > ws/app/mcpp.toml -printf 'int main() { return 0; }\n' > ws/app/src/main.cpp - -# P1 -cd "$TMP/ws/lib" -"$MCPP" pack > inside.log 2>&1 || fail "P1: pack inside the member failed" inside.log -A=$(ls target/dist/*.tar.gz | head -1) -tar tzf "$A" | sort > "$TMP/inside.list" -rm -rf target/dist -cd "$TMP/ws" -"$MCPP" pack -p lib > root.log 2>&1 || fail "P1: pack -p lib at the root failed" root.log -B=$(ls lib/target/dist/*.tar.gz 2>/dev/null | head -1) -[ -n "$B" ] || fail "P1: pack -p did not write the member's archive" root.log -[ "$(basename "$A")" = "$(basename "$B")" ] || fail "P1: the archives are named differently: $(basename "$A") vs $(basename "$B")" root.log -tar tzf "$B" | sort > "$TMP/root.list" -diff -u "$TMP/inside.list" "$TMP/root.list" > "$TMP/diff.txt" || fail "P1: the archives differ" "$TMP/diff.txt" - -# P2 -"$MCPP" pack -p lib -o out829.tar.gz > p2.log 2>&1 || fail "P2: pack -p with -o failed" p2.log -[ -f "$TMP/ws/out829.tar.gz" ] || fail "P2: a relative -o did not land in the directory it was typed in" p2.log - -# P3 -if "$MCPP" pack -p nosuch > p3.log 2>&1; then fail "P3: an unknown member was accepted" p3.log; fi -grep -qi "nosuch" p3.log || fail "P3: the refusal does not name the value" p3.log - -echo "OK" diff --git a/tests/e2e/841_pack_summarizes_many_outputs.sh b/tests/e2e/841_pack_summarizes_many_outputs.sh deleted file mode 100644 index 0f1ebb380..000000000 --- a/tests/e2e/841_pack_summarizes_many_outputs.sh +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env bash -# requires: python3 -# 841_pack_summarizes_many_outputs.sh -- the human report of `mcpp pack` for a -# format that submits one output per file of a distribution tree. -# -# A format may declare one output per file (the validation project's release -# format declares 1,309 for one program), and a `Packed` line per file then -# buries the rest of the pass. -# -# P1 more than eight outputs are reported by the entry each lies in below -# their common parent, with a count; -# P2 --verbose reports every output; -# P3 `--message-format json` lists every output. -set -e - -TMP=$(mktemp -d) -trap "rm -rf $TMP" EXIT -cd "$TMP" -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } -MCPP="${MCPP:-mcpp}" - -mkdir -p app/src && cd app -cat > mcpp.toml <<'EOF' -[package] -name = "app" -version = "1.0.0" - -[targets.app] -kind = "bin" -main = "src/main.cpp" -EOF -printf 'int main() { return 0; }\n' > src/main.cpp -cat > build.mcpp <<'EOF' -import mcpp; -#include -#include -int main() { - mcpp::provides_pack_format("tree"); - if (std::string_view(mcpp::pack_format()) != "tree") return 0; - const std::string out = std::string(mcpp::out_dir()) + "/tree"; - auto copy = [&](const std::string& id, const std::string& dst) { - mcpp::action a; - a.id = id.c_str(); - a.role = "artifact"; - a.arg("${mcpp.self}").arg("stage").arg("--verify").arg("content") - .arg("--output").arg(dst.c_str()).arg("${mcpp.target_file:app}") - .input("${mcpp.target_file:app}") - .output(dst.c_str()) - .submit(); - }; - for (int i = 0; i < 6; ++i) { - copy("a" + std::to_string(i), out + "/bin/f" + std::to_string(i)); - copy("b" + std::to_string(i), out + "/data/f" + std::to_string(i)); - } - copy("m", out + "/manifest"); - return 0; -} -EOF - -"$MCPP" pack --pkg-format tree > p1.log 2>&1 || fail "pack failed" p1.log - -# P1 -n=$(grep -c "Packed" p1.log) -[ "$n" = 3 ] || fail "P1: $n Packed lines, expected 3" p1.log -grep -qE "Packed .*tree[/\\\\]bin \(6 files\)" p1.log || fail "P1: bin/ is not summarised" p1.log -grep -qE "Packed .*tree[/\\\\]data \(6 files\)" p1.log || fail "P1: data/ is not summarised" p1.log -grep -qE "Packed .*tree[/\\\\]manifest$" p1.log || fail "P1: the single file is not named" p1.log - -# P2 -"$MCPP" pack --pkg-format tree --verbose > p2.log 2>&1 || fail "pack --verbose failed" p2.log -n=$(grep -c "Packed" p2.log) -[ "$n" = 13 ] || fail "P2: $n Packed lines under --verbose, expected 13" p2.log - -# P3 -"$MCPP" pack --pkg-format tree --message-format json > p3.json 2> p3.err || fail "pack json failed" p3.err -python3 - p3.json <<'EOF' || fail "P3" p3.json -import json, sys -a = json.load(open(sys.argv[1]))["data"]["artifacts"] -assert len(a) == 13, f"P3: {len(a)} artifacts" -EOF - -echo "PASS: 841_pack_summarizes_many_outputs" diff --git a/tests/e2e/867_a_pack_over_several_members_plans_builds_and_runs_programs_once.sh b/tests/e2e/867_a_pack_over_several_members_plans_builds_and_runs_programs_once.sh deleted file mode 100644 index d5cc1b6a2..000000000 --- a/tests/e2e/867_a_pack_over_several_members_plans_builds_and_runs_programs_once.sh +++ /dev/null @@ -1,205 +0,0 @@ -#!/usr/bin/env bash -# requires: unix-shell -# 867 -- `mcpp pack` over several members plans once, builds once, runs each build -# program once, and gives each member's program its own staged tree. -# -# mcpp#749 (K1, member selection design 2026-09-30). A project that ships several -# programs from one workspace ran `mcpp pack -p ` once per program. Each -# invocation planned the graph, ran the build programs again, including those of the -# members the programs share, and started the build again. `--workspace`, and a -# repeated `-p`, plan the members as one selection and stage each in a tree of its -# own. -# -# The fixture. `cli` and `gui` are programs, each providing the format `zap` -# through its own build program, and both use `core`, a library with a build -# program of its own that provides nothing. Every program appends one line to -# `runs.log` under its `OUT_DIR` when it runs, stating the request it answered -# and the staged tree it was told. -# -# Criteria (#749, A to D): -# A. `mcpp pack --workspace --pkg-format zap` runs `core`'s program once in all, and -# each provider's program once for each of the two passes a dispatched -# format prepares: once when no format is asked, and once when it is. -# B. Each member's packed tree has the same files with the same content as -# `mcpp pack -p ` produces for it, and so does its distributable. -# C. After `mcpp build --workspace` with the same profile, the pack compiles -# nothing: no object is added to `.ninja_log`. -# D. Each member's program reads its own `pack_stage_dir()`, and the tree it -# names is the member's. -set -e -source "$(dirname "$0")/_host_path.sh" - -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } -MCPP="${MCPP:-mcpp}" - -export MCPP_HOME="$TMP/mcpp-home" -source "$(dirname "$0")/_inherit_toolchain.sh" - -mkdir -p "$TMP/ws" -cd "$TMP/ws" -cat > mcpp.toml <<'EOF' -[workspace] -members = ["core", "cli", "gui"] -EOF - -mkdir -p core/src -cat > core/mcpp.toml <<'EOF' -[package] -name = "core" -version = "0.1.0" - -[targets.core] -kind = "lib" -EOF -cat > core/src/core.cppm <<'EOF' -export module zcore; -export int core_answer() { return 42; } -EOF -cat > core/build.mcpp <<'EOF' -import mcpp; -#include -#include -int main() { - std::ofstream log(std::string(mcpp::out_dir()) + "/runs.log", std::ios::app); - log << "format=" << mcpp::pack_format() << " stage=" << mcpp::pack_stage_dir() << "\n"; - return 0; -} -EOF - -# The distribution step: what the staged tree held, and whose it was. -cat > dist.sh <<'EOF' -#!/usr/bin/env bash -set -e -stage="$1"; out="$2"; member="$3" -{ echo "member=$member"; ( cd "$stage" && find . -type f | sort ); } > "$out" -EOF -chmod +x dist.sh - -for m in cli gui; do - mkdir -p $m/src - cat > $m/mcpp.toml < $m/src/main.cpp < -import zcore; -int main() { std::printf("$m %d\n", core_answer()); return 0; } -EOF - cat > $m/build.mcpp <<'EOF' -import mcpp; -#include -#include -#include -int main() { - const std::string out = mcpp::out_dir(); - // One line per run: the request it answered and the tree it was told. - { - std::ofstream log(out + "/runs.log", std::ios::app); - log << "format=" << mcpp::pack_format() << " stage=" << mcpp::pack_stage_dir() << "\n"; - } - // Half one, unconditional; half two, only when the format is asked for. - mcpp::provides_pack_format("zap"); - if (std::string_view(mcpp::pack_format()) != "zap") return 0; - { - std::ofstream seen(out + "/seen-stage.txt", std::ios::trunc); - seen << mcpp::pack_stage_dir() << "\n"; - } - const std::string root = std::string(mcpp::manifest_dir()) + "/.."; - const std::string dist = out + "/" + mcpp::package_name() + ".zap"; - mcpp::action a; - a.id = "zap"; - a.role = "artifact"; - a.arg((root + "/dist.sh").c_str()) - .arg("${mcpp.stage_dir}") - .arg(dist.c_str()) - .arg(mcpp::package_name()) - .input((std::string("${mcpp.target_file:") + mcpp::package_name() + "}").c_str()) - .output(dist.c_str()) - .submit(); - return 0; -} -EOF -done - -# A member's build-program output directory: one per configuration and -# package, below the workspace root (mcpp 2026.10.10.1+). Every command here -# builds the one configuration the pack asks for. -outdir() { ls -d target/.build-mcpp/out/*/"$1" 2>/dev/null | head -1; } -# The lines a member's program appended to its log, all of them. -runs() { cat "$(outdir "$1")/runs.log" 2>/dev/null; } -count() { runs "$1" | grep -c "$2" || true; } - -stage_of() { ls -d "$1"/target/dist/"$1"-0.1.0-* | grep -v -e '\.stage-manifest$' -e '\.tar\.gz$' | head -1; } -objects() { awk -F'\t' '$4 ~ /\.o$/' "$1/.ninja_log" | wc -l | tr -d ' '; } - -# ── A ──────────────────────────────────────────────────────────────────────── -"$MCPP" pack --workspace --pkg-format zap > a.log 2>&1 || fail "A: the pack failed" a.log -# `core` is not a program, so it is not packed, and its program ran once in all: -# in the pass that asks for no format, and never again for a member whose answer -# does not depend on the request. -[ "$(count core '')" = 1 ] || fail "A: core's program ran $(count core '') times, not once" $(outdir core)/runs.log -for m in cli gui; do - [ "$(count $m '')" = 2 ] || fail "A: $m's program ran $(count $m '') times, not once for each pass" $(outdir $m)/runs.log - [ "$(count $m '^format= stage=$')" = 1 ] || fail "A: $m's first pass was told a request" $(outdir $m)/runs.log - [ "$(count $m '^format=zap stage=/')" = 1 ] || fail "A: $m's second pass was not told its format and stage" $(outdir $m)/runs.log -done -grep -q "Packed .*cli.zap" a.log || fail "A: cli's distributable was not reported" a.log -grep -q "Packed .*gui.zap" a.log || fail "A: gui's distributable was not reported" a.log -echo "ok: A, one plan, one run of the shared member's program, one run of each provider's per pass" - -# ── D ──────────────────────────────────────────────────────────────────────── -for m in cli gui; do - # The two spellings of one directory are compared as directories: a - # temporary directory may be reached through a symbolic link. - want="$(cd "$(stage_of $m)" && pwd -P)" - saw="$(cat $(outdir $m)/seen-stage.txt)" - [ -d "$saw" ] && [ "$(cd "$saw" && pwd -P)" = "$want" ] \ - || fail "D: $m's program read '$saw', and its tree is '$want'" a.log - grep -qx "member=$m" $(outdir $m)/$m.zap || fail "D: $m's distributable was made for another member" $(outdir $m)/$m.zap - grep -qx "./bin/$m" $(outdir $m)/$m.zap || fail "D: $m's tree did not hold $m's program" $(outdir $m)/$m.zap -done -[ "$(cat $(outdir cli)/seen-stage.txt)" != "$(cat $(outdir gui)/seen-stage.txt)" ] \ - || fail "D: two members were told one staged tree" -grep -q "gui" $(outdir cli)/cli.zap && fail "D: cli's tree holds gui's program" $(outdir cli)/cli.zap -echo "ok: D, each member's program reads its own staged tree" - -# ── B ──────────────────────────────────────────────────────────────────────── -mkdir multi single -for m in cli gui; do - mkdir multi/$m single/$m - cp -R "$(stage_of $m)" multi/$m/tree - cp "$(stage_of $m).stage-manifest" multi/$m/stage-manifest - cp $(outdir $m)/$m.zap multi/$m/dist - "$MCPP" pack -p $m --pkg-format zap > b-$m.log 2>&1 || fail "B: mcpp pack -p $m failed" b-$m.log - cp -R "$(stage_of $m)" single/$m/tree - cp "$(stage_of $m).stage-manifest" single/$m/stage-manifest - cp $(outdir $m)/$m.zap single/$m/dist - diff -r multi/$m single/$m > b-$m.diff || fail "B: $m's packed tree differs from the one mcpp pack -p $m makes" b-$m.diff -done -echo "ok: B, each member's tree and distributable are those of mcpp pack -p " - -# ── C ──────────────────────────────────────────────────────────────────────── -rm -rf target core/target cli/target gui/target -"$MCPP" build --workspace --release > c-build.log 2>&1 || fail "C: the workspace did not build" c-build.log -dirs=$(find target -name build.ninja -not -path '*/.build-mcpp/*' -exec dirname {} \;) -[ "$(echo "$dirs" | wc -l | tr -d ' ')" = 1 ] || fail "C: expected one build directory, got: $dirs" c-build.log -before=$(objects "$dirs") -[ "$before" -gt 0 ] || fail "C: the build compiled nothing" "$dirs/.ninja_log" -"$MCPP" pack --workspace --pkg-format zap > c-pack.log 2>&1 || fail "C: the pack failed" c-pack.log -[ "$(find target -name build.ninja -not -path '*/.build-mcpp/*' | wc -l | tr -d ' ')" = 1 ] || fail "C: the pack used another build directory" c-pack.log -after=$(objects "$dirs") -[ "$after" = "$before" ] || fail "C: the pack compiled $((after - before)) object(s) after the build" "$dirs/.ninja_log" c-pack.log -echo "ok: C, after a build of the workspace the pack compiles nothing" - -echo "PASS: 867_a_pack_over_several_members_plans_builds_and_runs_programs_once" diff --git a/tests/e2e/868_a_pack_over_several_members_is_refused_before_anything_is_compiled.sh b/tests/e2e/868_a_pack_over_several_members_is_refused_before_anything_is_compiled.sh deleted file mode 100644 index d94b404a0..000000000 --- a/tests/e2e/868_a_pack_over_several_members_is_refused_before_anything_is_compiled.sh +++ /dev/null @@ -1,324 +0,0 @@ -#!/usr/bin/env bash -# requires: unix-shell -# 868 -- what `mcpp pack` over several members refuses, and that it refuses before -# anything is compiled, naming what it refused. -# -# mcpp#749 (K1, member selection design 2026-09-30). A pack over several members -# pays for one build, so an input it cannot serve is worth refusing before that -# build rather than after it. Each refusal below is asserted to arrive with no -# object compiled anywhere in the workspace. -# -# Criteria: -# E. A selected member without the requested `--format` is refused by name. -# `plain` provides nothing; `cli` provides `zap`. -# F. A target name is refused with several members: it names a target of one -# package. (`--target` given twice is two legs of every member since -# 2026.10.30.1, SPEC-014 §1.4, and is no longer refused.) -# G. `--output` with several members must be a directory, and a path that exists -# as a file is refused. -# H. A member with no program target is refused by name when `-p` names it, and -# skipped by `--workspace`. -# I. Two members that would write one archive are refused, naming both. -# J. A package that several packed members reach cannot provide a format for -# them: a provider is given one staged tree. The members it serves are -# refused by name. `mcpp pack -p ` over the same provider works. -# K. A member that declared the format and submitted nothing fails alone: the -# others are packed, the failure names the member, and the status is not zero. -# So does a configuration that cannot be planned, for its members. -set -e -source "$(dirname "$0")/_host_path.sh" - -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } -MCPP="${MCPP:-mcpp}" - -export MCPP_HOME="$TMP/mcpp-home" -source "$(dirname "$0")/_inherit_toolchain.sh" - -# No object of the members was compiled in $1, and no build of them was driven. -# The build programs are compiled while planning (they declare the providers), -# in the build programs' graph under target/.build-mcpp/host (mcpp#797). -compiled_nothing() { - [ -z "$(find "$1" -name .ninja_log -not -path '*/.build-mcpp/host/*' 2>/dev/null)" ] -} - -# A program member named $1, optionally using the packages in $2. -program_member() { - local m="$1" deps="$2" - mkdir -p "$m/src" - { - printf '[package]\nname = "%s"\nversion = "0.1.0"\n' "$m" - [ -z "$deps" ] || printf '\n[dependencies]\n%s\n' "$deps" - printf '\n[targets.%s]\nkind = "bin"\nmain = "src/main.cpp"\n' "$m" - } > "$m/mcpp.toml" - printf '#include \nint main() { std::puts("%s"); return 0; }\n' "$m" > "$m/src/main.cpp" -} - -# The distribution step: what the staged tree held. -write_dist() { - cat > "$1/dist.sh" <<'EOF' -#!/usr/bin/env bash -set -e -stage="$1"; out="$2" -{ ( cd "$stage" && find . -type f | sort ); } > "$out" -EOF - chmod +x "$1/dist.sh" -} - -# A build program that provides `zap` and submits it, or only declares it. -provider() { # provider submit|silent - local gate="if (std::string_view(mcpp::pack_format()) != \"zap\") return 0;" - if [ "$2" = silent ]; then gate="return 0;"; fi - cat > "$1/build.mcpp" < -#include -int main() { - mcpp::provides_pack_format("zap"); - $gate - const std::string root = std::string(mcpp::manifest_dir()) + "/.."; - const std::string dist = std::string(mcpp::out_dir()) + "/" + mcpp::package_name() + ".zap"; - mcpp::action a; - a.id = "zap"; - a.role = "artifact"; - a.arg((root + "/dist.sh").c_str()).arg("\${mcpp.stage_dir}").arg(dist.c_str()) - .input((std::string("\${mcpp.target_file:") + mcpp::package_name() + "}").c_str()) - .output(dist.c_str()) - .submit(); - return 0; -} -EOF -} - -mkdir -p "$TMP/ws" -cd "$TMP/ws" -cat > mcpp.toml <<'EOF' -[workspace] -members = ["cli", "plain", "lib"] -EOF -write_dist . -program_member cli "" -provider cli submit -program_member plain "" -mkdir -p lib/src -cat > lib/mcpp.toml <<'EOF' -[package] -name = "lib" -version = "0.1.0" - -[targets.lib] -kind = "lib" -EOF -printf 'export module zlib;\nexport int f() { return 1; }\n' > lib/src/lib.cppm - -# ── E ──────────────────────────────────────────────────────────────────────── -set +e -"$MCPP" pack --mode system --workspace --pkg-format zap > e.log 2>&1 -rc=$? -set -e -[ "$rc" -ne 0 ] || fail "E: a member without the format was packed" e.log -grep -q "plain" e.log || fail "E: the refusal does not name the member" e.log -grep -q "zap" e.log || fail "E: the refusal does not name the format" e.log -grep -q "member 'cli'" e.log && fail "E: the refusal names a member that provides the format" e.log -compiled_nothing . || fail "E: the refusal arrived after a compile" e.log -echo "ok: E, a member without the format is refused by name, before the build" - -# ── F ──────────────────────────────────────────────────────────────────────── -set +e -"$MCPP" pack --mode system -p cli -p plain cli > f1.log 2>&1 -rc1=$? -set -e -[ "$rc1" -ne 0 ] && grep -q "target name" f1.log && grep -q "'cli'" f1.log \ - || fail "F: a target name with several members was not refused, naming it" f1.log -compiled_nothing . || fail "F: a refusal arrived after a compile" f1.log -echo "ok: F, a target name is refused with several members" - -# ── G ──────────────────────────────────────────────────────────────────────── -: > a-file -set +e -"$MCPP" pack --mode system -p cli -p plain -o a-file > g.log 2>&1 -rc=$? -set -e -[ "$rc" -ne 0 ] && grep -q -- "--output names a file" g.log \ - || fail "G: --output naming a file was not refused with several members" g.log -compiled_nothing . || fail "G: the refusal arrived after a compile" g.log -echo "ok: G, --output must be a directory with several members" - -# ── H ──────────────────────────────────────────────────────────────────────── -set +e -"$MCPP" pack --mode system -p cli -p lib > h.log 2>&1 -rc=$? -set -e -[ "$rc" -ne 0 ] && grep -q "member 'lib' has no program target" h.log \ - || fail "H: a member with no program was not refused by name" h.log -compiled_nothing . || fail "H: the refusal arrived after a compile" h.log -"$MCPP" pack --mode system --workspace > h2.log 2>&1 || fail "H: --workspace failed over a member with no program" h2.log -grep -q "Skipping lib" h2.log || fail "H: --workspace did not say it skipped lib" h2.log -[ -n "$(find cli plain -name 'cli-0.1.0-*.tar.gz' -o -name 'plain-0.1.0-*.tar.gz' | head -1)" ] \ - || fail "H: the programs were not packed" h2.log -[ -z "$(find lib -name '*.tar.gz' | head -1)" ] || fail "H: the library was packed as a program" h2.log -echo "ok: H, a member with no program is refused by -p and skipped by --workspace" - -# ── I ──────────────────────────────────────────────────────────────────────── -mkdir -p "$TMP/ns" && cd "$TMP/ns" -cat > mcpp.toml <<'EOF' -[workspace] -members = ["a/tool", "b/tool"] -EOF -for ns in a b; do - mkdir -p $ns/tool/src - cat > $ns/tool/mcpp.toml < $ns/tool/src/main.cpp -done -set +e -"$MCPP" pack --mode system --workspace -o out > i.log 2>&1 -rc=$? -set -e -[ "$rc" -ne 0 ] && grep -q "would both write" i.log && grep -q "a.tool" i.log && grep -q "b.tool" i.log \ - || fail "I: two members writing one archive were not refused, naming both" i.log -compiled_nothing . || fail "I: the refusal arrived after a compile" i.log -# Without --output each is written below its own target/dist, and both are packed. -"$MCPP" pack --mode system --workspace > i2.log 2>&1 || fail "I: two members of one name were not packed below their own directories" i2.log -[ -n "$(find a/tool/target/dist -name 'tool-0.1.0-*.tar.gz' | head -1)" ] \ - && [ -n "$(find b/tool/target/dist -name 'tool-0.1.0-*.tar.gz' | head -1)" ] \ - || fail "I: a member of the two was not packed" i2.log -echo "ok: I, two members that write one archive are refused naming both" - -# ── J ──────────────────────────────────────────────────────────────────────── -# `prov` provides the format, and both `cli2` and `gui2` reach it: a provider is -# given one staged tree, and two members would need two. -mkdir -p "$TMP/shared" && cd "$TMP/shared" -PROV_HOST=$(host_path "$TMP/shared/prov") -cat > mcpp.toml <<'EOF' -[workspace] -members = ["cli2", "gui2"] -EOF -write_dist . -mkdir -p prov/src -cat > prov/mcpp.toml <<'EOF' -[package] -name = "prov" -version = "0.1.0" - -[targets.prov] -kind = "lib" -EOF -printf 'export module zprov;\nexport int p() { return 1; }\n' > prov/src/prov.cppm -cat > prov/build.mcpp <<'EOF' -import mcpp; -#include -#include -int main() { - mcpp::provides_pack_format("zap"); - if (std::string_view(mcpp::pack_format()) != "zap") return 0; - const std::string dist = std::string(mcpp::out_dir()) + "/prov.zap"; - mcpp::action a; - a.id = "zap"; - a.role = "artifact"; - a.arg((std::string(mcpp::manifest_dir()) + "/../dist.sh").c_str()).arg("${mcpp.stage_dir}").arg(dist.c_str()) - .output(dist.c_str()) - .submit(); - return 0; -} -EOF -for m in cli2 gui2; do - program_member $m "prov = { path = \"$PROV_HOST\" }" -done -set +e -"$MCPP" pack --mode system --workspace --pkg-format zap > j.log 2>&1 -rc=$? -set -e -[ "$rc" -ne 0 ] && grep -q "member 'cli2'" j.log && grep -q "member 'gui2'" j.log && grep -q "prov" j.log \ - || fail "J: members served only by a shared provider were not refused by name" j.log -compiled_nothing . || fail "J: the refusal arrived after a compile" j.log -"$MCPP" pack --mode system -p cli2 --pkg-format zap > j2.log 2>&1 || fail "J: one member over the same provider was not packed" j2.log -provided=$(find . -name prov.zap | head -1) -[ -n "$provided" ] && grep -qx "./bin/cli2" "$provided" \ - || fail "J: the provider was not given the member's tree" j2.log -echo "ok: J, a provider several packed members reach is refused for them, and serves one" - -# A provider that only one packed member reaches acts for that member, beside a -# member that provides the format from its own build program. -mkdir -p "$TMP/mixed" && cd "$TMP/mixed" -cat > mcpp.toml <<'EOF' -[workspace] -members = ["cli3", "gui3"] -EOF -write_dist . -program_member cli3 "prov = { path = \"$PROV_HOST\" }" -program_member gui3 "" -provider gui3 submit -"$MCPP" pack --mode system --workspace --pkg-format zap > j3.log 2>&1 || fail "J: a provider one member reaches did not serve it beside another member's own" j3.log -provided=$(find . -name prov.zap | head -1) -[ -n "$provided" ] && grep -qx "./bin/cli3" "$provided" && ! grep -q "gui3" "$provided" \ - || fail "J: the provider was not given cli3's tree alone" j3.log -own=$(find . -name gui3.zap | head -1) -[ -n "$own" ] && grep -qx "./bin/gui3" "$own" || fail "J: gui3's own provider was not given gui3's tree" j3.log -grep -q "Packed .*prov.zap" j3.log && grep -q "Packed .*gui3.zap" j3.log \ - || fail "J: a distributable was not reported for each member" j3.log -echo "ok: J, a provider one packed member reaches acts for that member" - -# ── K ──────────────────────────────────────────────────────────────────────── -mkdir -p "$TMP/partial" && cd "$TMP/partial" -cat > mcpp.toml <<'EOF' -[workspace] -members = ["good", "mute"] -EOF -write_dist . -program_member good "" -provider good submit -program_member mute "" -provider mute silent -set +e -"$MCPP" pack --mode system --workspace --pkg-format zap > k.log 2>&1 -rc=$? -set -e -[ "$rc" -ne 0 ] || fail "K: a member that claimed nothing was reported as packed" k.log -grep -q "member 'mute': no action claimed --pkg-format 'zap'" k.log \ - || fail "K: the failure does not name the member" k.log -[ -n "$(find target/.build-mcpp/out -path '*/good/good.zap' | head -1)" ] || fail "K: the member that claimed the format was not packed" k.log -grep -q "Packed .*good.zap" k.log || fail "K: the member that was packed was not reported" k.log -echo "ok: K, a member that claimed nothing fails alone, by name, and the others are packed" - -# A configuration that cannot be planned fails for its members alone, too: `broken` -# asks for another standard, so it is planned in a group of its own, and its build -# program does not compile. -mkdir -p "$TMP/planfail" && cd "$TMP/planfail" -cat > mcpp.toml <<'EOF' -[workspace] -members = ["fine", "broken"] -EOF -program_member fine "" -mkdir -p broken/src -cat > broken/mcpp.toml <<'EOF' -[package] -name = "broken" -version = "0.1.0" -standard = "c++26" - -[targets.broken] -kind = "bin" -main = "src/main.cpp" -EOF -printf 'int main() { return 0; }\n' > broken/src/main.cpp -printf 'this is not C++\n' > broken/build.mcpp -set +e -"$MCPP" pack --mode system --workspace > k2.log 2>&1 -rc=$? -set -e -[ "$rc" -ne 0 ] || fail "K: a configuration that could not be planned was reported as packed" k2.log -grep -q "broken" k2.log || fail "K: the failure does not name the member" k2.log -[ -n "$(find fine -name 'fine-0.1.0-*.tar.gz' | head -1)" ] || fail "K: the member of the other configuration was not packed" k2.log -echo "ok: K, a configuration that cannot be planned fails for its members alone" - -echo "PASS: 868_a_pack_over_several_members_is_refused_before_anything_is_compiled" diff --git a/tests/e2e/869_a_pack_over_several_members_names_each_member_in_its_report.sh b/tests/e2e/869_a_pack_over_several_members_names_each_member_in_its_report.sh deleted file mode 100644 index 41e8fe0ff..000000000 --- a/tests/e2e/869_a_pack_over_several_members_names_each_member_in_its_report.sh +++ /dev/null @@ -1,183 +0,0 @@ -#!/usr/bin/env bash -# requires: unix-shell python3 -# 869 -- what `mcpp pack` over several members writes and reports for the built-in -# formats: each member's archive or tree where it would be written alone, below a -# directory `--output` names, and each member named in the report. -# -# mcpp#749 (K1, member selection design 2026-09-30). -# -# Criteria: -# L. `--workspace` and a repeated `-p` pack the same members, in `[workspace] -# members` order, each into its own `target/dist` under the name it has -# when packed alone; an archive is the one `mcpp pack -p ` makes. -# Without a selector a virtual workspace root packs its first member with a -# program, as it always did. -# M. `--output ` writes each member's archive, or with `--format dir` -# its tree, below the directory, under its default name. -# N. `--exclude` removes a member from `--workspace`. -# P. Members of two configurations (one is `standard = "c++26"`) are planned in -# two groups, each built once, and every member is packed from its own. -# O. `--message-format json` keeps the envelope: `data.artifacts` lists every -# member's artifacts, each naming its member, `data.stage` stays null for -# several members, and `data.stages` names one tree per member. For one -# member the envelope is the one it always was: no `member`, no `stages`. -set -e -source "$(dirname "$0")/_host_path.sh" - -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } -MCPP="${MCPP:-mcpp}" - -export MCPP_HOME="$TMP/mcpp-home" -source "$(dirname "$0")/_inherit_toolchain.sh" - -mkdir -p "$TMP/ws" -cd "$TMP/ws" -cat > mcpp.toml <<'EOF' -[workspace] -members = ["core", "gui", "cli"] -EOF -mkdir -p core/src -cat > core/mcpp.toml <<'EOF' -[package] -name = "core" -version = "0.1.0" - -[targets.core] -kind = "lib" -EOF -printf 'export module zcore;\nexport int core_answer() { return 42; }\n' > core/src/core.cppm -for m in cli gui; do - mkdir -p $m/src - cat > $m/mcpp.toml <\nimport zcore;\nint main() { std::printf("%s %%d\\n", core_answer()); return 0; }\n' "$m" > $m/src/main.cpp -done - -archive_of() { find "$1/target/dist" -maxdepth 1 -name "$1-0.1.0-*.tar.gz" 2>/dev/null | head -1; } -members_in() { python3 -c 'import json,sys; d=json.load(sys.stdin)["data"]; print(" ".join(a.get("member","-") for a in d["artifacts"]))'; } - -# ── L ──────────────────────────────────────────────────────────────────────── -"$MCPP" pack --mode system --workspace > l1.log 2>&1 || fail "L: --workspace did not pack" l1.log -for m in cli gui; do - [ -n "$(archive_of $m)" ] || fail "L: $m was not packed below its own target/dist" l1.log -done -[ -z "$(find core -name '*.tar.gz' | head -1)" ] || fail "L: the library was packed as a program" l1.log -# Order: [workspace] members lists gui before cli, and the report follows it. -gui_at=$(grep -n "Packing gui" l1.log | head -1 | cut -d: -f1) -cli_at=$(grep -n "Packing cli" l1.log | head -1 | cut -d: -f1) -[ -n "$gui_at" ] && [ -n "$cli_at" ] && [ "$gui_at" -lt "$cli_at" ] \ - || fail "L: the members are not reported in [workspace] members order" l1.log -mkdir together -for m in cli gui; do cp "$(archive_of $m)" together/$m.tar.gz; done -rm -rf cli/target/dist gui/target/dist -# A repeated -p selects the same members, whatever order it is written in. -"$MCPP" pack --mode system -p cli -p gui > l2.log 2>&1 || fail "L: -p cli -p gui did not pack" l2.log -for m in cli gui; do - [ -n "$(archive_of $m)" ] || fail "L: -p cli -p gui did not pack $m" l2.log -done -# The archive is the one a member packed alone makes. -mkdir alone -for m in cli gui; do - rm -rf $m/target/dist - "$MCPP" pack --mode system -p $m > l3-$m.log 2>&1 || fail "L: mcpp pack -p $m failed" l3-$m.log - cp "$(archive_of $m)" alone/$m.tar.gz - mkdir -p together/x-$m alone/x-$m - tar -xzf together/$m.tar.gz -C together/x-$m - tar -xzf alone/$m.tar.gz -C alone/x-$m - diff -r together/x-$m alone/x-$m > l4-$m.diff || fail "L: $m's archive from the pack of several differs from the one packed alone" l4-$m.diff -done -# Without a selector a virtual workspace root packs the first member that has a -# program, as it always did; it is --workspace that packs them all. -rm -rf cli/target/dist gui/target/dist -"$MCPP" pack --mode system > l5.log 2>&1 || fail "L: a bare mcpp pack at the workspace root failed" l5.log -[ -n "$(archive_of gui)" ] && [ -z "$(archive_of cli)" ] \ - || fail "L: a bare mcpp pack at the workspace root did not pack the first member with a program alone" l5.log -echo "ok: L, --workspace and a repeated -p pack each member as it is packed alone, in member order" - -# ── M ──────────────────────────────────────────────────────────────────────── -"$MCPP" pack --mode system --workspace -o out > m1.log 2>&1 || fail "M: --output did not pack" m1.log -for m in cli gui; do - [ -n "$(find out -maxdepth 1 -name "$m-0.1.0-*.tar.gz" | head -1)" ] \ - || fail "M: $m's archive is not below the directory --output names" m1.log -done -[ "$(find out -maxdepth 1 -name '*.tar.gz' | wc -l | tr -d ' ')" = 2 ] || fail "M: the directory holds more than the two archives" m1.log -"$MCPP" pack --mode system --workspace --pkg-format dir -o out-trees > m2.log 2>&1 || fail "M: --pkg-format dir with --output did not pack" m2.log -for m in cli gui; do - t=$(find out-trees -maxdepth 1 -type d -name "$m-0.1.0-*" | head -1) - [ -n "$t" ] && [ -f "$t/bin/$m" ] || fail "M: $m's tree is not below the directory --output names" m2.log -done -echo "ok: M, --output holds each member's archive or tree under its default name" - -# ── N ──────────────────────────────────────────────────────────────────────── -rm -rf cli/target/dist gui/target/dist -"$MCPP" pack --mode system --workspace --exclude gui > n.log 2>&1 || fail "N: --exclude did not pack" n.log -[ -n "$(archive_of cli)" ] || fail "N: cli was not packed" n.log -[ -z "$(archive_of gui)" ] || fail "N: the member --exclude names was packed" n.log -echo "ok: N, --exclude removes a member from --workspace" - -# ── O ──────────────────────────────────────────────────────────────────────── -"$MCPP" pack --mode system --workspace --message-format json > o1.json 2> o1.log || fail "O: --message-format json failed" o1.log -python3 -c 'import json; json.load(open("o1.json"))' || fail "O: standard output is not one JSON document" o1.json -[ "$(python3 -c 'import json; d=json.load(open("o1.json")); print(d["kind"])')" = "mcpp.pack" ] || fail "O: the envelope kind changed" o1.json -[ "$(python3 -c 'import json; d=json.load(open("o1.json"))["data"]; print(d["stage"] is None)')" = "True" ] \ - || fail "O: data.stage is not null for several members" o1.json -got=$(members_in < o1.json) -[ "$got" = "gui cli" ] || fail "O: data.artifacts names '$got', not gui and cli in member order" o1.json -[ "$(python3 -c 'import json; d=json.load(open("o1.json"))["data"]; print(" ".join(s["member"] for s in d["stages"]))')" = "gui cli" ] \ - || fail "O: data.stages does not name one tree per member" o1.json -python3 - <<'EOF' || fail "O: a stage record lacks a field" o1.json -import json -d = json.load(open("o1.json"))["data"] -for s in d["stages"]: - assert set(s) == {"member", "dir", "manifest", "closure"}, s - assert s["closure"] in ("walked", "not-walked"), s -for a in d["artifacts"]: - assert set(a) == {"path", "type", "format", "targets", "member", "sha256"}, a -EOF -"$MCPP" pack --mode system -p cli --message-format json > o2.json 2> o2.log || fail "O: a pack of one member failed" o2.log -python3 - <<'EOF' || fail "O: the envelope of one member changed" o2.json -import json -d = json.load(open("o2.json"))["data"] -assert "stages" not in d, d -assert isinstance(d["stage"], dict) and set(d["stage"]) == {"dir", "manifest", "closure"}, d -assert len(d["artifacts"]) == 1 and set(d["artifacts"][0]) == {"path", "type", "format", "targets", "sha256"}, d -EOF -echo "ok: O, the envelope lists every member's artifacts and keeps the single-member shape" - -# ── P ──────────────────────────────────────────────────────────────────────── -mkdir -p "$TMP/cfg" -cd "$TMP/cfg" -cat > mcpp.toml <<'EOF' -[workspace] -members = ["app23", "app26"] -EOF -for a in app23 app26; do - mkdir -p $a/src - { - printf '[package]\nname = "%s"\nversion = "0.1.0"\n' "$a" - if [ "$a" = app26 ]; then printf 'standard = "c++26"\n'; fi - printf '\n[targets.%s]\nkind = "bin"\nmain = "src/main.cpp"\n' "$a" - } > $a/mcpp.toml - printf '#include \nint main() { std::printf("%%ld\\n", (long)__cplusplus); return 0; }\n' > $a/src/main.cpp -done -"$MCPP" pack --mode system --workspace --message-format json > p.json 2> p.log || fail "P: members of two configurations were not packed" p.log -[ "$(find target -name build.ninja -not -path '*/.build-mcpp/*' | wc -l | tr -d ' ')" = 2 ] || fail "P: expected one build directory for each configuration" p.log -[ "$(members_in < p.json)" = "app23 app26" ] || fail "P: data.artifacts does not name the two members in order" p.json -for a in app23 app26; do - [ -n "$(find $a/target/dist -maxdepth 1 -name "$a-0.1.0-*.tar.gz" | head -1)" ] || fail "P: $a was not packed" p.log -done -echo "ok: P, members of two configurations are each packed from the build of their own" - -echo "PASS: 869_a_pack_over_several_members_names_each_member_in_its_report" diff --git a/tests/e2e/870_a_pack_over_several_members_resolves_and_fails_per_member.sh b/tests/e2e/870_a_pack_over_several_members_resolves_and_fails_per_member.sh deleted file mode 100644 index f75e98e0b..000000000 --- a/tests/e2e/870_a_pack_over_several_members_resolves_and_fails_per_member.sh +++ /dev/null @@ -1,131 +0,0 @@ -#!/usr/bin/env bash -# requires: unix-shell -# 870 -- a pack over several members resolves a target name for the member an -# action is for, lets one member fail alone in the distribution step, and reports -# the members in `[workspace] members` order across configurations. -# -# mcpp#749 (K1, member selection design 2026-09-30, section 15.3). A pack of -# several members is one plan per configuration, so two members' targets of one -# name are in one plan, one ninja drive runs every member's distribution step, -# and the members are processed one configuration at a time. -# -# The fixture. `a`, `c` and `b`, in that order in `[workspace] members`. `a` and -# `b` each have a program target named `tool`, and `c`, which states another -# standard and is therefore a configuration of its own, has `ctool`. Each member's -# build program provides the format `zap` and submits an action that copies -# `${mcpp.target_file:}` to `/.zap`, through -# `dist.sh`, which fails for a member whose `fail-` file exists. -# -# Criteria: -# A. Each member's distributable is its own program: `a.zap` prints `a` and -# `b.zap` prints `b`, although both name the target `tool`. -# B. The JSON envelope lists the members' stages in `[workspace] members` -# order, `a`, `c`, `b`, although `a` and `b` are one configuration and are -# packed before `c`. -# C. When `b`'s distribution step fails, `a` and `c` are packed with files this -# pack made, `b` is reported by name, and the command exits non-zero. -set -e -source "$(dirname "$0")/_host_path.sh" - -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } -MCPP="${MCPP:-mcpp}" - -export MCPP_HOME="$TMP/mcpp-home" -source "$(dirname "$0")/_inherit_toolchain.sh" - -mkdir -p "$TMP/ws" -cd "$TMP/ws" - -cat > mcpp.toml <<'EOF' -[workspace] -members = ["a", "c", "b"] -EOF - -cat > dist.sh <<'EOF' -#!/usr/bin/env bash -set -e -here="$(cd "$(dirname "$0")" && pwd)" -[ -e "$here/fail-$3" ] && { echo "dist.sh: $3 is asked to fail" >&2; exit 1; } -cp "$1" "$2" -EOF -chmod +x dist.sh - -member() { # member [standard] - local m=$1 t=$2 std=${3:-} - mkdir -p $m/src - { - echo '[package]' - echo "name = \"$m\"" - echo 'version = "0.1.0"' - [ -n "$std" ] && echo "standard = \"$std\"" - echo - echo "[targets.$t]" - echo 'kind = "bin"' - echo 'main = "src/main.cpp"' - } > $m/mcpp.toml - printf '#include \nint main() { std::puts("%s"); return 0; }\n' $m > $m/src/main.cpp - cat > $m/build.mcpp < -#include -int main() { - mcpp::provides_pack_format("zap"); - if (std::string_view(mcpp::pack_format()) != "zap") return 0; - const std::string out = mcpp::out_dir(); - const std::string root = std::string(mcpp::manifest_dir()) + "/.."; - const std::string dist = out + "/" + mcpp::package_name() + ".zap"; - mcpp::action a; - a.id = "zap"; - a.role = "artifact"; - a.arg((root + "/dist.sh").c_str()) - .arg("\${mcpp.target_file:$t}") - .arg(dist.c_str()) - .arg(mcpp::package_name()) - .input("\${mcpp.target_file:$t}") - .output(dist.c_str()) - .submit(); - return 0; -} -EOF -} -member a tool -member b tool -member c ctool c++20 - -# A member's distributable, in its build-program output directory: one per -# configuration and package, below the workspace root (mcpp 2026.10.10.1+). -zap() { ls -d target/.build-mcpp/out/*/"$1"/"$1".zap 2>/dev/null | head -1; } - -# ── A ──────────────────────────────────────────────────────────────────────── -"$MCPP" pack --workspace --pkg-format zap > a.log 2>&1 || fail "A: the pack failed" a.log -for m in a b c; do - [ -x "$(zap $m)" ] || fail "A: no distributable for $m" a.log - [ "$("$(zap $m)")" = "$m" ] || fail "A: $m's distributable runs '$("$(zap $m)")', not $m's program" a.log -done -echo "ok: A, each member's distributable is its own program, although a and b both name the target tool" - -# ── B ──────────────────────────────────────────────────────────────────────── -"$MCPP" pack --workspace --pkg-format zap --message-format json > b.json 2> b.err \ - || fail "B: the JSON pack failed" b.err b.json -order=$(python3 -c 'import json,sys -d = json.load(open(sys.argv[1])) -print(" ".join(s["member"] for s in d["data"]["stages"]))' b.json) || fail "B: the envelope has no stages" b.json -[ "$order" = "a c b" ] || fail "B: the stages are listed as '$order', not in [workspace] members order (a c b)" b.json -echo "ok: B, the stages are listed in [workspace] members order across configurations" - -# ── C ──────────────────────────────────────────────────────────────────────── -touch fail-b -rc=0 -"$MCPP" pack --workspace --pkg-format zap > c.log 2>&1 || rc=$? -[ "$rc" -ne 0 ] || fail "C: the pack exited 0 although b's step failed" c.log -for m in a c; do - [ -x "$(zap $m)" ] && [ "$("$(zap $m)")" = "$m" ] || fail "C: $m was not packed beside b's failure" c.log - grep -qE "Packed .*$m\.zap" c.log || fail "C: $m's distributable was not reported" c.log -done -[ ! -e "$(zap b)" ] || fail "C: b's distributable of the previous pack is still in place" c.log -grep -qE "member 'b'|'b'" c.log || fail "C: the failure does not name b" c.log -echo "ok: C, a member whose distribution step fails fails alone" - -echo "PASS: 870_a_pack_over_several_members_resolves_and_fails_per_member" diff --git a/tests/e2e/871_a_pack_states_its_build_and_every_drive_takes_the_job_count.sh b/tests/e2e/871_a_pack_states_its_build_and_every_drive_takes_the_job_count.sh deleted file mode 100644 index db043f2cc..000000000 --- a/tests/e2e/871_a_pack_states_its_build_and_every_drive_takes_the_job_count.sh +++ /dev/null @@ -1,114 +0,0 @@ -#!/usr/bin/env bash -# requires: pack python3 -# 871 -- `mcpp pack` states the build it performs as `mcpp build` states one, -# and every command that compiles runs at most `[build] jobs` compiles at once -# (pack drive and selection design 2026-10-01, A; mcpp#753). -# -# Each command assembled its own ninja options. `mcpp build` attached the -# report and passed the job count; `mcpp pack` did neither, so a release job -# whose pack recompiled mcpp showed `Planning` for six minutes and no -# `Compiling` line, and `mcpp test` and `mcpp pack` ran ninja's default number -# of jobs whatever `[build] jobs` said. The backend now takes both from the plan -# and the open report. -# -# Criteria: -# A. A cold `mcpp pack --pkg-format tar` writes a `Compiling` line for the -# package and `Finished` before `Packing`. -# B. Under `--message-format json`, standard output is one JSON document. -# C. With `[build] jobs = 1`, no two compile steps overlap in `.ninja_log` -# under `mcpp build`, `mcpp test` and `mcpp pack`. Four units of about a -# third of a second each overlap under ninja's default on any machine -# with two cores. -set -e - -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } -cd "$TMP" - -mkdir -p app/src app/tests -cat > app/mcpp.toml <<'EOF' -[package] -name = "app" -version = "0.1.0" - -[build] -jobs = 1 - -[targets.app] -kind = "bin" -main = "src/main.cpp" -EOF -# Four translation units whose compile takes a measurable time: a constant -# evaluation of a few hundred thousand steps each, in two nested loops so that -# no loop exceeds GCC's per-loop limit (262144 iterations) and the whole stays -# within clang's step limit. -for i in 1 2 3 4; do - cat > app/src/u$i.cpp < app/src/main.cpp <<'EOF' -#include -unsigned long long value1(); unsigned long long value2(); -unsigned long long value3(); unsigned long long value4(); -int main() { std::printf("%llu\n", value1() ^ value2() ^ value3() ^ value4()); } -EOF -cat > app/tests/t.cpp <<'EOF' -int main() { return 0; } -EOF -cd app - -# no_overlap