Expand test suite #13

Merged
jorijnvdgraaf merged 1 commit from claude/issue-12 into master 2026-05-27 21:56:18 +02:00
16 changed files with 794 additions and 0 deletions

View file

@ -98,6 +98,15 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
// Mirrors how downstream consumers link their own libraries into tests. // Mirrors how downstream consumers link their own libraries into tests.
if (cfg.target == "x86_64-pc-linux-gnu") { if (cfg.target == "x86_64-pc-linux-gnu") {
cfg.AddTest("HelloWorld").Dependencies({ crafterBuildLib.get() }); cfg.AddTest("HelloWorld").Dependencies({ crafterBuildLib.get() });
cfg.AddTest("StaticLib").Dependencies({ crafterBuildLib.get() });
cfg.AddTest("ModuleInterface").Dependencies({ crafterBuildLib.get() });
cfg.AddTest("DependencyLink").Dependencies({ crafterBuildLib.get() });
cfg.AddTest("ShaderCompile").Dependencies({ crafterBuildLib.get() });
cfg.AddTest("StandardArgs").Dependencies({ crafterBuildLib.get() });
cfg.AddTest("TestRunnerSpec").Dependencies({ crafterBuildLib.get() });
cfg.AddTest("VariantId").Dependencies({ crafterBuildLib.get() });
cfg.AddTest("WasiBrowserRuntime").Dependencies({ crafterBuildLib.get() });
cfg.AddTest("RunSingleTestExit").Dependencies({ crafterBuildLib.get() });
} }
return cfg; return cfg;

View file

@ -0,0 +1,7 @@
import std;
import Calc;
int main() {
std::print("{}", Add(3, 4));
return 0;
}

View file

@ -0,0 +1,4 @@
export module Calc;
import std;
export int Add(int a, int b) { return a + b; }

View file

@ -0,0 +1,90 @@
import std;
import Crafter.Build;
namespace fs = std::filesystem;
using namespace Crafter;
// Two-config build: an exe linked against a sibling static library via
// cfg.dependencies. Exercises cross-config module import resolution (the
// exe imports Calc, which is defined in the lib's interface set) and the
// dep-archive linking step.
int main() {
fs::path fixtureRoot = fs::current_path() / "tests" / "DependencyLink" / "fixture";
auto lib = std::make_unique<Configuration>();
lib->path = fixtureRoot / "mathlib";
lib->name = "calc";
lib->outputName = "calc";
lib->target = HostTarget();
lib->type = ConfigurationType::LibraryStatic;
{
std::array<fs::path, 1> ifaces = { "Calc" };
std::array<fs::path, 0> impls = {};
lib->GetInterfacesAndImplementations(ifaces, impls);
}
Configuration app;
app.path = fixtureRoot;
app.name = "calc-app";
app.outputName = "calc-app";
app.target = HostTarget();
app.type = ConfigurationType::Executable;
app.dependencies = { lib.get() };
{
std::array<fs::path, 0> ifaces = {};
std::array<fs::path, 1> impls = { "main" };
app.GetInterfacesAndImplementations(ifaces, impls);
}
if (app.implementations.size() != 1) {
std::println(std::cerr, "expected 1 implementation, got {}", app.implementations.size());
return 1;
}
// The import resolves to the dependency's interface, not a local one.
if (!app.implementations[0].moduleDependencies.empty()) {
std::println(std::cerr, "expected no local module deps, got {}",
app.implementations[0].moduleDependencies.size());
return 1;
}
if (app.implementations[0].externalModuleDependencies.size() != 1) {
std::println(std::cerr, "expected 1 external module dep, got {}",
app.implementations[0].externalModuleDependencies.size());
return 1;
}
if (app.implementations[0].externalModuleDependencies[0].first->name != "Calc") {
std::println(std::cerr, "expected external dep 'Calc', got '{}'",
app.implementations[0].externalModuleDependencies[0].first->name);
return 1;
}
std::unordered_map<fs::path, std::shared_future<BuildResult>> depResults;
std::mutex depMutex;
BuildResult r = Build(app, depResults, depMutex);
if (!r.result.empty()) {
std::println(std::cerr, "build failed: {}", r.result);
return 1;
}
fs::path libArchive = lib->BinDir() / "libcalc.a";
if (!fs::exists(libArchive)) {
std::println(std::cerr, "dep archive not produced at {}", libArchive.string());
return 1;
}
fs::path bin = app.BinDir() / "calc-app";
if (!fs::exists(bin)) {
std::println(std::cerr, "binary not produced at {}", bin.string());
return 1;
}
auto run = RunCommandWithTimeout(bin.string(), std::chrono::seconds(10));
if (run.exitCode != 0 || run.timedOut || run.crashed) {
std::println(std::cerr, "exe did not exit cleanly: exit={} output={}",
run.exitCode, run.output);
return 1;
}
if (run.output != "7") {
std::println(std::cerr, "expected '7', got '{}'", run.output);
return 1;
}
return 0;
}

