Expand test suite #13
16 changed files with 794 additions and 0 deletions
|
|
@ -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.
|
||||
if (cfg.target == "x86_64-pc-linux-gnu") {
|
||||
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;
|
||||
|
|
|
|||
7
tests/DependencyLink/fixture/main.cpp
Normal file
7
tests/DependencyLink/fixture/main.cpp
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import std;
|
||||
import Calc;
|
||||
|
||||
int main() {
|
||||
std::print("{}", Add(3, 4));
|
||||
return 0;
|
||||
}
|
||||
4
tests/DependencyLink/fixture/mathlib/Calc.cppm
Normal file
4
tests/DependencyLink/fixture/mathlib/Calc.cppm
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export module Calc;
|
||||
import std;
|
||||
|
||||
export int Add(int a, int b) { return a + b; }
|
||||
90
tests/DependencyLink/main.cpp
Normal file
90
tests/DependencyLink/main.cpp
Normal 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;
|
||||
}
|
||||
4
tests/ModuleInterface/fixture/Greeter.cppm
Normal file
4
tests/ModuleInterface/fixture/Greeter.cppm
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export module Greeter;
|
||||
import std;
|
||||
|
||||
export std::string Greet() { return "ok-from-module"; }
|
||||
7
tests/ModuleInterface/fixture/main.cpp
Normal file
7
tests/ModuleInterface/fixture/main.cpp
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import std;
|
||||
import Greeter;
|
||||
|
||||
int main() {
|
||||
std::print("{}", Greet());
|
||||
return 0;
|
||||
}
|
||||
65
tests/ModuleInterface/main.cpp
Normal file
65
tests/ModuleInterface/main.cpp
Normal 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;
|
||||
}
|
||||
87
tests/RunSingleTestExit/main.cpp
Normal file
87
tests/RunSingleTestExit/main.cpp
Normal 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;
|
||||
}
|
||||
8
tests/ShaderCompile/fixture/triangle.frag
Normal file
8
tests/ShaderCompile/fixture/triangle.frag
Normal 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);
|
||||
}
|
||||
53
tests/ShaderCompile/main.cpp
Normal file
53
tests/ShaderCompile/main.cpp
Normal 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;
|
||||
}
|
||||
75
tests/StandardArgs/main.cpp
Normal file
75
tests/StandardArgs/main.cpp
Normal 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;
|
||||
}
|
||||
4
tests/StaticLib/fixture/Greet.cppm
Normal file
4
tests/StaticLib/fixture/Greet.cppm
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export module Greet;
|
||||
import std;
|
||||
|
||||
export std::string Hello() { return "hi"; }
|
||||
49
tests/StaticLib/main.cpp
Normal file
49
tests/StaticLib/main.cpp
Normal 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;
|
||||
}
|
||||
115
tests/TestRunnerSpec/main.cpp
Normal file
115
tests/TestRunnerSpec/main.cpp
Normal 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
120
tests/VariantId/main.cpp
Normal 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;
|
||||
}
|
||||
97
tests/WasiBrowserRuntime/main.cpp
Normal file
97
tests/WasiBrowserRuntime/main.cpp
Normal 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue