From 19b059a88c213a0ffed755df70e166cb7aeed214 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Wed, 29 Apr 2026 03:52:08 +0200 Subject: [PATCH] more tests --- README.md | 2 +- tests/Cuda/Cuda.cpp | 94 ++++++++++++++++++++++++++++++++ tests/Cuda/inner/kernel.cu | 16 ++++++ tests/Cuda/inner/main.cpp | 8 +++ tests/Cuda/inner/project.cpp | 29 ++++++++++ tests/Cuda/project.cpp | 20 +++++++ tests/Shader/Shader.cpp | 61 +++++++++++++++++++++ tests/Shader/inner/main.cpp | 3 + tests/Shader/inner/project.cpp | 21 +++++++ tests/Shader/inner/triangle.glsl | 13 +++++ tests/Shader/project.cpp | 20 +++++++ 11 files changed, 286 insertions(+), 1 deletion(-) create mode 100644 tests/Cuda/Cuda.cpp create mode 100644 tests/Cuda/inner/kernel.cu create mode 100644 tests/Cuda/inner/main.cpp create mode 100644 tests/Cuda/inner/project.cpp create mode 100644 tests/Cuda/project.cpp create mode 100644 tests/Shader/Shader.cpp create mode 100644 tests/Shader/inner/main.cpp create mode 100644 tests/Shader/inner/project.cpp create mode 100644 tests/Shader/inner/triangle.glsl create mode 100644 tests/Shader/project.cpp diff --git a/README.md b/README.md index 470b38e..94ca3f8 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Bootstrap requires `clang`, `cmake`, `git`, `lld`, and `libc++`. ```bash ./build.sh # produces bin/crafter-build -CRAFTER_BUILD_HOME=$PWD/build ./bin/crafter-build # rebuild via project.cpp +./bin/crafter-build # rebuild via project.cpp ``` For distro-packaged installs, `crafter-build` finds its modules at `/share/crafter-build/` automatically — no env var required. diff --git a/tests/Cuda/Cuda.cpp b/tests/Cuda/Cuda.cpp new file mode 100644 index 0000000..b46dcb8 --- /dev/null +++ b/tests/Cuda/Cuda.cpp @@ -0,0 +1,94 @@ +/* +Crafter® Build +Copyright (C) 2026 Catcrafts® +Catcrafts.net + +LGPL-3.0-only. + +End-to-end CUDA build through the V2 pipeline: +the inner project.cpp registers a .cu translation unit via cfg.cuda, and +crafter.build-lib drives nvcc to produce a host object that links into +the final executable. The test skips when the CUDA SDK is missing +(CI hosts have neither nvcc nor libcudart) and otherwise runs the built +binary to confirm the .cu's host-callable entry returns through the link. + +No GPU is required: the kernel is staged but never launched, so cudart's +fat-binary registration runs at startup but device init does not. +*/ + +import std; +import Crafter.Build; +#include "../_shared/TestUtil.h" +namespace fs = std::filesystem; +using namespace TestUtil; +using namespace Crafter; + +namespace { + bool ToolPresent(std::string_view name) { + return std::system(std::format("which {} > /dev/null 2>&1", name).c_str()) == 0; + } +} + +int main() { + try { + if (!ToolPresent("nvcc")) { + Skip("nvcc not on PATH — install the CUDA SDK to enable this test"); + } + + // Resolve the CUDA root from nvcc's own location so the inner project + // can point the linker at libcudart.so without a hardcoded path. + // Standard SDK layout puts nvcc at /bin/nvcc and cudart at + // /lib64; if either assumption is broken we skip rather than + // fail, since that means the install is non-standard. + fs::path cwd = fs::current_path(); + auto probe = RunInDir(cwd, "command -v nvcc"); + if (probe.exitCode != 0 || probe.output.empty()) { + Skip("nvcc not on PATH"); + } + std::string nvccPath = probe.output; + while (!nvccPath.empty() && (nvccPath.back() == '\n' || nvccPath.back() == '\r')) { + nvccPath.pop_back(); + } + fs::path real = fs::canonical(nvccPath); + fs::path cudaLibDir = real.parent_path().parent_path() / "lib64"; + if (!fs::exists(cudaLibDir / "libcudart.so")) { + Skip(std::format("libcudart.so not found at {} — incomplete CUDA SDK install", + cudaLibDir.string())); + } + ::setenv("CRAFTER_CUDA_LIBDIR", cudaLibDir.string().c_str(), 1); + + fs::path src = cwd / "tests" / "Cuda" / "inner"; + Configuration cfg = LoadFixture("Cuda", src); + fs::path work = fs::current_path(); + + auto br = BuildOnce(cfg); + if (!br.result.empty()) { + std::println(std::cerr, "build failed:\n{}", br.result); + return 1; + } + + fs::path artifact = work / "bin" / "cuda-test-x86_64-pc-linux-gnu-native" / "cuda-test"; + if (!fs::exists(artifact)) { + std::println(std::cerr, "expected artifact missing at {}", artifact.string()); + return 1; + } + + // libcudart.so is on the system loader path on most CUDA installs + // (Arch puts /opt/cuda/lib64 in /etc/ld.so.conf.d), but other distros + // don't, so set LD_LIBRARY_PATH explicitly for the run. + auto run = RunInDir(work, std::format( + "LD_LIBRARY_PATH='{}' '{}'", cudaLibDir.string(), artifact.string())); + if (run.exitCode != 0) { + std::println(std::cerr, "artifact exited nonzero (rc={}):\n{}", run.exitCode, run.output); + return 1; + } + if (run.output != "answer=42\n") { + std::println(std::cerr, "output mismatch:\n expected: \"answer=42\\n\"\n got: {:?}", run.output); + return 1; + } + return 0; + } catch (const std::exception& e) { + std::println(std::cerr, "test exception: {}", e.what()); + return 1; + } +} diff --git a/tests/Cuda/inner/kernel.cu b/tests/Cuda/inner/kernel.cu new file mode 100644 index 0000000..d100053 --- /dev/null +++ b/tests/Cuda/inner/kernel.cu @@ -0,0 +1,16 @@ +// A staged-but-unlaunched kernel forces nvcc to emit a real fatbin and +// the cudart-registration glue, which is what makes this a meaningful +// CUDA-pipeline test. The host entry the C++ side actually calls returns +// the same constant via plain CPU code so the test passes on hosts that +// have the SDK but no NVIDIA driver/GPU. +__device__ int compute_on_device() { + return 42; +} + +__global__ void kernel(int* out) { + *out = compute_on_device(); +} + +extern "C" int kernel_compute() { + return 42; +} diff --git a/tests/Cuda/inner/main.cpp b/tests/Cuda/inner/main.cpp new file mode 100644 index 0000000..d5829f7 --- /dev/null +++ b/tests/Cuda/inner/main.cpp @@ -0,0 +1,8 @@ +import std; + +extern "C" int kernel_compute(); + +int main() { + std::println("answer={}", kernel_compute()); + return 0; +} diff --git a/tests/Cuda/inner/project.cpp b/tests/Cuda/inner/project.cpp new file mode 100644 index 0000000..98eff15 --- /dev/null +++ b/tests/Cuda/inner/project.cpp @@ -0,0 +1,29 @@ +import std; +import Crafter.Build; +namespace fs = std::filesystem; +using namespace Crafter; + +extern "C" Configuration CrafterBuildProject(std::span) { + Configuration cfg; + cfg.path = "./"; + cfg.name = "cuda-test"; + cfg.outputName = "cuda-test"; + cfg.target = "x86_64-pc-linux-gnu"; + cfg.type = ConfigurationType::Executable; + + std::array ifaces = {}; + std::array impls = { "main" }; + cfg.GetInterfacesAndImplementations(ifaces, impls); + + cfg.cuda.push_back("kernel"); + + // The outer test driver discovers the SDK location and exports it; nvcc + // emits __cudaRegisterFatBinary references into every .cu object even + // when no kernel is launched, so libcudart has to be on the link line. + if (const char* libDir = std::getenv("CRAFTER_CUDA_LIBDIR")) { + cfg.linkFlags.push_back(std::format("-L{}", libDir)); + cfg.linkFlags.push_back("-lcudart"); + } + + return cfg; +} diff --git a/tests/Cuda/project.cpp b/tests/Cuda/project.cpp new file mode 100644 index 0000000..107c671 --- /dev/null +++ b/tests/Cuda/project.cpp @@ -0,0 +1,20 @@ +import std; +import Crafter.Build; +namespace fs = std::filesystem; +using namespace Crafter; + +extern "C" Configuration CrafterBuildProject(std::span) { + Configuration cfg; + cfg.path = "tests/Cuda/"; + cfg.name = "Cuda"; + cfg.outputName = "Cuda"; + cfg.target = "x86_64-pc-linux-gnu"; + cfg.type = ConfigurationType::Executable; + cfg.dependencies = { ParentLib("crafter.build-lib") }; + cfg.linkFlags.push_back("-Wl,--export-dynamic"); + cfg.linkFlags.push_back("-ldl"); + std::array ifaces = {}; + std::array impls = { "Cuda" }; + cfg.GetInterfacesAndImplementations(ifaces, impls); + return cfg; +} diff --git a/tests/Shader/Shader.cpp b/tests/Shader/Shader.cpp new file mode 100644 index 0000000..75adfde --- /dev/null +++ b/tests/Shader/Shader.cpp @@ -0,0 +1,61 @@ +/* +Crafter® Build +Copyright (C) 2026 Catcrafts® +Catcrafts.net + +LGPL-3.0-only. + +End-to-end shader build through the V2 pipeline: +the inner project.cpp registers a vertex shader via cfg.shaders, and +crafter.build-lib invokes glslang in-process to lower it to SPIR-V. The +test verifies the resulting .spv file lands in the per-target bin dir +and starts with the SPIR-V magic word — a zero-length output or one +written under a different name would be flagged here rather than +silently passing. +*/ + +import std; +import Crafter.Build; +#include "../_shared/TestUtil.h" +namespace fs = std::filesystem; +using namespace TestUtil; +using namespace Crafter; + +int main() { + try { + fs::path src = fs::current_path() / "tests" / "Shader" / "inner"; + Configuration cfg = LoadFixture("Shader", src); + fs::path work = fs::current_path(); + + auto br = BuildOnce(cfg); + if (!br.result.empty()) { + std::println(std::cerr, "build failed:\n{}", br.result); + return 1; + } + + fs::path spv = work / "bin" / "shader-test-x86_64-pc-linux-gnu-native" / "triangle.spv"; + if (!fs::exists(spv)) { + std::println(std::cerr, "expected SPIR-V output missing at {}", spv.string()); + return 1; + } + + // SPIR-V module header magic is 0x07230203, written as a 32-bit word + // in the module's endianness (always little-endian on the targets we + // build). Matching the raw byte sequence catches truncated writes + // and any future regression that produces a non-SPIR-V .spv. + std::ifstream f(spv, std::ios::binary); + unsigned char magic[4] = {}; + f.read(reinterpret_cast(magic), 4); + if (magic[0] != 0x03 || magic[1] != 0x02 || magic[2] != 0x23 || magic[3] != 0x07) { + std::println(std::cerr, + "SPIR-V magic mismatch: got {:#04x} {:#04x} {:#04x} {:#04x}", + magic[0], magic[1], magic[2], magic[3]); + return 1; + } + + return 0; + } catch (const std::exception& e) { + std::println(std::cerr, "test exception: {}", e.what()); + return 1; + } +} diff --git a/tests/Shader/inner/main.cpp b/tests/Shader/inner/main.cpp new file mode 100644 index 0000000..54e275a --- /dev/null +++ b/tests/Shader/inner/main.cpp @@ -0,0 +1,3 @@ +import std; + +int main() { return 0; } diff --git a/tests/Shader/inner/project.cpp b/tests/Shader/inner/project.cpp new file mode 100644 index 0000000..669e1d0 --- /dev/null +++ b/tests/Shader/inner/project.cpp @@ -0,0 +1,21 @@ +import std; +import Crafter.Build; +namespace fs = std::filesystem; +using namespace Crafter; + +extern "C" Configuration CrafterBuildProject(std::span) { + Configuration cfg; + cfg.path = "./"; + cfg.name = "shader-test"; + cfg.outputName = "shader-test"; + cfg.target = "x86_64-pc-linux-gnu"; + cfg.type = ConfigurationType::Executable; + + std::array ifaces = {}; + std::array impls = { "main" }; + cfg.GetInterfacesAndImplementations(ifaces, impls); + + cfg.shaders.emplace_back("triangle.glsl", "main", ShaderType::Vertex); + + return cfg; +} diff --git a/tests/Shader/inner/triangle.glsl b/tests/Shader/inner/triangle.glsl new file mode 100644 index 0000000..e1abf19 --- /dev/null +++ b/tests/Shader/inner/triangle.glsl @@ -0,0 +1,13 @@ +#version 450 + +layout(location = 0) out vec3 color; + +void main() { + vec2 positions[3] = vec2[]( + vec2( 0.0, -0.5), + vec2( 0.5, 0.5), + vec2(-0.5, 0.5) + ); + gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0); + color = vec3(1.0, 0.0, 0.0); +} diff --git a/tests/Shader/project.cpp b/tests/Shader/project.cpp new file mode 100644 index 0000000..8dd624d --- /dev/null +++ b/tests/Shader/project.cpp @@ -0,0 +1,20 @@ +import std; +import Crafter.Build; +namespace fs = std::filesystem; +using namespace Crafter; + +extern "C" Configuration CrafterBuildProject(std::span) { + Configuration cfg; + cfg.path = "tests/Shader/"; + cfg.name = "Shader"; + cfg.outputName = "Shader"; + cfg.target = "x86_64-pc-linux-gnu"; + cfg.type = ConfigurationType::Executable; + cfg.dependencies = { ParentLib("crafter.build-lib") }; + cfg.linkFlags.push_back("-Wl,--export-dynamic"); + cfg.linkFlags.push_back("-ldl"); + std::array ifaces = {}; + std::array impls = { "Shader" }; + cfg.GetInterfacesAndImplementations(ifaces, impls); + return cfg; +}