View file

@ -0,0 +1,4 @@
export module Greeter;
import std;
export std::string Greet() { return "ok-from-module"; }

View file

@ -0,0 +1,7 @@
import std;
import Greeter;
int main() {
std::print("{}", Greet());
return 0;
}

View file

@ -0,0 +1,65 @@
import std;
import Crafter.Build;
namespace fs = std::filesystem;
using namespace Crafter;
// Build an executable that imports a project-local .cppm module, then run
// it and check the produced output. Exercises the within-project module
// dependency path (main.cpp imports Greeter, resolved through cfg.interfaces).
int main() {
Configuration cfg;
cfg.path = fs::current_path() / "tests" / "ModuleInterface" / "fixture";
cfg.name = "greeter-app";
cfg.outputName = "greeter-app";
cfg.target = HostTarget();
cfg.type = ConfigurationType::Executable;
std::array<fs::path, 1> ifaces = { "Greeter" };
std::array<fs::path, 1> impls = { "main" };
cfg.GetInterfacesAndImplementations(ifaces, impls);
// Implementation should have picked up the import Greeter; through the
// interface list resolution in GetInterfacesAndImplementations.
if (cfg.implementations.size() != 1) {
std::println(std::cerr, "expected 1 implementation, got {}", cfg.implementations.size());
return 1;
}
if (cfg.implementations[0].moduleDependencies.size() != 1) {
std::println(std::cerr,
"expected main.cpp to depend on 1 module, got {}",
cfg.implementations[0].moduleDependencies.size());
return 1;
}
if (cfg.implementations[0].moduleDependencies[0]->name != "Greeter") {
std::println(std::cerr,
"expected dep 'Greeter', got '{}'",
cfg.implementations[0].moduleDependencies[0]->name);
return 1;
}
std::unordered_map<fs::path, std::shared_future<BuildResult>> depResults;
std::mutex depMutex;
BuildResult r = Build(cfg, depResults, depMutex);
if (!r.result.empty()) {
std::println(std::cerr, "build failed: {}", r.result);
return 1;
}
fs::path bin = cfg.BinDir() / "greeter-app";
if (!fs::exists(bin)) {
std::println(std::cerr, "binary not produced at {}", bin.string());
return 1;
}
auto run = RunCommandWithTimeout(bin.string(), std::chrono::seconds(10));
if (run.exitCode != 0 || run.timedOut || run.crashed) {
std::println(std::cerr, "binary did not exit cleanly: exit={} output={}",
run.exitCode, run.output);
return 1;
}
if (run.output != "ok-from-module") {
std::println(std::cerr, "unexpected output: '{}'", run.output);
return 1;
}
return 0;
}

View file

@ -0,0 +1,87 @@
import std;
import Crafter.Build;
namespace fs = std::filesystem;
using namespace Crafter;
namespace {
int failures = 0;
void Check(bool cond, std::string_view msg) {
if (!cond) {
std::println(std::cerr, "FAIL: {}", msg);
++failures;
}
}
// Write a tiny POSIX shell script that exits with `code` and return its
// path. The harness invokes `<binary> <args>` through the host shell, so
// a #!/bin/sh script works as a stand-in for a real test binary.
fs::path WriteExitScript(const fs::path& dir, std::string_view stem, int code) {
fs::path script = dir / stem;
std::ofstream out(script);
out << "#!/bin/sh\nexit " << code << "\n";
out.close();
fs::permissions(script,
fs::perms::owner_read | fs::perms::owner_write | fs::perms::owner_exec
| fs::perms::group_read | fs::perms::group_exec
| fs::perms::others_read | fs::perms::others_exec,
fs::perm_options::replace);
return script;
}
}
// Drives RunSingleTest against shell-script fixtures and asserts the
// exit-code → TestOutcome mapping documented in the README:
// 0 -> Pass
// 77 -> Skipped (autoconf convention)
// anything else -> Fail
int main() {
fs::path dir = fs::temp_directory_path() / "crafter-build-run-single-test";
std::error_code ec;
fs::remove_all(dir, ec);
fs::create_directories(dir);
Test t;
t.runner = TestRunner::Local();
t.config.outputName = "run-single";
{
fs::path script = WriteExitScript(dir, "pass.sh", 0);
TestResult r = RunSingleTest(t, script, std::chrono::seconds(5));
Check(r.outcome == TestOutcome::Pass, "exit 0 -> Pass");
Check(r.exitCode == 0, "Pass carries exitCode 0");
}
{
fs::path script = WriteExitScript(dir, "skip.sh", 77);
TestResult r = RunSingleTest(t, script, std::chrono::seconds(5));
Check(r.outcome == TestOutcome::Skipped, "exit 77 -> Skipped");
Check(r.exitCode == 77, "Skipped carries exitCode 77");
}
{
fs::path script = WriteExitScript(dir, "fail.sh", 3);
TestResult r = RunSingleTest(t, script, std::chrono::seconds(5));
Check(r.outcome == TestOutcome::Fail, "non-zero/non-77 exit -> Fail");
Check(r.exitCode == 3, "Fail carries the original exitCode");
}
// Cmd-prefix runner: wrap the script in `sh {bin} {args}` so we exercise
// the templated-exec path through RunSingleTest. Probes don't run here
// (RunSingleTest doesn't probe — RunTests does), so any prefix that
// resolves on PATH is fine.
{
Test ct;
ct.runner = TestRunner::Cmd("sh");
ct.config.outputName = "run-single-prefix";
fs::path script = WriteExitScript(dir, "prefixed.sh", 0);
TestResult r = RunSingleTest(ct, script, std::chrono::seconds(5));
Check(r.outcome == TestOutcome::Pass, "Cmd-prefixed exit 0 -> Pass");
}
fs::remove_all(dir, ec);
if (failures > 0) {
std::println(std::cerr, "{} assertions failed", failures);
return 1;
}
return 0;
}

View file

@ -0,0 +1,8 @@
#version 450
layout(location = 0) in vec2 inUV;
layout(location = 0) out vec4 outColor;
void main() {
outColor = vec4(inUV, 0.0, 1.0);
}

View file

@ -0,0 +1,53 @@
import std;
import Crafter.Build;
namespace fs = std::filesystem;
using namespace Crafter;
// Compile a tiny GLSL fragment shader directly via the Shader API. Verifies
// the SPIR-V output lands in the target dir and starts with the SPIR-V magic
// number, without dragging in the full Build() pipeline.
int main() {
fs::path src = fs::current_path() / "tests" / "ShaderCompile" / "fixture" / "triangle.frag";
if (!fs::exists(src)) {
std::println(std::cerr, "fixture missing: {}", src.string());
return 1;
}
fs::path outDir = fs::temp_directory_path() / "crafter-build-shader-test";
std::error_code ec;
fs::remove_all(outDir, ec);
fs::create_directories(outDir);
Shader shader(fs::path(src), "main", ShaderType::Fragment);
std::array<fs::path, 0> includeDirs = {};
std::string err = shader.Compile(outDir, includeDirs);
if (!err.empty()) {
std::println(std::cerr, "shader compile failed: {}", err);
return 1;
}
fs::path spv = outDir / "triangle.spv";
if (!fs::exists(spv)) {
std::println(std::cerr, "spv not produced at {}", spv.string());
return 1;
}
// SPIR-V binaries start with the magic number 0x07230203 (little-endian
// when written by glslang on x86). Read the first four bytes to confirm
// the produced file is real SPIR-V, not a fluke empty file.
std::ifstream in(spv, std::ios::binary);
std::uint32_t magic = 0;
in.read(reinterpret_cast<char*>(&magic), sizeof(magic));
if (!in || magic != 0x07230203u) {
std::println(std::cerr, "spv has wrong magic: 0x{:08x}", magic);
return 1;
}
// Compiling again should be a no-op since the source isn't newer.
if (!shader.Check(outDir)) {
std::println(std::cerr, "Shader::Check returned false on an up-to-date spv");
return 1;
}
return 0;
}

View file

@ -0,0 +1,75 @@
import std;
import Crafter.Build;
using namespace Crafter;
namespace {
int failures = 0;
void Check(bool cond, std::string_view msg) {
if (!cond) {
std::println(std::cerr, "FAIL: {}", msg);
++failures;
}
}
}
// Pure-data tests for ApplyStandardArgs and ArgQuery — no build/run, just
// confirms the documented arg parsing produces the documented mutations.
int main() {
// --debug + --target= + --march= + --mtune=
{
Configuration cfg;
cfg.target = "ignored";
std::array<std::string_view, 4> raw = {
"--debug", "--target=aarch64-linux-gnu", "--march=armv8-a", "--mtune=cortex-a72",
};
ApplyStandardArgs(cfg, raw);
Check(cfg.debug, "--debug should set cfg.debug");
Check(cfg.target == "aarch64-linux-gnu", "--target= should overwrite cfg.target");
Check(cfg.march == "armv8-a", "--march= should overwrite cfg.march");
Check(cfg.mtune == "cortex-a72", "--mtune= should overwrite cfg.mtune");
}
// --lib promotes Executable -> LibraryStatic; --shared then promotes to Dynamic.
{
Configuration cfg;
cfg.type = ConfigurationType::Executable;
std::array<std::string_view, 1> raw = { "--lib" };
ApplyStandardArgs(cfg, raw);
Check(cfg.type == ConfigurationType::LibraryStatic, "--lib promotes Exe -> Static");
}
{
Configuration cfg;
cfg.type = ConfigurationType::Executable;
// Order shouldn't matter — promotions chain in priority order.
std::array<std::string_view, 2> raw = { "--shared", "--lib" };
ApplyStandardArgs(cfg, raw);
Check(cfg.type == ConfigurationType::LibraryDynamic, "--lib + --shared lands on Dynamic regardless of order");
}
{
Configuration cfg;
cfg.type = ConfigurationType::Executable;
std::array<std::string_view, 1> raw = { "--shared" };
ApplyStandardArgs(cfg, raw);
Check(cfg.type == ConfigurationType::Executable, "--shared alone on Executable is a no-op");
}
// ArgQuery is returned over the same args span and exposes Has/Get.
{
Configuration cfg;
std::array<std::string_view, 3> raw = { "--timing", "--prefix=/opt/x", "extra" };
ArgQuery q = ApplyStandardArgs(cfg, raw);
Check(q.Has("--timing"), "ArgQuery::Has true for present flag");
Check(!q.Has("--missing"), "ArgQuery::Has false for absent flag");
auto prefix = q.Get("--prefix=");
Check(prefix.has_value(), "ArgQuery::Get returns a value for known prefix");
Check(prefix.value_or("") == "/opt/x", "ArgQuery::Get returns the substring after =");
Check(!q.Get("--other=").has_value(), "ArgQuery::Get returns nullopt for absent prefix");
}
if (failures > 0) {
std::println(std::cerr, "{} assertions failed", failures);
return 1;
}
return 0;
}

View file

@ -0,0 +1,4 @@
export module Greet;
import std;
export std::string Hello() { return "hi"; }

49
tests/StaticLib/main.cpp Normal file
View file

@ -0,0 +1,49 @@
import std;
import Crafter.Build;
namespace fs = std::filesystem;
using namespace Crafter;
// Build a tiny module-only static library and confirm libgreet.a lands in
// BinDir(). Exercises ConfigurationType::LibraryStatic, interface module
// parsing via GetInterfacesAndImplementations, and the ar/llvm-lib archive
// step in Crafter.Build-Clang.cpp.
int main() {
Configuration cfg;
cfg.path = fs::current_path() / "tests" / "StaticLib" / "fixture";
cfg.name = "greet";
cfg.outputName = "greet";
cfg.target = HostTarget();
cfg.type = ConfigurationType::LibraryStatic;
std::array<fs::path, 1> ifaces = { "Greet" };
std::array<fs::path, 0> impls = {};
cfg.GetInterfacesAndImplementations(ifaces, impls);
if (cfg.interfaces.size() != 1) {
std::println(std::cerr, "expected 1 interface, got {}", cfg.interfaces.size());
return 1;
}
if (cfg.interfaces[0]->name != "Greet") {
std::println(std::cerr, "expected interface 'Greet', got '{}'", cfg.interfaces[0]->name);
return 1;
}
std::unordered_map<fs::path, std::shared_future<BuildResult>> depResults;
std::mutex depMutex;
BuildResult r = Build(cfg, depResults, depMutex);
if (!r.result.empty()) {
std::println(std::cerr, "build failed: {}", r.result);
return 1;
}
fs::path archive = cfg.BinDir() / "libgreet.a";
if (!fs::exists(archive)) {
std::println(std::cerr, "archive not produced at {}", archive.string());
return 1;
}
if (fs::file_size(archive) == 0) {
std::println(std::cerr, "archive {} is empty", archive.string());
return 1;
}
return 0;
}

View file

@ -0,0 +1,115 @@
import std;
import Crafter.Build;
using namespace Crafter;
// POSIX env helpers — libc++ exports only std::getenv via `import std`, so
// forward-declare the setters we need against the C library directly. The
// test only runs on POSIX hosts (the project.cpp gates new tests by
// x86_64-pc-linux-gnu) so we don't need a Windows alternative here.
extern "C" int setenv(const char* name, const char* value, int overwrite);
extern "C" int unsetenv(const char* name);
namespace {
int failures = 0;
void Check(bool cond, std::string_view msg) {
if (!cond) {
std::println(std::cerr, "FAIL: {}", msg);
++failures;
}
}
}
// Pure-data tests for TestRunner factories. No tests are actually executed;
// we just confirm the routing tables in ForTarget / FromSpec / FromEnv.
int main() {
// FromSpec: empty -> nullopt; recognized -> populated; garbage -> throws.
{
auto empty = TestRunner::FromSpec("");
Check(!empty.has_value(), "FromSpec(\"\") returns nullopt");
}
{
auto local = TestRunner::FromSpec("local");
Check(local.has_value() && local->IsLocal(), "FromSpec(\"local\") returns Local");
Check(local.has_value() && local->name == "local", "Local name is 'local'");
}
{
auto cmd = TestRunner::FromSpec("cmd:wasmtime");
Check(cmd.has_value(), "FromSpec(\"cmd:wasmtime\") returns a runner");
Check(cmd.has_value() && cmd->name == "cmd:wasmtime", "Cmd name carries the binary");
Check(cmd.has_value() && cmd->exec.find("wasmtime") != std::string::npos, "Cmd exec carries the binary");
Check(cmd.has_value() && !cmd->probe.empty(), "Cmd probe is set so harness can detect missing tools");
}
{
bool threw = false;
try {
(void)TestRunner::FromSpec("garbage");
} catch (const std::exception&) {
threw = true;
}
Check(threw, "FromSpec rejects unrecognized non-empty specs");
}
// ForTarget routing.
{
Configuration cfg;
cfg.target = HostTarget();
TestRunner r = TestRunner::ForTarget(cfg);
Check(r.IsLocal(), "ForTarget(host) is Local");
}
{
Configuration cfg;
cfg.target = "wasm32-wasip1";
TestRunner r = TestRunner::ForTarget(cfg);
Check(r.name == "cmd:wasmtime", "ForTarget(wasm32-wasip1) routes through wasmtime");
}
{
Configuration cfg;
cfg.target = "aarch64-linux-gnu";
TestRunner r = TestRunner::ForTarget(cfg);
Check(r.name == "cmd:qemu-aarch64", "ForTarget(aarch64-linux-gnu) routes through qemu-aarch64");
}
{
Configuration cfg;
cfg.target = "i686-linux-gnu";
TestRunner r = TestRunner::ForTarget(cfg);
Check(r.name == "cmd:qemu-i386", "ForTarget(i686-linux-gnu) rewrites arch to i386");
}
{
// Sysroot propagates to QEMU_LD_PREFIX so the dynamic linker is reachable.
Configuration cfg;
cfg.target = "aarch64-linux-gnu";
cfg.sysroot = "/opt/alarm-sysroot";
TestRunner r = TestRunner::ForTarget(cfg);
Check(r.exec.find("QEMU_LD_PREFIX=/opt/alarm-sysroot") != std::string::npos,
"ForTarget propagates sysroot to QEMU_LD_PREFIX");
}
{
// From a Linux host the only sensible way to run x86_64-w64-mingw32 is wine.
Configuration cfg;
cfg.target = "x86_64-w64-mingw32";
TestRunner r = TestRunner::ForTarget(cfg);
if (HostTarget().find("linux") != std::string::npos) {
Check(r.name == "wine", "ForTarget(mingw) on Linux host uses wine");
}
}
// FromEnv: when the env var is set, it wins over the fallback.
{
// Use a unique fake triple so we don't stomp on a real env var the
// CI/dev shell may have set.
const char* name = "CRAFTER_BUILD_RUNNER_fake_target_for_unit_test";
setenv(name, "cmd:fake-tool", 1);
TestRunner r = TestRunner::FromEnv("fake-target-for-unit-test", TestRunner::Local());
Check(r.name == "cmd:fake-tool", "FromEnv reads CRAFTER_BUILD_RUNNER_<target>");
unsetenv(name);
TestRunner fallback = TestRunner::FromEnv("fake-target-for-unit-test", TestRunner::Local());
Check(fallback.IsLocal(), "FromEnv falls back when env var is unset");
}
if (failures > 0) {
std::println(std::cerr, "{} assertions failed", failures);
return 1;
}
return 0;
}

120
tests/VariantId/main.cpp Normal file
View file

@ -0,0 +1,120 @@
import std;
import Crafter.Build;
using namespace Crafter;
namespace {
int failures = 0;
void Check(bool cond, std::string_view msg) {
if (!cond) {
std::println(std::cerr, "FAIL: {}", msg);
++failures;
}
}
Configuration MakeBase() {
Configuration cfg;
cfg.path = "/tmp/variant-id-test";
cfg.name = "vt";
cfg.outputName = "vt";
cfg.target = "x86_64-pc-linux-gnu";
cfg.march = "native";
cfg.mtune = "native";
cfg.type = ConfigurationType::Executable;
return cfg;
}
}
// VariantId is the cache key for build outputs. Two Configurations that
// differ only in something that affects codegen must produce different
// VariantId / BuildDir / BinDir, otherwise their .o files collide.
int main() {
{
// Baseline against itself: VariantId is deterministic.
Configuration a = MakeBase();
Configuration b = MakeBase();
Check(a.VariantId() == b.VariantId(), "identical configs have identical VariantId");
Check(a.BuildDir() == b.BuildDir(), "identical configs have identical BuildDir");
}
{
// type differs -> VariantId differs.
Configuration a = MakeBase();
Configuration b = MakeBase();
b.type = ConfigurationType::LibraryStatic;
Check(a.VariantId() != b.VariantId(), "type change perturbs VariantId");
}
{
// debug differs -> VariantId differs.
Configuration a = MakeBase();
Configuration b = MakeBase();
b.debug = true;
Check(a.VariantId() != b.VariantId(), "debug change perturbs VariantId");
}
{
// sysroot differs -> VariantId differs.
Configuration a = MakeBase();
Configuration b = MakeBase();
b.sysroot = "/opt/sysroot";
Check(a.VariantId() != b.VariantId(), "sysroot change perturbs VariantId");
}
{
// defines differ -> VariantId differs.
Configuration a = MakeBase();
Configuration b = MakeBase();
b.defines.push_back({"FOO", "1"});
Check(a.VariantId() != b.VariantId(), "define added perturbs VariantId");
}
{
// Same define name, different value -> VariantId differs.
Configuration a = MakeBase();
a.defines.push_back({"FOO", "1"});
Configuration b = MakeBase();
b.defines.push_back({"FOO", "2"});
Check(a.VariantId() != b.VariantId(), "define value change perturbs VariantId");
}
{
// compileFlags differ -> VariantId differs.
Configuration a = MakeBase();
Configuration b = MakeBase();
b.compileFlags.push_back("-fno-omit-frame-pointer");
Check(a.VariantId() != b.VariantId(), "compile flag added perturbs VariantId");
}
{
// target differs -> VariantId differs.
Configuration a = MakeBase();
Configuration b = MakeBase();
b.target = "aarch64-linux-gnu";
Check(a.VariantId() != b.VariantId(), "target change perturbs VariantId");
}
{
// march differs -> VariantId differs.
Configuration a = MakeBase();
Configuration b = MakeBase();
b.march = "x86-64-v3";
Check(a.VariantId() != b.VariantId(), "march change perturbs VariantId");
}
{
// PcmDir() differs between Executable (in BuildDir) and Library
// (in BinDir) — Library PCMs land in the installable bin dir so
// downstream consumers can find them.
Configuration exe = MakeBase();
Configuration lib = MakeBase();
lib.type = ConfigurationType::LibraryStatic;
Check(exe.PcmDir() == exe.BuildDir(), "Executable PcmDir == BuildDir");
Check(lib.PcmDir() == lib.BinDir(), "Library PcmDir == BinDir");
}
{
// VariantId is embedded in the path so distinct ids produce distinct dirs.
Configuration a = MakeBase();
Configuration b = MakeBase();
b.debug = true;
Check(a.BuildDir() != b.BuildDir(), "different VariantId yields different BuildDir");
Check(a.BinDir() != b.BinDir(), "different VariantId yields different BinDir");
}
if (failures > 0) {
std::println(std::cerr, "{} assertions failed", failures);
return 1;
}
return 0;
}

View file

@ -0,0 +1,97 @@
import std;
import Crafter.Build;
namespace fs = std::filesystem;
using namespace Crafter;
// POSIX env helper — std::setenv is non-standard. Linux-only is fine here;
// project.cpp gates this test on x86_64-pc-linux-gnu.
extern "C" int setenv(const char* name, const char* value, int overwrite);
// EnableWasiBrowserRuntime stamps runtime.js, a generated index.html, and a
// files.json manifest onto cfg.files so the build step copies them next to
// the .wasm. We don't run the wasm build here (avoids requiring wasi-sdk +
// the wasi-libc++ PCM rebuild on top of every test); we just verify the
// helper produced the right artifacts and registered them on the config.
int main() {
Configuration cfg;
cfg.path = fs::temp_directory_path() / "crafter-build-wasi-runtime-test";
cfg.name = "wasi-helper";
cfg.outputName = "wasi-helper";
cfg.target = "wasm32-wasip1";
cfg.type = ConfigurationType::Executable;
std::error_code ec;
fs::remove_all(cfg.path, ec);
fs::create_directories(cfg.path);
// EnableWasiBrowserRuntime looks up runtime.js + index.html.in under
// GetCrafterBuildHome()/wasi-runtime. The test binary lives in
// bin/<variant>/, so the auto-detect walks parents and won't find a
// share/crafter-build/Crafter.Build.cppm anchor — point CRAFTER_BUILD_HOME
// at the repo root (cwd when crafter-build test runs) so it picks up
// ./wasi-runtime/. Probe both repo root and the system-installed share
// dir to also work from a packaged checkout.
fs::path repoWasi = fs::current_path() / "wasi-runtime";
fs::path systemHome = "/usr/local/share/crafter-build";
if (fs::exists(repoWasi / "runtime.js")) {
setenv("CRAFTER_BUILD_HOME", fs::current_path().c_str(), 1);
} else if (fs::exists(systemHome / "wasi-runtime" / "runtime.js")) {
setenv("CRAFTER_BUILD_HOME", systemHome.c_str(), 1);
} else {
std::println(std::cerr,
"wasi-runtime assets not found near {} or {}",
repoWasi.string(), systemHome.string());
return 77; // skipped — environmental, not a code defect
}
EnableWasiBrowserRuntime(cfg);
// Three files registered: runtime.js, generated index.html, files.json.
if (cfg.files.size() != 3) {
std::println(std::cerr, "expected 3 cfg.files entries, got {}", cfg.files.size());
for (const auto& f : cfg.files) std::println(std::cerr, " - {}", f.string());
return 1;
}
auto findByName = [&](std::string_view name) -> const fs::path* {
for (const fs::path& f : cfg.files) {
if (f.filename() == name) return &f;
}
return nullptr;
};
const fs::path* runtimeJs = findByName("runtime.js");
const fs::path* indexHtml = findByName("index.html");
const fs::path* manifest = findByName("files.json");
if (!runtimeJs) { std::println(std::cerr, "runtime.js not registered"); return 1; }
if (!indexHtml) { std::println(std::cerr, "index.html not registered"); return 1; }
if (!manifest) { std::println(std::cerr, "files.json not registered"); return 1; }
// The generated index.html and manifest live under the project's build/
// wasi-runtime/<name>/ directory and must actually exist on disk.
if (!fs::exists(*indexHtml)) {
std::println(std::cerr, "index.html not produced at {}", indexHtml->string());
return 1;
}
if (!fs::exists(*manifest)) {
std::println(std::cerr, "files.json not produced at {}", manifest->string());
return 1;
}
// The template's {{WASM}} placeholder should have been replaced with
// the configured outputName.
std::ifstream in(*indexHtml);
std::string html{std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>()};
if (html.find("wasi-helper.wasm") == std::string::npos) {
std::println(std::cerr, "index.html missing wasi-helper.wasm reference");
return 1;
}
if (html.find("{{WASM}}") != std::string::npos) {
std::println(std::cerr, "index.html still contains unreplaced {{WASM}} placeholder");
return 1;
}
fs::remove_all(cfg.path, ec);
return 0;
}