test runner, cross-target runners, lib/exe split
- subprocess-isolated test runner (replaces V1 dlopen-RunTest); Pass/Fail/Crash/Timeout/Skipped outcomes via :Test partition - TestRunner abstraction with command templates: Local, Ssh, SshWin (cmd.exe-shell), QemuUser, FromEnv; probe-based skip when runner unreachable - transitive PCM-path propagation in Build(); resolveImport walks deps recursively; depResults cache keyed by PcmDir() so per-target builds don't collide - cfg.sysroot threaded through BuildStdPcm + base compile/link command (enables aarch64 cross via Arch Linux ARM rootfs) - lib + exe split: project.cpp defines crafterBuildLib (LibraryStatic) + crafterBuildExe (Executable depending on it); build.sh produces lib/libcrafter-build.a alongside bin/crafter-build for downstream static-link consumers - Windows DLL+launcher: CRAFTER_API macro, /EXPORT flag for project.dll's CrafterBuildProject; Crafter::Run as the real entry point with main.cpp as a thin wrapper - 18 tests: HelloWorld/WithModule/Defines/CrossProjectModule/ Diamond × (Linux + sshwin:winvm), plus Incremental, BuildError, Libraries, RunnerClassification, QemuUser, SshRunner, WindowsViaSsh, CrossArchAarch64 - single ./bin/crafter-build test runs everything; Windows variants skip gracefully if winvm SSH alias unreachable Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
f13671b2be
commit
cdfdb976c8
60 changed files with 2029 additions and 104 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,4 +1,5 @@
|
|||
build/
|
||||
bin/
|
||||
lib/*.a
|
||||
share/crafter-build/
|
||||
.claude
|
||||
2
PKGBUILD
2
PKGBUILD
|
|
@ -20,6 +20,8 @@ build() {
|
|||
package() {
|
||||
cd "$startdir"
|
||||
install -Dm755 bin/crafter-build "$pkgdir/usr/bin/crafter-build"
|
||||
install -Dm644 lib/libcrafter-build.a "$pkgdir/usr/lib/libcrafter-build.a"
|
||||
install -dm755 "$pkgdir/usr/share/crafter-build"
|
||||
install -m644 share/crafter-build/*.cppm "$pkgdir/usr/share/crafter-build/"
|
||||
install -m644 share/crafter-build/*.h "$pkgdir/usr/share/crafter-build/"
|
||||
}
|
||||
|
|
|
|||
126
README.md
126
README.md
|
|
@ -5,7 +5,7 @@ A C++26 modules build system. Project descriptions are written in C++ — no JSO
|
|||
## Status
|
||||
|
||||
- **Linux**: working end-to-end, packageable as a distro package. Verified on Arch Linux via [PKGBUILD](PKGBUILD) in a fresh container.
|
||||
- **Windows**: bootstrap builds and runs; cross-EXE/DLL symbol-resolution work in progress.
|
||||
- **Windows**: working end-to-end. Bootstrap (`build.cmd`) produces `crafter-build.exe` + `crafter-build.dll` + `crafter-build.lib`; user `project.cpp` links against the import lib via the host-controlled DLL boundary.
|
||||
|
||||
## Quick start (Linux)
|
||||
|
||||
|
|
@ -79,12 +79,15 @@ Clones live at `~/.cache/crafter.build/external/<name>-<hash>/`, keyed by `(url,
|
|||
|
||||
```
|
||||
<prefix>/bin/crafter-build # the executable
|
||||
<prefix>/lib/libcrafter-build.a # static archive for downstream consumers
|
||||
<prefix>/share/crafter-build/*.cppm # module sources (rebuilt on user machine on first run)
|
||||
~/.cache/crafter.build/<target>-<march>/ # std.pcm + Crafter.Build*.pcm, built locally
|
||||
```
|
||||
|
||||
Sources ship instead of PCMs because libc++ ABI varies between machines — the user's machine builds its own PCMs against its own libc++ on first run (~6-8s one-time cost), making the install resilient to libc++ version differences across distros.
|
||||
|
||||
The static archive is for downstream projects that want to use Crafter.Build *as a library* (construct `Configuration` objects, call `Build()`, etc., directly in C++) without invoking the CLI. Link with `-lcrafter-build` plus the project's external dep libs (`-lSPIRV -lglslang -lOSDependent -lMachineIndependent -lglslang-default-resource-limits -lGenericCodeGen`) and `-ldl` (for `LoadProject`'s dlopen path on Linux). On Windows the equivalent is `bin/crafter-build-static.lib`.
|
||||
|
||||
## Build incrementality
|
||||
|
||||
Per-import precise tracking for both within-project and cross-project module dependencies:
|
||||
|
|
@ -95,16 +98,128 @@ Per-import precise tracking for both within-project and cross-project module dep
|
|||
|
||||
Diamond deps (`A → {B, C}; B → X; C → X`) build `X` exactly once via a `std::shared_future<BuildResult>` cache.
|
||||
|
||||
## Tests
|
||||
|
||||
Tests are normal executables — any `int main()`, exit code `0` = pass, nonzero = fail, captured stdout+stderr is the failure message. There's no Crafter-specific entry-point convention, so a GoogleTest/Catch2/doctest binary works as-is.
|
||||
|
||||
Declare tests by pushing onto `cfg.tests` in your `project.cpp`:
|
||||
|
||||
```cpp
|
||||
Test t;
|
||||
t.config.path = "./";
|
||||
t.config.name = "VectorMath";
|
||||
t.config.outputName = "VectorMath";
|
||||
t.config.target = "x86_64-pc-linux-gnu";
|
||||
t.config.type = ConfigurationType::Executable;
|
||||
std::array<fs::path, 0> ifaces = {};
|
||||
std::array<fs::path, 1> impls = { "tests/VectorMath" };
|
||||
t.config.GetInterfacesAndImplementations(ifaces, impls);
|
||||
cfg.tests.push_back(std::move(t));
|
||||
```
|
||||
|
||||
Each test runs in its own subprocess so a segfault doesn't take the runner down. Default timeout is 60 s per test (`--timeout=N` overrides). Tests run in parallel up to `hardware_concurrency()`; pass `--jobs=N` to override. Per-test stdout+stderr is buffered and printed atomically on completion so parallel output stays readable, and is also written to `build/test-logs/<name>.log` when a test fails, crashes, or times out.
|
||||
|
||||
```bash
|
||||
crafter-build test # build + run all tests
|
||||
crafter-build test 'Vector*' # glob-filter test names
|
||||
crafter-build test --list # enumerate without running
|
||||
crafter-build test --jobs=1 # serial mode
|
||||
crafter-build test --timeout=5 # override per-test timeout
|
||||
```
|
||||
|
||||
Outcomes are reported as ✅ pass, ❌ fail, 💥 crash (with the signal name), or ⏱ timeout. The runner exits 0 only if every test passed.
|
||||
|
||||
### Cross-target test runners
|
||||
|
||||
`Test::runner` runs the test binary somewhere other than this machine. Three factories ship today (Linux host only):
|
||||
|
||||
```cpp
|
||||
t.runner = TestRunner::Local(); // default
|
||||
t.runner = TestRunner::QemuUser("qemu-aarch64"); // cross-arch via qemu user-mode
|
||||
t.runner = TestRunner::Ssh("winvm", "/tmp/crafter-test"); // cross-OS via scp + ssh
|
||||
```
|
||||
|
||||
`TestRunner::Ssh(host, remoteDir)` ships the test's whole `bin/<name>-<target>-<march>/` output dir via `scp -r`, runs the binary over `ssh`, then cleans up the remote temp dir. Each test invocation gets a unique remote subdir to keep parallel runs isolated. Assumes Unix-like shell on the remote (`mkdir -p`, `rm -rf`); for Windows remotes (e.g. `cmd.exe`) construct `TestRunner` directly with your own command templates — `copy`, `exec`, `cleanup` are public string fields with `{bundle}`, `{remote_bundle}`, `{bin}`, `{bin_name}`, `{args}` placeholders.
|
||||
|
||||
`TestRunner::QemuUser(qemuBin)` simply wraps `<qemuBin> {bin} {args}` — same machine, just a command prefix. Pair it with `t.config.target = "aarch64-linux-gnu"` (plus an aarch64 sysroot installed on the host) for cross-arch Linux testing.
|
||||
|
||||
For cross-arch builds, set `cfg.sysroot` on the `Configuration` to point at a sysroot for the target. `BuildStdPcm` and the compile/link commands both consume it: `--sysroot=<path>` is passed to clang, and `<sysroot>/usr/share/libc++/v1/std.cppm` is precompiled instead of the host's.
|
||||
|
||||
For aarch64 specifically, the cheapest sysroot is an extracted [Arch Linux ARM rootfs](http://os.archlinuxarm.org/) plus `libc++` + `libc++abi` + `gcc` installed into it via `pacman --root <rootfs> --config <alarm-pacman.conf> --dbpath <rootfs>/var/lib/pacman -Sy …` (point pacman at ALARM's mirrors with an explicit config to avoid pulling host-arch packages). Then `cfg.sysroot = "<rootfs>";` plus a manual `TestRunner` whose exec template prepends `QEMU_LD_PREFIX=<rootfs> qemu-aarch64 …` so qemu-user finds `ld-linux-aarch64.so.1`. See `tests/fixtures/cross-arch-aarch64/` for a working example.
|
||||
|
||||
For `t.config.target = "x86_64-w64-mingw32"` (Windows from Linux) — V2's existing build pipeline cross-compiles on Linux as long as `mingw-w64` is installed. Combine with `TestRunner::Ssh("winvm", ...)` for end-to-end Windows-target testing from a Linux host. Full MSVC ABI (`x86_64-pc-windows-msvc`) cross-compile from Linux is not yet wired; use MinGW for now.
|
||||
|
||||
#### Global config: `TestRunner::FromEnv`
|
||||
|
||||
To avoid hardcoding hosts in every project's `project.cpp`, use `TestRunner::FromEnv(target)`:
|
||||
|
||||
```cpp
|
||||
t.runner = TestRunner::FromEnv(t.config.target);
|
||||
```
|
||||
|
||||
This reads the env var `CRAFTER_BUILD_RUNNER_<NORMALIZED_TARGET>` (target triple with `-` and `.` replaced by `_`). Set it once per shell or in CI:
|
||||
|
||||
```bash
|
||||
export CRAFTER_BUILD_RUNNER_x86_64_w64_mingw32=ssh:winvm:/tmp/crafter-test
|
||||
export CRAFTER_BUILD_RUNNER_aarch64_linux_gnu=qemu:qemu-aarch64
|
||||
```
|
||||
|
||||
Spec grammar: `local`, `qemu:<qemu-bin>`, `ssh:<host>`, `ssh:<host>:<remoteDir>`, `sshwin:<host>`, `sshwin:<host>:<remoteDir>` (cmd.exe-shell variant for Windows remotes). Unset → falls back to the second arg of `FromEnv` (defaults to `Local()`).
|
||||
|
||||
For anything more elaborate (rsync, ssh ControlMaster, custom flags, env vars on the spawn, `QEMU_LD_PREFIX` for sysroots) — construct `TestRunner` manually with your own templates. The factories are sugar over the same fields.
|
||||
|
||||
Not yet supported: WSL2 / Windows-host execution (the runner currently requires a Linux host because `RunCommandWithTimeout` is Linux-only), MSVC ABI cross-compile from Linux.
|
||||
|
||||
#### Skipping unreachable runners
|
||||
|
||||
`TestRunner` carries a `probe` command that runs once per `RunTests` invocation (cached by `runner.name`); if it exits non-zero, every Test using that runner reports as `⏭ Skipped: runner '<name>' not available` and the suite continues with everything else. The factories set sensible defaults: `Ssh`/`SshWin` probe via `ssh -q -o BatchMode=yes -o ConnectTimeout=5 <host> …`, `QemuUser` via `which <qemu-bin>`, `Local` is unconditionally available.
|
||||
|
||||
Effect: if you don't have a Windows VM at all, you don't have to do anything — the Windows variants in the in-tree suite skip cleanly and the Linux variants pass. If you do have one, set up an SSH host alias matching what the project.cpp expects (in this repo: `winvm` in `~/.ssh/config`) and the Windows variants run automatically. Skipped tests count toward the summary but do not fail the suite (`AllPassed()` returns true on skip-only runs).
|
||||
|
||||
#### Per-(target, runner) tests
|
||||
|
||||
The 5 simple tests `HelloWorld`, `WithModule`, `Defines`, `CrossProjectModule`, `Diamond` are declared **directly** in the project's `project.cpp`, one entry per `(target, runner)` pair:
|
||||
|
||||
```cpp
|
||||
std::vector<TargetRunner> targets = {
|
||||
{ "x86_64-pc-linux-gnu", TestRunner::Local(), {} },
|
||||
{ "x86_64-w64-mingw32", TestRunner::SshWin("winvm", "C:/temp/crafter-tests"), {"-lstdc++exp"} },
|
||||
};
|
||||
```
|
||||
|
||||
So `./bin/crafter-build test` (no env vars) spawns one Test per `(target, runner)` pair, all in one report. With the `winvm` SSH alias reachable:
|
||||
|
||||
```
|
||||
✅ HelloWorld (5ms)
|
||||
✅ HelloWorld (sshwin:winvm) (2348ms)
|
||||
✅ WithModule (3ms)
|
||||
✅ WithModule (sshwin:winvm) (2204ms)
|
||||
…
|
||||
18 passed
|
||||
```
|
||||
|
||||
Without `winvm` reachable, the SshWin variants skip:
|
||||
|
||||
```
|
||||
✅ HelloWorld (5ms)
|
||||
⏭ HelloWorld (sshwin:winvm) skipped: runner 'sshwin:winvm' not available
|
||||
…
|
||||
```
|
||||
|
||||
Local runner emits no parenthetical (existing default for the common case); cross-target runners show their name (e.g. `sshwin:winvm`, `qemu:qemu-aarch64`) so you see at a glance which environment ran each test.
|
||||
|
||||
The 8 outer-driver tests (Incremental, BuildError, RunnerClassification, QemuUser, SshRunner, WindowsViaSsh, CrossArchAarch64, Libraries) stay one-line in the report because they're meta-tests — the test driver is the test, validating host-side build behavior or runner integration. They're not multi-target.
|
||||
|
||||
## Architecture
|
||||
|
||||
- **Modules**: `Crafter.Build:Shader` / `:Platform` / `:Interface` / `:Implementation` / `:External` / `:Clang` partitions, re-exported by the `Crafter.Build` umbrella.
|
||||
- **Modules**: `Crafter.Build:Shader` / `:Platform` / `:Interface` / `:Implementation` / `:External` / `:Clang` / `:Test` partitions, re-exported by the `Crafter.Build` umbrella.
|
||||
- **Build process**: parallel — interface PCMs, implementation `.o` files, external dep clones, dep-config recursive builds, and shader compilations all spawn threads, sync at well-defined join points.
|
||||
- **`LoadProject`** compiles `project.cpp` to `<project>/build/project.so` and `dlopen`s it. The host exe is linked with `-Wl,--export-dynamic` so the project's undefined symbols resolve against the running binary.
|
||||
- **`LoadProject`** compiles `project.cpp` to a shared object (`.so` on Linux, `.dll` on Windows) and loads it. On Linux the host exe is linked with `-Wl,--export-dynamic` so the project's undefined symbols resolve back into the running binary. On Windows the host is split into `crafter-build.dll` (everything) + a thin `crafter-build.exe` launcher; the public `Crafter::*` API is annotated with a `CRAFTER_API` macro (`__declspec(dllexport)` when the host is built, `__declspec(dllimport)` when project.dll consumes it via the rebuilt user-cache PCMs), and the host passes `-Wl,/EXPORT:CrafterBuildProject` when linking project.dll so user `project.cpp` files don't need platform-gated annotations.
|
||||
|
||||
## Compatibility / portability
|
||||
|
||||
- Linux: x86_64. Tested on Arch in a clean container.
|
||||
- Windows: bootstrap works (build.cmd produces working exe); the `LoadProject` path needs the in-progress DLL+launcher refactor to support cross-EXE/DLL symbol resolution.
|
||||
- Windows: x86_64 MSVC ABI. `build.cmd` produces `bin/crafter-build.{exe,dll,lib}`; userprojs verified end-to-end (`project.cpp` → `project.dll` loaded by the host → `userapp.exe` built and run).
|
||||
|
||||
The `CRAFTER_BUILD_MARCH` and `CRAFTER_BUILD_MTUNE` env vars override the default `-march=native` / `-mtune=native` for distro CI builds:
|
||||
|
||||
|
|
@ -114,9 +229,6 @@ CRAFTER_BUILD_MARCH=x86-64-v3 CRAFTER_BUILD_MTUNE=generic ./build.sh
|
|||
|
||||
## Roadmap
|
||||
|
||||
- [ ] Windows DLL+launcher refactor (in progress)
|
||||
- [ ] Configuration inheritance (`extends:`-style merging from base configs)
|
||||
- [ ] Test runner (dlopen-based test loading)
|
||||
- [ ] Target-triple auto-detection (`clang -print-target-triple`)
|
||||
- [ ] Prefer system glslang when available, fall back to git+cmake
|
||||
- [ ] Order-preserving deduplication of `BuildResult.libs` (current `unordered_set` doesn't preserve link order — only matters for cyclic static libs)
|
||||
|
|
|
|||
26
build.cmd
26
build.cmd
|
|
@ -1,6 +1,7 @@
|
|||
@echo OFF
|
||||
mkdir build 2>nul
|
||||
mkdir bin 2>nul
|
||||
mkdir lib 2>nul
|
||||
mkdir share\crafter-build 2>nul
|
||||
|
||||
copy /Y interfaces\Crafter.Build.cppm share\crafter-build\
|
||||
|
|
@ -10,6 +11,8 @@ copy /Y interfaces\Crafter.Build-Interface.cppm share\crafter-build\
|
|||
copy /Y interfaces\Crafter.Build-Implementation.cppm share\crafter-build\
|
||||
copy /Y interfaces\Crafter.Build-External.cppm share\crafter-build\
|
||||
copy /Y interfaces\Crafter.Build-Clang.cppm share\crafter-build\
|
||||
copy /Y interfaces\Crafter.Build-Test.cppm share\crafter-build\
|
||||
copy /Y interfaces\Crafter.Build-Api.h share\crafter-build\
|
||||
|
||||
if not exist .\build\glslang\NUL git clone https://github.com/KhronosGroup/glslang.git .\build\glslang
|
||||
|
||||
|
|
@ -45,6 +48,7 @@ clang++ %common_options% -fmodule-output interfaces\Crafter.Build-Interface.cppm
|
|||
clang++ %common_options% -fmodule-output interfaces\Crafter.Build-Implementation.cppm -o .\build\Crafter.Build-Implementation.o
|
||||
clang++ %common_options% -fmodule-output interfaces\Crafter.Build-External.cppm -o .\build\Crafter.Build-External.o
|
||||
clang++ %common_options% -fmodule-output interfaces\Crafter.Build-Clang.cppm -o .\build\Crafter.Build-Clang.o
|
||||
clang++ %common_options% -fmodule-output interfaces\Crafter.Build-Test.cppm -o .\build\Crafter.Build-Test.o
|
||||
clang++ %common_options% -fmodule-output interfaces\Crafter.Build.cppm -o .\build\Crafter.Build.o
|
||||
|
||||
clang++ %common_options% .\implementations\Crafter.Build-Shader.cpp -o .\build\Crafter.Build-Shader_impl.o
|
||||
|
|
@ -53,11 +57,11 @@ clang++ %common_options% .\implementations\Crafter.Build-Interface.cpp -o .\buil
|
|||
clang++ %common_options% .\implementations\Crafter.Build-Implementation.cpp -o .\build\Crafter.Build-Implementation_impl.o
|
||||
clang++ %common_options% .\implementations\Crafter.Build-External.cpp -o .\build\Crafter.Build-External_impl.o
|
||||
clang++ %common_options% .\implementations\Crafter.Build-Clang.cpp -o .\build\Crafter.Build-Clang_impl.o
|
||||
clang++ %common_options% .\implementations\Crafter.Build-Test.cpp -o .\build\Crafter.Build-Test_impl.o
|
||||
clang++ %common_options% .\implementations\main.cpp -o .\build\main.o
|
||||
|
||||
REM Step 1: link all impl .o files into crafter-build.dll, generating crafter-build.lib import lib
|
||||
clang++ %useLibcLinker% -shared -std=c++26 -O3 -march=%CRAFTER_BUILD_MARCH% -mtune=%CRAFTER_BUILD_MTUNE% -L.\build -fuse-ld=lld ^
|
||||
-Wl,-export-all-symbols ^
|
||||
-Wl,/IMPLIB:.\bin\crafter-build.lib ^
|
||||
-lSPIRV -lGenericCodeGen -lglslang -lOSDependent -lMachineIndependent -lglslang-default-resource-limits ^
|
||||
.\build\Crafter.Build-Shader.o ^
|
||||
|
|
@ -66,6 +70,7 @@ clang++ %useLibcLinker% -shared -std=c++26 -O3 -march=%CRAFTER_BUILD_MARCH% -mtu
|
|||
.\build\Crafter.Build-Implementation.o ^
|
||||
.\build\Crafter.Build-External.o ^
|
||||
.\build\Crafter.Build-Clang.o ^
|
||||
.\build\Crafter.Build-Test.o ^
|
||||
.\build\Crafter.Build.o ^
|
||||
.\build\Crafter.Build-Shader_impl.o ^
|
||||
.\build\Crafter.Build-Platform_impl.o ^
|
||||
|
|
@ -73,6 +78,7 @@ clang++ %useLibcLinker% -shared -std=c++26 -O3 -march=%CRAFTER_BUILD_MARCH% -mtu
|
|||
.\build\Crafter.Build-Implementation_impl.o ^
|
||||
.\build\Crafter.Build-External_impl.o ^
|
||||
.\build\Crafter.Build-Clang_impl.o ^
|
||||
.\build\Crafter.Build-Test_impl.o ^
|
||||
-o .\bin\crafter-build.dll
|
||||
|
||||
REM Step 2: link the launcher exe against crafter-build.lib
|
||||
|
|
@ -81,4 +87,22 @@ clang++ %useLibcLinker% -std=c++26 -O3 -march=%CRAFTER_BUILD_MARCH% -mtune=%CRAF
|
|||
.\bin\crafter-build.lib ^
|
||||
-o .\bin\crafter-build.exe
|
||||
|
||||
REM Step 3: bundle the same .o set as a static archive for downstream consumers
|
||||
llvm-lib.exe /OUT:.\lib\crafter-build-static.lib ^
|
||||
.\build\Crafter.Build-Shader.o ^
|
||||
.\build\Crafter.Build-Platform.o ^
|
||||
.\build\Crafter.Build-Interface.o ^
|
||||
.\build\Crafter.Build-Implementation.o ^
|
||||
.\build\Crafter.Build-External.o ^
|
||||
.\build\Crafter.Build-Clang.o ^
|
||||
.\build\Crafter.Build-Test.o ^
|
||||
.\build\Crafter.Build.o ^
|
||||
.\build\Crafter.Build-Shader_impl.o ^
|
||||
.\build\Crafter.Build-Platform_impl.o ^
|
||||
.\build\Crafter.Build-Interface_impl.o ^
|
||||
.\build\Crafter.Build-Implementation_impl.o ^
|
||||
.\build\Crafter.Build-External_impl.o ^
|
||||
.\build\Crafter.Build-Clang_impl.o ^
|
||||
.\build\Crafter.Build-Test_impl.o
|
||||
|
||||
copy /Y "%LIBCXX_DIR%\lib\c++.dll" ".\bin\c++.dll"
|
||||
|
|
|
|||
24
build.sh
24
build.sh
|
|
@ -1,5 +1,6 @@
|
|||
mkdir -p build
|
||||
mkdir -p bin
|
||||
mkdir -p lib
|
||||
mkdir -p share/crafter-build
|
||||
|
||||
cp interfaces/Crafter.Build.cppm share/crafter-build/
|
||||
|
|
@ -9,6 +10,8 @@ cp interfaces/Crafter.Build-Interface.cppm share/crafter-build/
|
|||
cp interfaces/Crafter.Build-Implementation.cppm share/crafter-build/
|
||||
cp interfaces/Crafter.Build-External.cppm share/crafter-build/
|
||||
cp interfaces/Crafter.Build-Clang.cppm share/crafter-build/
|
||||
cp interfaces/Crafter.Build-Test.cppm share/crafter-build/
|
||||
cp interfaces/Crafter.Build-Api.h share/crafter-build/
|
||||
|
||||
git clone https://github.com/KhronosGroup/glslang.git ./build/glslang
|
||||
|
||||
|
|
@ -42,6 +45,7 @@ clang++ $common_options -fmodule-output interfaces/Crafter.Build-Interface.cppm
|
|||
clang++ $common_options -fmodule-output interfaces/Crafter.Build-Implementation.cppm -o ./build/Crafter.Build-Implementation.o
|
||||
clang++ $common_options -fmodule-output interfaces/Crafter.Build-External.cppm -o ./build/Crafter.Build-External.o
|
||||
clang++ $common_options -fmodule-output interfaces/Crafter.Build-Clang.cppm -o ./build/Crafter.Build-Clang.o
|
||||
clang++ $common_options -fmodule-output interfaces/Crafter.Build-Test.cppm -o ./build/Crafter.Build-Test.o
|
||||
clang++ $common_options -fmodule-output interfaces/Crafter.Build.cppm -o ./build/Crafter.Build.o
|
||||
|
||||
clang++ $common_options ./implementations/Crafter.Build-Shader.cpp -o ./build/Crafter.Build-Shader_impl.o
|
||||
|
|
@ -50,8 +54,26 @@ clang++ $common_options ./implementations/Crafter.Build-Interface.cpp -o ./build
|
|||
clang++ $common_options ./implementations/Crafter.Build-Implementation.cpp -o ./build/Crafter.Build-Implementation_impl.o
|
||||
clang++ $common_options ./implementations/Crafter.Build-External.cpp -o ./build/Crafter.Build-External_impl.o
|
||||
clang++ $common_options ./implementations/Crafter.Build-Clang.cpp -o ./build/Crafter.Build-Clang_impl.o
|
||||
clang++ $common_options ./implementations/Crafter.Build-Test.cpp -o ./build/Crafter.Build-Test_impl.o
|
||||
clang++ $common_options ./implementations/main.cpp -o ./build/main.o
|
||||
|
||||
ar rcs ./lib/libcrafter-build.a \
|
||||
./build/Crafter.Build-Shader.o \
|
||||
./build/Crafter.Build-Platform.o \
|
||||
./build/Crafter.Build-Interface.o \
|
||||
./build/Crafter.Build-Implementation.o \
|
||||
./build/Crafter.Build-External.o \
|
||||
./build/Crafter.Build-Clang.o \
|
||||
./build/Crafter.Build-Test.o \
|
||||
./build/Crafter.Build.o \
|
||||
./build/Crafter.Build-Shader_impl.o \
|
||||
./build/Crafter.Build-Platform_impl.o \
|
||||
./build/Crafter.Build-Interface_impl.o \
|
||||
./build/Crafter.Build-Implementation_impl.o \
|
||||
./build/Crafter.Build-External_impl.o \
|
||||
./build/Crafter.Build-Clang_impl.o \
|
||||
./build/Crafter.Build-Test_impl.o
|
||||
|
||||
clang++ -std=c++26 -stdlib=libc++ -O3 -march=$MARCH -mtune=$MTUNE -fuse-ld=lld \
|
||||
-Wl,--export-dynamic \
|
||||
-L./build \
|
||||
|
|
@ -61,6 +83,7 @@ clang++ -std=c++26 -stdlib=libc++ -O3 -march=$MARCH -mtune=$MTUNE -fuse-ld=lld \
|
|||
./build/Crafter.Build-Implementation.o \
|
||||
./build/Crafter.Build-External.o \
|
||||
./build/Crafter.Build-Clang.o \
|
||||
./build/Crafter.Build-Test.o \
|
||||
./build/Crafter.Build.o \
|
||||
./build/Crafter.Build-Shader_impl.o \
|
||||
./build/Crafter.Build-Platform_impl.o \
|
||||
|
|
@ -68,6 +91,7 @@ clang++ -std=c++26 -stdlib=libc++ -O3 -march=$MARCH -mtune=$MTUNE -fuse-ld=lld \
|
|||
./build/Crafter.Build-Implementation_impl.o \
|
||||
./build/Crafter.Build-External_impl.o \
|
||||
./build/Crafter.Build-Clang_impl.o \
|
||||
./build/Crafter.Build-Test_impl.o \
|
||||
./build/main.o \
|
||||
-lSPIRV -lGenericCodeGen -lglslang -lOSDependent -lMachineIndependent -lglslang-default-resource-limits \
|
||||
-ldl \
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ export module Crafter.Build:Clang_impl;
|
|||
import std;
|
||||
import :Clang;
|
||||
import :Platform;
|
||||
import :Test;
|
||||
namespace fs = std::filesystem;
|
||||
using namespace Crafter;
|
||||
|
||||
|
|
@ -35,7 +36,10 @@ void Configuration::GetInterfacesAndImplementations(std::span<fs::path> interfac
|
|||
return true;
|
||||
}
|
||||
}
|
||||
for(Configuration* depCfg : this->dependencies) {
|
||||
|
||||
std::unordered_set<Configuration*> seen;
|
||||
std::function<bool(Configuration*)> walk = [&](Configuration* depCfg) -> bool {
|
||||
if (!seen.insert(depCfg).second) return false;
|
||||
for(const std::unique_ptr<Module>& depInterface : depCfg->interfaces) {
|
||||
if(depInterface->name == importName) {
|
||||
fs::path depPcmPath = (depCfg->PcmDir() / depInterface->path.filename()).string() + ".pcm";
|
||||
|
|
@ -43,6 +47,14 @@ void Configuration::GetInterfacesAndImplementations(std::span<fs::path> interfac
|
|||
return true;
|
||||
}
|
||||
}
|
||||
for(Configuration* sub : depCfg->dependencies) {
|
||||
if (walk(sub)) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
for(Configuration* depCfg : this->dependencies) {
|
||||
if (walk(depCfg)) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
|
@ -314,6 +326,10 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
|||
|
||||
std::string command = std::format("{} --target={} -march={} -mtune={} -std=c++26 -D CRAFTER_BUILD_CONFIGURATION_TARGET=\\\"{}\\\" -D CRAFTER_BUILD_CONFIGURATION_TARGET_{} -fprebuilt-module-path={} -fprebuilt-module-path={}", GetBaseCommand(config), config.target, config.march, config.mtune, editedTarget, editedTarget, stdPcmDir.string(), pcmDir.string());
|
||||
|
||||
if (!config.sysroot.empty()) {
|
||||
command += std::format(" --sysroot={}", config.sysroot);
|
||||
}
|
||||
|
||||
if(config.type == ConfigurationType::LibraryDynamic) {
|
||||
#ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu
|
||||
command += " -fPIC -D CRAFTER_BUILD_CONFIGURATION_TYPE_SHARED_LIBRARY";
|
||||
|
|
@ -334,15 +350,26 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
|||
depThreads.reserve(config.dependencies.size());
|
||||
std::atomic<bool> repack(false);
|
||||
|
||||
for(Configuration* dep : config.dependencies) {
|
||||
{
|
||||
std::unordered_set<Configuration*> seen;
|
||||
std::function<void(Configuration*)> addFlags = [&](Configuration* dep) {
|
||||
if (!seen.insert(dep).second) return;
|
||||
for (const auto& entry : fs::recursive_directory_iterator(dep->path)) {
|
||||
if (entry.is_directory() && entry.path().filename() == "include") {
|
||||
command += " -I" + entry.path().string();
|
||||
}
|
||||
}
|
||||
|
||||
command += std::format(" -I{} -fprebuilt-module-path={}", dep->path.string(), dep->PcmDir().string());
|
||||
for (Configuration* sub : dep->dependencies) {
|
||||
addFlags(sub);
|
||||
}
|
||||
};
|
||||
for (Configuration* dep : config.dependencies) {
|
||||
addFlags(dep);
|
||||
}
|
||||
}
|
||||
|
||||
for(Configuration* dep : config.dependencies) {
|
||||
depThreads.emplace_back([&, dep](){
|
||||
try {
|
||||
if (buildCancelled.load(std::memory_order_relaxed)) return;
|
||||
|
|
@ -352,12 +379,13 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
|||
bool isBuilder = false;
|
||||
|
||||
depMutex.lock();
|
||||
auto it = depResults.find(dep->path);
|
||||
fs::path cacheKey = dep->PcmDir();
|
||||
auto it = depResults.find(cacheKey);
|
||||
if (it == depResults.end()) {
|
||||
isBuilder = true;
|
||||
promise = std::make_shared<std::promise<BuildResult>>();
|
||||
resultFuture = promise->get_future().share();
|
||||
depResults.emplace(dep->path, resultFuture);
|
||||
depResults.emplace(cacheKey, resultFuture);
|
||||
} else {
|
||||
resultFuture = it->second;
|
||||
}
|
||||
|
|
@ -594,10 +622,23 @@ int Crafter::Run(int argc, char** argv) {
|
|||
std::vector<std::string_view> projectArgs;
|
||||
projectArgs.reserve(argc);
|
||||
|
||||
bool runTests = false;
|
||||
RunTestsOptions testOpts;
|
||||
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
std::string_view arg = argv[i];
|
||||
if (arg.starts_with("--project=")) {
|
||||
if (arg == "test") {
|
||||
runTests = true;
|
||||
} else if (arg.starts_with("--project=")) {
|
||||
projectFile = arg.substr(std::string_view("--project=").size());
|
||||
} else if (runTests && arg.starts_with("--jobs=")) {
|
||||
testOpts.jobs = std::stoi(std::string(arg.substr(std::string_view("--jobs=").size())));
|
||||
} else if (runTests && arg.starts_with("--timeout=")) {
|
||||
testOpts.timeoutOverride = std::chrono::seconds(std::stoi(std::string(arg.substr(std::string_view("--timeout=").size()))));
|
||||
} else if (runTests && arg == "--list") {
|
||||
testOpts.listOnly = true;
|
||||
} else if (runTests && !arg.starts_with("-")) {
|
||||
testOpts.globs.emplace_back(arg);
|
||||
} else {
|
||||
projectArgs.push_back(arg);
|
||||
}
|
||||
|
|
@ -610,6 +651,11 @@ int Crafter::Run(int argc, char** argv) {
|
|||
|
||||
Configuration config = LoadProject(projectFile, projectArgs);
|
||||
|
||||
if (runTests) {
|
||||
TestSummary summary = RunTests(config, testOpts);
|
||||
return summary.AllPassed() ? 0 : 1;
|
||||
}
|
||||
|
||||
std::unordered_map<fs::path, std::shared_future<BuildResult>> depResults;
|
||||
std::mutex depMutex;
|
||||
BuildResult result = Build(config, depResults, depMutex);
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ std::string Crafter::RunCommand(const std::string_view cmd) {
|
|||
|
||||
CommandResult Crafter::RunCommandChecked(std::string_view cmd) {
|
||||
std::array<char, 128> buffer;
|
||||
CommandResult result{0, ""};
|
||||
CommandResult result{};
|
||||
|
||||
std::string with = "cmd /C \"" + std::string(cmd) + " 2>&1\"";
|
||||
|
||||
|
|
@ -76,6 +76,10 @@ CommandResult Crafter::RunCommandChecked(std::string_view cmd) {
|
|||
return result;
|
||||
}
|
||||
|
||||
CommandResult Crafter::RunCommandWithTimeout(std::string_view, std::chrono::seconds) {
|
||||
throw std::runtime_error("RunCommandWithTimeout not yet implemented on Windows");
|
||||
}
|
||||
|
||||
std::string Crafter::BuildStdPcm(const Configuration& config, fs::path stdPcm) {
|
||||
std::string libcxx = std::getenv("LIBCXX_DIR");
|
||||
std::string stdcppm = std::format("{}\\modules\\c++\\v1\\std.cppm", libcxx);
|
||||
|
|
@ -99,13 +103,14 @@ std::string Crafter::GetBaseCommand(const Configuration& config) {
|
|||
}
|
||||
|
||||
namespace {
|
||||
constexpr std::array<std::string_view, 7> kCrafterBuildModules = {
|
||||
constexpr std::array<std::string_view, 8> kCrafterBuildModules = {
|
||||
"Crafter.Build-Shader",
|
||||
"Crafter.Build-Platform",
|
||||
"Crafter.Build-Interface",
|
||||
"Crafter.Build-Implementation",
|
||||
"Crafter.Build-External",
|
||||
"Crafter.Build-Clang",
|
||||
"Crafter.Build-Test",
|
||||
"Crafter.Build",
|
||||
};
|
||||
|
||||
|
|
@ -121,7 +126,7 @@ namespace {
|
|||
}
|
||||
std::string cmd = std::format(
|
||||
"clang++ --target=x86_64-pc-windows-msvc -march=native -mtune=native "
|
||||
"-std=c++26 -O3 "
|
||||
"-std=c++26 -O3 -D CRAFTER_BUILD_DLL_IMPORT "
|
||||
"-nostdinc++ -nostdlib++ -isystem %LIBCXX_DIR%\\include\\c++\\v1 "
|
||||
"-Wno-reserved-identifier -Wno-reserved-module-identifier "
|
||||
"-fprebuilt-module-path={} "
|
||||
|
|
@ -178,9 +183,11 @@ Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const
|
|||
|
||||
std::string compileCmd = std::format(
|
||||
"clang++ --target=x86_64-pc-windows-msvc -march=native -mtune=native "
|
||||
"-std=c++26 -shared -O3 -Wno-return-type-c-linkage "
|
||||
"-std=c++26 -shared -O3 -Wno-return-type-c-linkage -fuse-ld=lld "
|
||||
"-D CRAFTER_BUILD_DLL_IMPORT "
|
||||
"-nostdinc++ -nostdlib++ -isystem %LIBCXX_DIR%\\include\\c++\\v1 "
|
||||
"-fprebuilt-module-path={} "
|
||||
"-Wl,/EXPORT:CrafterBuildProject "
|
||||
"{} {} -o {} -L %LIBCXX_DIR%\\lib -lc++",
|
||||
cacheDir.string(),
|
||||
absProject.string(), crafterBuildLib.string(), dllPath.string());
|
||||
|
|
@ -229,7 +236,7 @@ std::string Crafter::RunCommand(const std::string_view cmd) {
|
|||
|
||||
CommandResult Crafter::RunCommandChecked(std::string_view cmd) {
|
||||
std::array<char, 128> buffer;
|
||||
CommandResult result{0, ""};
|
||||
CommandResult result{};
|
||||
|
||||
std::string with = std::string(cmd) + " 2>&1";
|
||||
FILE* pipe = popen(with.c_str(), "r");
|
||||
|
|
@ -243,7 +250,47 @@ CommandResult Crafter::RunCommandChecked(std::string_view cmd) {
|
|||
if (WIFEXITED(status)) {
|
||||
result.exitCode = WEXITSTATUS(status);
|
||||
} else if (WIFSIGNALED(status)) {
|
||||
result.exitCode = 128 + WTERMSIG(status);
|
||||
result.signal = WTERMSIG(status);
|
||||
result.exitCode = 128 + result.signal;
|
||||
result.crashed = true;
|
||||
} else {
|
||||
result.exitCode = -1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
CommandResult Crafter::RunCommandWithTimeout(std::string_view cmd, std::chrono::seconds timeout) {
|
||||
std::array<char, 128> buffer;
|
||||
CommandResult result{};
|
||||
|
||||
std::string wrapped = std::format(
|
||||
"timeout --kill-after=2 {} {} 2>&1",
|
||||
timeout.count(), cmd);
|
||||
|
||||
FILE* pipe = popen(wrapped.c_str(), "r");
|
||||
if (!pipe) throw std::runtime_error("popen() failed!");
|
||||
|
||||
while (fgets(buffer.data(), buffer.size(), pipe) != nullptr) {
|
||||
result.output += buffer.data();
|
||||
}
|
||||
|
||||
int status = pclose(pipe);
|
||||
if (WIFEXITED(status)) {
|
||||
int code = WEXITSTATUS(status);
|
||||
if (code == 124) {
|
||||
result.timedOut = true;
|
||||
result.exitCode = 124;
|
||||
} else if (code >= 128) {
|
||||
result.signal = code - 128;
|
||||
result.exitCode = code;
|
||||
result.crashed = true;
|
||||
} else {
|
||||
result.exitCode = code;
|
||||
}
|
||||
} else if (WIFSIGNALED(status)) {
|
||||
result.signal = WTERMSIG(status);
|
||||
result.exitCode = 128 + result.signal;
|
||||
result.crashed = true;
|
||||
} else {
|
||||
result.exitCode = -1;
|
||||
}
|
||||
|
|
@ -276,8 +323,14 @@ std::string Crafter::BuildStdPcm(const Configuration& config, fs::path stdPcm) {
|
|||
return "";
|
||||
}
|
||||
} else {
|
||||
if(!fs::exists(stdPcm) || fs::last_write_time(stdPcm) < fs::last_write_time("/usr/share/libc++/v1/std.cppm")) {
|
||||
return RunCommand(std::format("clang++ --target={} -std=c++26 -stdlib=libc++ -march={} -mtune={} -O3 -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile /usr/share/libc++/v1/std.cppm -o {}", config.target, config.march, config.mtune, stdPcm.string()));
|
||||
std::string stdCppm = config.sysroot.empty()
|
||||
? std::string("/usr/share/libc++/v1/std.cppm")
|
||||
: std::format("{}/usr/share/libc++/v1/std.cppm", config.sysroot);
|
||||
std::string sysrootFlag = config.sysroot.empty()
|
||||
? std::string()
|
||||
: std::format(" --sysroot={}", config.sysroot);
|
||||
if(!fs::exists(stdPcm) || fs::last_write_time(stdPcm) < fs::last_write_time(stdCppm)) {
|
||||
return RunCommand(std::format("clang++ --target={} -std=c++26 -stdlib=libc++{} -march={} -mtune={} -O3 -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile {} -o {}", config.target, sysrootFlag, config.march, config.mtune, stdCppm, stdPcm.string()));
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
|
|
@ -307,13 +360,14 @@ std::string Crafter::GetBaseCommand(const Configuration& config) {
|
|||
}
|
||||
|
||||
namespace {
|
||||
constexpr std::array<std::string_view, 7> kCrafterBuildModules = {
|
||||
constexpr std::array<std::string_view, 8> kCrafterBuildModules = {
|
||||
"Crafter.Build-Shader",
|
||||
"Crafter.Build-Platform",
|
||||
"Crafter.Build-Interface",
|
||||
"Crafter.Build-Implementation",
|
||||
"Crafter.Build-External",
|
||||
"Crafter.Build-Clang",
|
||||
"Crafter.Build-Test",
|
||||
"Crafter.Build",
|
||||
};
|
||||
|
||||
|
|
|
|||
475
implementations/Crafter.Build-Test.cpp
Normal file
475
implementations/Crafter.Build-Test.cpp
Normal file
|
|
@ -0,0 +1,475 @@
|
|||
/*
|
||||
Crafter® Build
|
||||
Copyright (C) 2026 Catcrafts®
|
||||
Catcrafts.net
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License version 3.0 as published by the Free Software Foundation;
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
export module Crafter.Build:Test_impl;
|
||||
import std;
|
||||
import :Test;
|
||||
import :Clang;
|
||||
import :Platform;
|
||||
namespace fs = std::filesystem;
|
||||
using namespace Crafter;
|
||||
|
||||
namespace {
|
||||
bool TargetIsWindows(std::string_view target) {
|
||||
return target.find("windows") != std::string_view::npos
|
||||
|| target.find("mingw") != std::string_view::npos;
|
||||
}
|
||||
|
||||
fs::path TestBinaryPath(const Configuration& cfg) {
|
||||
fs::path outputDir = cfg.path/"bin"/std::format("{}-{}-{}", cfg.name, cfg.target, cfg.march);
|
||||
return outputDir / (TargetIsWindows(cfg.target) ? cfg.outputName + ".exe" : cfg.outputName);
|
||||
}
|
||||
|
||||
bool MatchGlob(std::string_view glob, std::string_view name) {
|
||||
std::size_t gi = 0, ni = 0, star = std::string_view::npos, mark = 0;
|
||||
while (ni < name.size()) {
|
||||
if (gi < glob.size() && (glob[gi] == '?' || glob[gi] == name[ni])) {
|
||||
++gi; ++ni;
|
||||
} else if (gi < glob.size() && glob[gi] == '*') {
|
||||
star = gi++;
|
||||
mark = ni;
|
||||
} else if (star != std::string_view::npos) {
|
||||
gi = star + 1;
|
||||
ni = ++mark;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
while (gi < glob.size() && glob[gi] == '*') ++gi;
|
||||
return gi == glob.size();
|
||||
}
|
||||
|
||||
bool MatchAny(std::span<const std::string> globs, std::string_view name) {
|
||||
if (globs.empty()) return true;
|
||||
for (const auto& g : globs) {
|
||||
if (MatchGlob(g, name)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string ShellQuote(std::string_view s) {
|
||||
std::string out;
|
||||
out.reserve(s.size() + 2);
|
||||
out.push_back('\'');
|
||||
for (char c : s) {
|
||||
if (c == '\'') out += "'\\''";
|
||||
else out.push_back(c);
|
||||
}
|
||||
out.push_back('\'');
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string JoinAndQuoteArgs(std::span<const std::string> args) {
|
||||
std::string out;
|
||||
for (const auto& a : args) {
|
||||
if (!out.empty()) out.push_back(' ');
|
||||
out += ShellQuote(a);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string Substitute(std::string_view tmpl, const std::map<std::string, std::string>& ph) {
|
||||
std::string out;
|
||||
out.reserve(tmpl.size());
|
||||
std::size_t i = 0;
|
||||
while (i < tmpl.size()) {
|
||||
if (tmpl[i] == '{') {
|
||||
std::size_t end = tmpl.find('}', i + 1);
|
||||
if (end != std::string_view::npos) {
|
||||
std::string key(tmpl.substr(i, end - i + 1));
|
||||
if (auto it = ph.find(key); it != ph.end()) {
|
||||
out += it->second;
|
||||
i = end + 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
out.push_back(tmpl[i++]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string SignalName(int sig) {
|
||||
switch (sig) {
|
||||
case 1: return "SIGHUP";
|
||||
case 2: return "SIGINT";
|
||||
case 4: return "SIGILL";
|
||||
case 6: return "SIGABRT";
|
||||
case 8: return "SIGFPE";
|
||||
case 9: return "SIGKILL";
|
||||
case 11: return "SIGSEGV";
|
||||
case 13: return "SIGPIPE";
|
||||
case 14: return "SIGALRM";
|
||||
case 15: return "SIGTERM";
|
||||
default: return std::format("signal {}", sig);
|
||||
}
|
||||
}
|
||||
|
||||
void WriteLog(const fs::path& projectPath, const std::string& name, const std::string& output) {
|
||||
fs::path logDir = projectPath / "build" / "test-logs";
|
||||
std::error_code ec;
|
||||
fs::create_directories(logDir, ec);
|
||||
if (ec) return;
|
||||
std::ofstream(logDir / (name + ".log")) << output;
|
||||
}
|
||||
|
||||
void PrintResult(const TestResult& r, std::string_view runnerName) {
|
||||
auto ms = r.duration.count();
|
||||
std::string runnerSuffix = (runnerName.empty() || runnerName == "local")
|
||||
? std::string()
|
||||
: std::format(" ({})", runnerName);
|
||||
switch (r.outcome) {
|
||||
case TestOutcome::Pass:
|
||||
std::println("✅ {}{} ({}ms)", r.name, runnerSuffix, ms);
|
||||
break;
|
||||
case TestOutcome::Fail:
|
||||
std::println("❌ {}{} ({}ms) exit {}", r.name, runnerSuffix, ms, r.exitCode);
|
||||
if (!r.output.empty()) {
|
||||
for (auto line : std::views::split(r.output, '\n')) {
|
||||
std::string_view sv(line.begin(), line.end());
|
||||
if (!sv.empty()) std::println(" {}", sv);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case TestOutcome::Crash:
|
||||
std::println("\U0001F4A5 {}{} ({}ms) crashed: {}", r.name, runnerSuffix, ms, SignalName(r.signal));
|
||||
if (!r.output.empty()) {
|
||||
for (auto line : std::views::split(r.output, '\n')) {
|
||||
std::string_view sv(line.begin(), line.end());
|
||||
if (!sv.empty()) std::println(" {}", sv);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case TestOutcome::Timeout:
|
||||
std::println("⏱ {}{} ({}ms) timeout", r.name, runnerSuffix, ms);
|
||||
break;
|
||||
case TestOutcome::Skipped:
|
||||
std::println("⏭ {}{} skipped: {}", r.name, runnerSuffix,
|
||||
r.output.empty() ? std::string("(no reason)") : r.output);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TestRunner TestRunner::Local() {
|
||||
TestRunner r;
|
||||
r.name = "local";
|
||||
return r;
|
||||
}
|
||||
|
||||
TestRunner TestRunner::Ssh(std::string host, std::string remoteDir) {
|
||||
TestRunner r;
|
||||
r.name = std::format("ssh:{}", host);
|
||||
r.remoteDir = std::move(remoteDir);
|
||||
r.copy = std::format("ssh -q {0} 'mkdir -p {{remote_bundle}}' && scp -r -q {{bundle}}/. {0}:{{remote_bundle}}/", host);
|
||||
r.exec = std::format("ssh -q {} 'cd {{remote_bundle}} && ./{{bin_name}} {{args}}'", host);
|
||||
r.cleanup = std::format("ssh -q {} 'rm -rf {{remote_bundle}}' || true", host);
|
||||
r.probe = std::format("ssh -q -o BatchMode=yes -o ConnectTimeout=5 {} true > /dev/null 2>&1", host);
|
||||
return r;
|
||||
}
|
||||
|
||||
TestRunner TestRunner::SshWin(std::string host, std::string remoteDir) {
|
||||
TestRunner r;
|
||||
r.name = std::format("sshwin:{}", host);
|
||||
r.remoteDir = std::move(remoteDir);
|
||||
// cmd.exe-friendly templates. Use backslash variants of remote paths because
|
||||
// cmd's mkdir won't auto-create intermediate directories with forward
|
||||
// slashes. scp tolerates either, so we keep forward slashes for the scp
|
||||
// dest (which `{remote_bundle}` provides). 2>nul + `& rem ok` swallows
|
||||
// mkdir's "already exists" error and forces exit code 0.
|
||||
r.copy = std::format("ssh -q {0} \"mkdir {{remote_bundle_win}} 2>nul & rem ok\" && scp -r -q {{bundle}}/. {0}:{{remote_bundle}}/", host);
|
||||
r.exec = std::format("ssh -q {} \"{{bin_win}} {{args}}\"", host);
|
||||
r.cleanup = std::format("ssh -q {} \"rd /s /q {{remote_bundle_win}} 2>nul & rem ok\"", host);
|
||||
r.probe = std::format("ssh -q -o BatchMode=yes -o ConnectTimeout=5 {} \"ver\" > /dev/null 2>&1", host);
|
||||
return r;
|
||||
}
|
||||
|
||||
TestRunner TestRunner::QemuUser(std::string qemuBin) {
|
||||
TestRunner r;
|
||||
r.name = std::format("qemu:{}", qemuBin);
|
||||
r.exec = std::format("{} {{bin}} {{args}}", qemuBin);
|
||||
r.probe = std::format("which {} > /dev/null 2>&1", qemuBin);
|
||||
return r;
|
||||
}
|
||||
|
||||
namespace {
|
||||
std::string NormalizeTriple(std::string_view target) {
|
||||
std::string out(target);
|
||||
for (char& c : out) {
|
||||
if (c == '-' || c == '.') c = '_';
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::optional<TestRunner> ParseRunnerSpec(std::string_view spec) {
|
||||
if (spec.empty()) return std::nullopt;
|
||||
std::vector<std::string> parts;
|
||||
for (auto piece : std::views::split(spec, ':')) {
|
||||
parts.emplace_back(std::string_view(piece.begin(), piece.end()));
|
||||
}
|
||||
if (parts.empty()) return std::nullopt;
|
||||
if (parts[0] == "local") return TestRunner::Local();
|
||||
if (parts[0] == "qemu" && parts.size() == 2) return TestRunner::QemuUser(parts[1]);
|
||||
if (parts[0] == "ssh" && parts.size() == 2) return TestRunner::Ssh(parts[1]);
|
||||
if (parts[0] == "ssh" && parts.size() >= 3) {
|
||||
std::string remote;
|
||||
for (std::size_t i = 2; i < parts.size(); ++i) {
|
||||
if (i > 2) remote.push_back(':');
|
||||
remote += parts[i];
|
||||
}
|
||||
return TestRunner::Ssh(parts[1], remote);
|
||||
}
|
||||
if (parts[0] == "sshwin" && parts.size() == 2) return TestRunner::SshWin(parts[1]);
|
||||
if (parts[0] == "sshwin" && parts.size() >= 3) {
|
||||
std::string remote;
|
||||
for (std::size_t i = 2; i < parts.size(); ++i) {
|
||||
if (i > 2) remote.push_back(':');
|
||||
remote += parts[i];
|
||||
}
|
||||
return TestRunner::SshWin(parts[1], remote);
|
||||
}
|
||||
throw std::runtime_error(std::format(
|
||||
"TestRunner::FromEnv: unrecognized runner spec '{}'", spec));
|
||||
}
|
||||
}
|
||||
|
||||
TestRunner TestRunner::FromEnv(std::string_view target, TestRunner fallback) {
|
||||
std::string envName = std::format("CRAFTER_BUILD_RUNNER_{}", NormalizeTriple(target));
|
||||
const char* v = std::getenv(envName.c_str());
|
||||
if (!v || !*v) return fallback;
|
||||
if (auto r = ParseRunnerSpec(v)) return std::move(*r);
|
||||
return fallback;
|
||||
}
|
||||
|
||||
TestResult Crafter::RunSingleTest(const Test& test, const fs::path& binary, std::chrono::seconds timeout) {
|
||||
using namespace std::chrono_literals;
|
||||
TestResult result;
|
||||
result.name = test.config.name;
|
||||
|
||||
std::map<std::string, std::string> ph;
|
||||
ph["{args}"] = JoinAndQuoteArgs(test.args);
|
||||
ph["{bin_name}"] = binary.filename().string();
|
||||
ph["{bundle}"] = binary.parent_path().string();
|
||||
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
CommandResult r;
|
||||
|
||||
if (test.runner.exec.empty()) {
|
||||
// Pure-local runner: spawn the binary directly.
|
||||
std::string cmd = std::format("{} {}", ShellQuote(binary.string()), ph["{args}"]);
|
||||
r = RunCommandWithTimeout(cmd, timeout);
|
||||
} else if (test.runner.copy.empty()) {
|
||||
// Prefix runner (qemu-user, wsl, ...): templated exec wraps a local binary.
|
||||
ph["{bin}"] = binary.string();
|
||||
r = RunCommandWithTimeout(Substitute(test.runner.exec, ph), timeout);
|
||||
} else {
|
||||
// Transport runner (ssh, ...): copy bundle → exec remotely → cleanup.
|
||||
std::string unique = std::format("{}-{}-{}",
|
||||
test.config.name,
|
||||
std::hash<std::thread::id>{}(std::this_thread::get_id()),
|
||||
std::chrono::steady_clock::now().time_since_epoch().count());
|
||||
ph["{remote_bundle}"] = std::format("{}/{}", test.runner.remoteDir, unique);
|
||||
ph["{bin}"] = std::format("{}/{}", ph["{remote_bundle}"], ph["{bin_name}"]);
|
||||
// Backslash variants for cmd.exe-shell remotes (cmd's mkdir won't
|
||||
// auto-create intermediate directories when path uses forward slashes).
|
||||
ph["{remote_bundle_win}"] = ph["{remote_bundle}"];
|
||||
std::replace(ph["{remote_bundle_win}"].begin(), ph["{remote_bundle_win}"].end(), '/', '\\');
|
||||
ph["{bin_win}"] = ph["{bin}"];
|
||||
std::replace(ph["{bin_win}"].begin(), ph["{bin_win}"].end(), '/', '\\');
|
||||
|
||||
CommandResult cp = RunCommandWithTimeout(Substitute(test.runner.copy, ph), 5min);
|
||||
if (cp.exitCode != 0) {
|
||||
auto end = std::chrono::steady_clock::now();
|
||||
result.duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
|
||||
result.outcome = TestOutcome::Fail;
|
||||
result.exitCode = cp.exitCode;
|
||||
result.output = std::format("copy step failed (exit {}):\n{}", cp.exitCode, cp.output);
|
||||
return result;
|
||||
}
|
||||
|
||||
r = RunCommandWithTimeout(Substitute(test.runner.exec, ph), timeout);
|
||||
|
||||
if (!test.runner.cleanup.empty()) {
|
||||
std::system(Substitute(test.runner.cleanup, ph).c_str());
|
||||
}
|
||||
}
|
||||
|
||||
auto end = std::chrono::steady_clock::now();
|
||||
result.duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
|
||||
result.output = std::move(r.output);
|
||||
result.exitCode = r.exitCode;
|
||||
result.signal = r.signal;
|
||||
|
||||
if (r.timedOut) result.outcome = TestOutcome::Timeout;
|
||||
else if (r.crashed) result.outcome = TestOutcome::Crash;
|
||||
else if (r.exitCode != 0) result.outcome = TestOutcome::Fail;
|
||||
else result.outcome = TestOutcome::Pass;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
TestSummary Crafter::RunTests(Configuration& projectCfg, const RunTestsOptions& opts) {
|
||||
TestSummary summary;
|
||||
|
||||
std::vector<Test*> filtered;
|
||||
filtered.reserve(projectCfg.tests.size());
|
||||
for (auto& test : projectCfg.tests) {
|
||||
if (MatchAny(opts.globs, test.config.name)) {
|
||||
filtered.push_back(&test);
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.listOnly) {
|
||||
for (auto* t : filtered) {
|
||||
std::println("{}", t->config.name);
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
|
||||
if (filtered.empty()) {
|
||||
std::println("No tests matched.");
|
||||
return summary;
|
||||
}
|
||||
|
||||
int jobs = opts.jobs > 0
|
||||
? opts.jobs
|
||||
: std::max(1u, std::thread::hardware_concurrency());
|
||||
jobs = std::min(jobs, static_cast<int>(filtered.size()));
|
||||
|
||||
std::unordered_map<fs::path, std::shared_future<BuildResult>> depResults;
|
||||
std::mutex depMutex;
|
||||
std::mutex printMutex;
|
||||
std::mutex probeMutex;
|
||||
std::unordered_map<std::string, bool> probeCache;
|
||||
std::atomic<std::size_t> next{0};
|
||||
std::vector<TestResult> results(filtered.size());
|
||||
|
||||
auto runnerAvailable = [&](const TestRunner& runner) -> bool {
|
||||
if (runner.probe.empty()) return true;
|
||||
std::lock_guard lk(probeMutex);
|
||||
if (auto it = probeCache.find(runner.name); it != probeCache.end()) {
|
||||
return it->second;
|
||||
}
|
||||
bool ok = (std::system(runner.probe.c_str()) == 0);
|
||||
probeCache[runner.name] = ok;
|
||||
return ok;
|
||||
};
|
||||
|
||||
auto worker = [&]() {
|
||||
while (true) {
|
||||
std::size_t i = next.fetch_add(1);
|
||||
if (i >= filtered.size()) break;
|
||||
Test& t = *filtered[i];
|
||||
TestResult r;
|
||||
r.name = t.config.name;
|
||||
|
||||
if (!runnerAvailable(t.runner)) {
|
||||
r.outcome = TestOutcome::Skipped;
|
||||
r.output = std::format("runner '{}' not available", t.runner.name);
|
||||
{
|
||||
std::lock_guard lk(printMutex);
|
||||
PrintResult(r, t.runner.name);
|
||||
}
|
||||
results[i] = std::move(r);
|
||||
continue;
|
||||
}
|
||||
|
||||
BuildResult br;
|
||||
try {
|
||||
br = Build(t.config, depResults, depMutex);
|
||||
} catch (const std::exception& e) {
|
||||
r.outcome = TestOutcome::Fail;
|
||||
r.output = std::format("build threw: {}", e.what());
|
||||
r.exitCode = -1;
|
||||
{
|
||||
std::lock_guard lk(printMutex);
|
||||
PrintResult(r, t.runner.name);
|
||||
}
|
||||
results[i] = std::move(r);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!br.result.empty()) {
|
||||
r.outcome = TestOutcome::Fail;
|
||||
r.output = std::format("build failed: {}", br.result);
|
||||
r.exitCode = -1;
|
||||
{
|
||||
std::lock_guard lk(printMutex);
|
||||
PrintResult(r, t.runner.name);
|
||||
}
|
||||
results[i] = std::move(r);
|
||||
continue;
|
||||
}
|
||||
|
||||
std::chrono::seconds timeout = opts.timeoutOverride.value_or(t.timeout);
|
||||
fs::path binary = TestBinaryPath(t.config);
|
||||
try {
|
||||
r = RunSingleTest(t, binary, timeout);
|
||||
} catch (const std::exception& e) {
|
||||
r.outcome = TestOutcome::Fail;
|
||||
r.output = std::format("runner threw: {}", e.what());
|
||||
r.exitCode = -1;
|
||||
}
|
||||
|
||||
if (r.outcome != TestOutcome::Pass && r.outcome != TestOutcome::Skipped && !r.output.empty()) {
|
||||
WriteLog(projectCfg.path, r.name, r.output);
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard lk(printMutex);
|
||||
PrintResult(r, t.runner.name);
|
||||
}
|
||||
results[i] = std::move(r);
|
||||
}
|
||||
};
|
||||
|
||||
std::vector<std::jthread> threads;
|
||||
threads.reserve(jobs);
|
||||
for (int j = 0; j < jobs; ++j) {
|
||||
threads.emplace_back(worker);
|
||||
}
|
||||
threads.clear(); // joins all jthreads
|
||||
|
||||
for (auto& r : results) {
|
||||
switch (r.outcome) {
|
||||
case TestOutcome::Pass: summary.passed++; break;
|
||||
case TestOutcome::Fail: summary.failed++; break;
|
||||
case TestOutcome::Crash: summary.crashed++; break;
|
||||
case TestOutcome::Timeout: summary.timedOut++; break;
|
||||
case TestOutcome::Skipped: summary.skipped++; break;
|
||||
}
|
||||
}
|
||||
summary.results = std::move(results);
|
||||
|
||||
std::print("\n");
|
||||
std::vector<std::string> parts;
|
||||
if (summary.passed) parts.push_back(std::format("{} passed", summary.passed));
|
||||
if (summary.failed) parts.push_back(std::format("{} failed", summary.failed));
|
||||
if (summary.crashed) parts.push_back(std::format("{} crashed", summary.crashed));
|
||||
if (summary.timedOut) parts.push_back(std::format("{} timed out", summary.timedOut));
|
||||
if (summary.skipped) parts.push_back(std::format("{} skipped", summary.skipped));
|
||||
std::string joined;
|
||||
for (std::size_t i = 0; i < parts.size(); ++i) {
|
||||
if (i) joined += ", ";
|
||||
joined += parts[i];
|
||||
}
|
||||
std::println("{}", joined);
|
||||
|
||||
return summary;
|
||||
}
|
||||
11
interfaces/Crafter.Build-Api.h
Normal file
11
interfaces/Crafter.Build-Api.h
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
#pragma once
|
||||
|
||||
#if defined(_WIN32)
|
||||
#if defined(CRAFTER_BUILD_DLL_IMPORT)
|
||||
#define CRAFTER_API __declspec(dllimport)
|
||||
#else
|
||||
#define CRAFTER_API __declspec(dllexport)
|
||||
#endif
|
||||
#else
|
||||
#define CRAFTER_API
|
||||
#endif
|
||||
|
|
@ -17,6 +17,8 @@ License along with this library; if not, write to the Free Software
|
|||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
module;
|
||||
#include "Crafter.Build-Api.h"
|
||||
export module Crafter.Build:Clang;
|
||||
import std;
|
||||
import :Shader;
|
||||
|
|
@ -43,6 +45,39 @@ export namespace Crafter {
|
|||
LibraryDynamic,
|
||||
};
|
||||
|
||||
struct Test;
|
||||
|
||||
struct TestRunner {
|
||||
std::string copy;
|
||||
std::string exec;
|
||||
std::string cleanup;
|
||||
std::string remoteDir;
|
||||
std::string name;
|
||||
// Runs once per RunTests invocation (cached by `name`). Exit 0 = runner
|
||||
// is available; non-zero = skip every Test using this runner with a
|
||||
// "runner unavailable" message. Empty = always available (e.g., Local).
|
||||
std::string probe;
|
||||
|
||||
bool IsLocal() const { return exec.empty(); }
|
||||
|
||||
static CRAFTER_API TestRunner Local();
|
||||
static CRAFTER_API TestRunner Ssh(std::string host, std::string remoteDir = "/tmp/crafter-tests");
|
||||
static CRAFTER_API TestRunner SshWin(std::string host, std::string remoteDir = "C:/temp/crafter-tests");
|
||||
static CRAFTER_API TestRunner QemuUser(std::string qemuBin);
|
||||
static CRAFTER_API TestRunner FromEnv(std::string_view target, TestRunner fallback = Local());
|
||||
};
|
||||
|
||||
enum class TestOutcome { Pass, Fail, Crash, Timeout, Skipped };
|
||||
|
||||
struct TestResult {
|
||||
std::string name;
|
||||
TestOutcome outcome = TestOutcome::Pass;
|
||||
int exitCode = 0;
|
||||
int signal = 0;
|
||||
std::chrono::milliseconds duration{0};
|
||||
std::string output;
|
||||
};
|
||||
|
||||
struct Configuration {
|
||||
fs::path path;
|
||||
std::string outputName;
|
||||
|
|
@ -50,6 +85,7 @@ export namespace Crafter {
|
|||
std::string march = "native";
|
||||
std::string mtune = "native";
|
||||
std::string target;
|
||||
std::string sysroot;
|
||||
bool debug = false;
|
||||
ConfigurationType type = ConfigurationType::Executable;
|
||||
std::vector<std::unique_ptr<Module>> interfaces;
|
||||
|
|
@ -63,7 +99,8 @@ export namespace Crafter {
|
|||
std::vector<ExternalDependency> externalDependencies;
|
||||
std::vector<std::string> compileFlags;
|
||||
std::vector<std::string> linkFlags;
|
||||
void GetInterfacesAndImplementations(std::span<fs::path> interfaces, std::span<fs::path> implementations);
|
||||
std::vector<Test> tests;
|
||||
CRAFTER_API void GetInterfacesAndImplementations(std::span<fs::path> interfaces, std::span<fs::path> implementations);
|
||||
fs::path PcmDir() const {
|
||||
return path
|
||||
/ (type == ConfigurationType::Executable ? "build" : "bin")
|
||||
|
|
@ -71,7 +108,14 @@ export namespace Crafter {
|
|||
}
|
||||
};
|
||||
|
||||
BuildResult Build(Configuration& config, std::unordered_map<fs::path, std::shared_future<BuildResult>>& depResults, std::mutex& depMutex);
|
||||
struct Test {
|
||||
Configuration config;
|
||||
TestRunner runner;
|
||||
std::chrono::seconds timeout{60};
|
||||
std::vector<std::string> args;
|
||||
};
|
||||
|
||||
int Run(int argc, char** argv);
|
||||
CRAFTER_API BuildResult Build(Configuration& config, std::unordered_map<fs::path, std::shared_future<BuildResult>>& depResults, std::mutex& depMutex);
|
||||
|
||||
CRAFTER_API int Run(int argc, char** argv);
|
||||
}
|
||||
|
|
@ -17,6 +17,8 @@ License along with this library; if not, write to the Free Software
|
|||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
module;
|
||||
#include "Crafter.Build-Api.h"
|
||||
export module Crafter.Build:External;
|
||||
import std;
|
||||
namespace fs = std::filesystem;
|
||||
|
|
@ -49,7 +51,7 @@ export namespace Crafter {
|
|||
fs::file_time_type latestArtifact = fs::file_time_type::min();
|
||||
};
|
||||
|
||||
ExternalBuildResult BuildExternal(
|
||||
CRAFTER_API ExternalBuildResult BuildExternal(
|
||||
const ExternalDependency& dep,
|
||||
std::atomic<bool>& cancelled);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ License along with this library; if not, write to the Free Software
|
|||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
module;
|
||||
#include "Crafter.Build-Api.h"
|
||||
export module Crafter.Build:Implementation;
|
||||
import std;
|
||||
namespace fs = std::filesystem;
|
||||
|
|
@ -30,8 +32,8 @@ namespace Crafter {
|
|||
std::vector<ModulePartition*> partitionDependencies;
|
||||
std::vector<std::pair<Module*, fs::path>> externalModuleDependencies;
|
||||
fs::path path;
|
||||
Implementation(fs::path&& path);
|
||||
bool Check(const fs::path& buildDir, const fs::path& pcmDir, fs::file_time_type sourceFloor = fs::file_time_type::min()) const;
|
||||
void Compile(const std::string_view clang, const fs::path& buildDir, std::atomic<bool>& buildCancelled, std::string& buildError) const;
|
||||
CRAFTER_API Implementation(fs::path&& path);
|
||||
CRAFTER_API bool Check(const fs::path& buildDir, const fs::path& pcmDir, fs::file_time_type sourceFloor = fs::file_time_type::min()) const;
|
||||
CRAFTER_API void Compile(const std::string_view clang, const fs::path& buildDir, std::atomic<bool>& buildCancelled, std::string& buildError) const;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ License along with this library; if not, write to the Free Software
|
|||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
module;
|
||||
#include "Crafter.Build-Api.h"
|
||||
export module Crafter.Build:Interface;
|
||||
import std;
|
||||
namespace fs = std::filesystem;
|
||||
|
|
@ -33,9 +35,9 @@ namespace Crafter {
|
|||
bool checked = false;
|
||||
std::string name;
|
||||
fs::path path;
|
||||
ModulePartition(std::string&& name, fs::path&& path);
|
||||
bool Check(const fs::path& pcmDir, fs::file_time_type sourceFloor = fs::file_time_type::min());
|
||||
void Compile(const std::string_view clang, const fs::path& pcmDir, const fs::path& buildDir, std::atomic<bool>& buildCancelled, std::string& buildError);
|
||||
CRAFTER_API ModulePartition(std::string&& name, fs::path&& path);
|
||||
CRAFTER_API bool Check(const fs::path& pcmDir, fs::file_time_type sourceFloor = fs::file_time_type::min());
|
||||
CRAFTER_API void Compile(const std::string_view clang, const fs::path& pcmDir, const fs::path& buildDir, std::atomic<bool>& buildCancelled, std::string& buildError);
|
||||
};
|
||||
|
||||
export class Module {
|
||||
|
|
@ -46,8 +48,8 @@ namespace Crafter {
|
|||
std::vector<std::unique_ptr<ModulePartition>> partitions;
|
||||
std::string name;
|
||||
fs::path path;
|
||||
Module(std::string&& name, fs::path&& path);
|
||||
bool Check(const fs::path& pcmDir, fs::file_time_type sourceFloor = fs::file_time_type::min());
|
||||
void Compile(const std::string_view clang, const fs::path& pcmDir, const fs::path& buildDir, std::atomic<bool>& buildCancelled, std::string& buildError);
|
||||
CRAFTER_API Module(std::string&& name, fs::path&& path);
|
||||
CRAFTER_API bool Check(const fs::path& pcmDir, fs::file_time_type sourceFloor = fs::file_time_type::min());
|
||||
CRAFTER_API void Compile(const std::string_view clang, const fs::path& pcmDir, const fs::path& buildDir, std::atomic<bool>& buildCancelled, std::string& buildError);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ License along with this library; if not, write to the Free Software
|
|||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
module;
|
||||
#include "Crafter.Build-Api.h"
|
||||
export module Crafter.Build:Platform;
|
||||
import std;
|
||||
namespace fs = std::filesystem;
|
||||
|
|
@ -24,13 +26,17 @@ namespace fs = std::filesystem;
|
|||
namespace Crafter {
|
||||
struct Configuration;
|
||||
struct CommandResult {
|
||||
int exitCode;
|
||||
int exitCode = 0;
|
||||
std::string output;
|
||||
bool crashed = false;
|
||||
bool timedOut = false;
|
||||
int signal = 0;
|
||||
};
|
||||
std::string BuildStdPcm(const Configuration& config, fs::path stdPcm);
|
||||
fs::path GetCacheDir();
|
||||
std::string RunCommand(const std::string_view command);
|
||||
CommandResult RunCommandChecked(std::string_view command);
|
||||
export CRAFTER_API CommandResult RunCommandWithTimeout(std::string_view command, std::chrono::seconds timeout);
|
||||
std::string GetBaseCommand(const Configuration& config);
|
||||
export Configuration LoadProject(const fs::path& projectFile, std::span<const std::string_view> args);
|
||||
export CRAFTER_API Configuration LoadProject(const fs::path& projectFile, std::span<const std::string_view> args);
|
||||
}
|
||||
|
|
@ -17,6 +17,8 @@ License along with this library; if not, write to the Free Software
|
|||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
module;
|
||||
#include "Crafter.Build-Api.h"
|
||||
export module Crafter.Build:Shader;
|
||||
import std;
|
||||
namespace fs = std::filesystem;
|
||||
|
|
@ -44,8 +46,8 @@ namespace Crafter {
|
|||
fs::path path;
|
||||
std::string entrypoint;
|
||||
ShaderType type;
|
||||
Shader(fs::path&& path, std::string&& entrypoint, ShaderType type);
|
||||
bool Check(const fs::path& outputDir) const;
|
||||
std::string Compile(const fs::path& outputDir) const;
|
||||
CRAFTER_API Shader(fs::path&& path, std::string&& entrypoint, ShaderType type);
|
||||
CRAFTER_API bool Check(const fs::path& outputDir) const;
|
||||
CRAFTER_API std::string Compile(const fs::path& outputDir) const;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
46
interfaces/Crafter.Build-Test.cppm
Normal file
46
interfaces/Crafter.Build-Test.cppm
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
Crafter® Build
|
||||
Copyright (C) 2026 Catcrafts®
|
||||
Catcrafts.net
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License version 3.0 as published by the Free Software Foundation;
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
module;
|
||||
#include "Crafter.Build-Api.h"
|
||||
export module Crafter.Build:Test;
|
||||
import std;
|
||||
import :Clang;
|
||||
|
||||
export namespace Crafter {
|
||||
struct RunTestsOptions {
|
||||
std::vector<std::string> globs;
|
||||
int jobs = 0;
|
||||
std::optional<std::chrono::seconds> timeoutOverride;
|
||||
bool listOnly = false;
|
||||
};
|
||||
|
||||
struct TestSummary {
|
||||
int passed = 0;
|
||||
int failed = 0;
|
||||
int crashed = 0;
|
||||
int timedOut = 0;
|
||||
int skipped = 0;
|
||||
std::vector<TestResult> results;
|
||||
bool AllPassed() const { return failed == 0 && crashed == 0 && timedOut == 0; }
|
||||
};
|
||||
|
||||
CRAFTER_API TestSummary RunTests(Configuration& projectCfg, const RunTestsOptions& opts);
|
||||
CRAFTER_API TestResult RunSingleTest(const Test& test, const std::filesystem::path& binary, std::chrono::seconds timeout);
|
||||
}
|
||||
|
|
@ -23,3 +23,4 @@ export import :Platform;
|
|||
export import :Implementation;
|
||||
export import :Shader;
|
||||
export import :External;
|
||||
export import :Test;
|
||||
176
project.cpp
176
project.cpp
|
|
@ -4,18 +4,20 @@ namespace fs = std::filesystem;
|
|||
using namespace Crafter;
|
||||
|
||||
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
|
||||
Configuration cfg;
|
||||
cfg.path = "./";
|
||||
cfg.name = "crafter.build";
|
||||
cfg.outputName = "crafter-build";
|
||||
cfg.target = "x86_64-pc-linux-gnu";
|
||||
cfg.type = ConfigurationType::Executable;
|
||||
|
||||
bool debug = false;
|
||||
for (std::string_view arg : args) {
|
||||
if (arg == "--debug") cfg.debug = true;
|
||||
if (arg == "--debug") debug = true;
|
||||
}
|
||||
|
||||
std::array<fs::path, 7> interfaces = {
|
||||
static auto crafterBuildLib = std::make_unique<Configuration>();
|
||||
crafterBuildLib->path = "./";
|
||||
crafterBuildLib->name = "crafter.build-lib";
|
||||
crafterBuildLib->outputName = "crafter-build";
|
||||
crafterBuildLib->target = "x86_64-pc-linux-gnu";
|
||||
crafterBuildLib->type = ConfigurationType::LibraryStatic;
|
||||
crafterBuildLib->debug = debug;
|
||||
{
|
||||
std::array<fs::path, 8> interfaces = {
|
||||
"interfaces/Crafter.Build",
|
||||
"interfaces/Crafter.Build-Shader",
|
||||
"interfaces/Crafter.Build-Platform",
|
||||
|
|
@ -23,6 +25,7 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
|||
"interfaces/Crafter.Build-Implementation",
|
||||
"interfaces/Crafter.Build-External",
|
||||
"interfaces/Crafter.Build-Clang",
|
||||
"interfaces/Crafter.Build-Test",
|
||||
};
|
||||
std::array<fs::path, 7> implementations = {
|
||||
"implementations/Crafter.Build-Shader",
|
||||
|
|
@ -31,11 +34,11 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
|||
"implementations/Crafter.Build-Implementation",
|
||||
"implementations/Crafter.Build-External",
|
||||
"implementations/Crafter.Build-Clang",
|
||||
"implementations/main",
|
||||
"implementations/Crafter.Build-Test",
|
||||
};
|
||||
cfg.GetInterfacesAndImplementations(interfaces, implementations);
|
||||
|
||||
ExternalDependency& glslang = cfg.externalDependencies.emplace_back();
|
||||
crafterBuildLib->GetInterfacesAndImplementations(interfaces, implementations);
|
||||
}
|
||||
ExternalDependency& glslang = crafterBuildLib->externalDependencies.emplace_back();
|
||||
glslang.name = "glslang";
|
||||
glslang.source.url = "https://github.com/KhronosGroup/glslang.git";
|
||||
glslang.source.branch = "main";
|
||||
|
|
@ -44,8 +47,155 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
|||
glslang.includeDirs = { "" };
|
||||
glslang.libs = { "SPIRV", "GenericCodeGen", "glslang", "OSDependent", "MachineIndependent", "glslang-default-resource-limits" };
|
||||
|
||||
Configuration cfg;
|
||||
cfg.path = "./";
|
||||
cfg.name = "crafter.build-exe";
|
||||
cfg.outputName = "crafter-build";
|
||||
cfg.target = "x86_64-pc-linux-gnu";
|
||||
cfg.type = ConfigurationType::Executable;
|
||||
cfg.debug = debug;
|
||||
cfg.dependencies = { crafterBuildLib.get() };
|
||||
{
|
||||
std::array<fs::path, 0> interfaces = {};
|
||||
std::array<fs::path, 1> implementations = { "implementations/main" };
|
||||
cfg.GetInterfacesAndImplementations(interfaces, implementations);
|
||||
}
|
||||
cfg.linkFlags.push_back("-Wl,--export-dynamic");
|
||||
cfg.linkFlags.push_back("-ldl");
|
||||
|
||||
// ----- Single-driver, multi-target tests (each appears once per (target, runner)). -----
|
||||
|
||||
// Lifetime holders for per-target lib Configurations referenced by Tests.
|
||||
static std::vector<std::unique_ptr<Configuration>> testLibPool;
|
||||
|
||||
struct TargetRunner {
|
||||
std::string target;
|
||||
TestRunner runner;
|
||||
std::vector<std::string> extraLinkFlags;
|
||||
};
|
||||
std::vector<TargetRunner> targets = {
|
||||
{ "x86_64-pc-linux-gnu", TestRunner::Local(), {} },
|
||||
{ "x86_64-w64-mingw32", TestRunner::SshWin("winvm", "C:/temp/crafter-tests"), {"-lstdc++exp"} },
|
||||
};
|
||||
|
||||
auto addPerTarget = [&](std::string name, auto buildOne) {
|
||||
for (auto& tr : targets) {
|
||||
Test t;
|
||||
t.config.name = name;
|
||||
t.config.target = tr.target;
|
||||
t.config.type = ConfigurationType::Executable;
|
||||
t.config.linkFlags.push_back("-fuse-ld=lld");
|
||||
for (auto& f : tr.extraLinkFlags) t.config.linkFlags.push_back(f);
|
||||
buildOne(t, tr.target);
|
||||
t.runner = tr.runner;
|
||||
cfg.tests.push_back(std::move(t));
|
||||
}
|
||||
};
|
||||
|
||||
addPerTarget("HelloWorld", [](Test& t, std::string_view) {
|
||||
t.config.path = "tests/fixtures/hello-world/";
|
||||
t.config.outputName = "hello";
|
||||
std::array<fs::path, 0> ifaces = {};
|
||||
std::array<fs::path, 1> impls = { "main" };
|
||||
t.config.GetInterfacesAndImplementations(ifaces, impls);
|
||||
});
|
||||
|
||||
addPerTarget("WithModule", [](Test& t, std::string_view) {
|
||||
t.config.path = "tests/fixtures/with-module/";
|
||||
t.config.outputName = "hello-mod";
|
||||
std::array<fs::path, 1> ifaces = { "interfaces/Greeter" };
|
||||
std::array<fs::path, 1> impls = { "main" };
|
||||
t.config.GetInterfacesAndImplementations(ifaces, impls);
|
||||
});
|
||||
|
||||
addPerTarget("Defines", [](Test& t, std::string_view) {
|
||||
t.config.path = "tests/fixtures/defines/";
|
||||
t.config.outputName = "defines-app";
|
||||
std::array<fs::path, 0> ifaces = {};
|
||||
std::array<fs::path, 1> impls = { "main" };
|
||||
t.config.GetInterfacesAndImplementations(ifaces, impls);
|
||||
t.config.defines.push_back({"CRAFTER_TEST_FOO", "42"});
|
||||
});
|
||||
|
||||
addPerTarget("CrossProjectModule", [&](Test& t, std::string_view target) {
|
||||
auto fooLib = std::make_unique<Configuration>();
|
||||
fooLib->path = "tests/fixtures/cross-project/lib/";
|
||||
fooLib->name = std::format("Foo-cross-project-{}", target);
|
||||
fooLib->outputName = "Foo";
|
||||
fooLib->target = std::string(target);
|
||||
fooLib->type = ConfigurationType::LibraryStatic;
|
||||
std::array<fs::path, 1> libIfaces = { "Foo" };
|
||||
std::array<fs::path, 0> libImpls = {};
|
||||
fooLib->GetInterfacesAndImplementations(libIfaces, libImpls);
|
||||
Configuration* fooLibPtr = fooLib.get();
|
||||
testLibPool.push_back(std::move(fooLib));
|
||||
|
||||
t.config.path = "tests/fixtures/cross-project/";
|
||||
t.config.outputName = "cross-app";
|
||||
t.config.dependencies = { fooLibPtr };
|
||||
std::array<fs::path, 0> mainIfaces = {};
|
||||
std::array<fs::path, 1> mainImpls = { "main" };
|
||||
t.config.GetInterfacesAndImplementations(mainIfaces, mainImpls);
|
||||
});
|
||||
|
||||
auto makeDiamondLib = [&](std::string_view dir, std::string_view modName,
|
||||
std::string_view target, std::span<Configuration*> deps) {
|
||||
auto lib = std::make_unique<Configuration>();
|
||||
lib->path = std::format("tests/fixtures/diamond/{}/", dir);
|
||||
lib->name = std::format("{}-diamond-{}", modName, target);
|
||||
lib->outputName = std::string(modName);
|
||||
lib->target = std::string(target);
|
||||
lib->type = ConfigurationType::LibraryStatic;
|
||||
lib->dependencies.assign(deps.begin(), deps.end());
|
||||
std::array<fs::path, 1> ifaces = { fs::path(modName) };
|
||||
std::array<fs::path, 0> impls = {};
|
||||
lib->GetInterfacesAndImplementations(ifaces, impls);
|
||||
return lib;
|
||||
};
|
||||
|
||||
addPerTarget("Diamond", [&](Test& t, std::string_view target) {
|
||||
auto X = makeDiamondLib("X", "X", target, {});
|
||||
Configuration* xDeps[] = { X.get() };
|
||||
auto B = makeDiamondLib("B", "B", target, xDeps);
|
||||
auto C = makeDiamondLib("C", "C", target, xDeps);
|
||||
Configuration* mainDeps[] = { B.get(), C.get() };
|
||||
|
||||
t.config.path = "tests/fixtures/diamond/";
|
||||
t.config.outputName = "diamond-app";
|
||||
t.config.dependencies.assign(mainDeps, mainDeps + 2);
|
||||
std::array<fs::path, 0> ifaces = {};
|
||||
std::array<fs::path, 1> impls = { "main" };
|
||||
t.config.GetInterfacesAndImplementations(ifaces, impls);
|
||||
|
||||
testLibPool.push_back(std::move(X));
|
||||
testLibPool.push_back(std::move(B));
|
||||
testLibPool.push_back(std::move(C));
|
||||
});
|
||||
|
||||
// ----- Outer-driver tests (still need their own driver source for multi-step
|
||||
// logic or meta-runner behavior). -----
|
||||
|
||||
auto addOuterDriverTest = [&](std::string name) {
|
||||
Test t;
|
||||
t.config.path = "./";
|
||||
t.config.name = name;
|
||||
t.config.outputName = name;
|
||||
t.config.target = "x86_64-pc-linux-gnu";
|
||||
t.config.type = ConfigurationType::Executable;
|
||||
std::array<fs::path, 0> empty = {};
|
||||
std::array<fs::path, 1> impls = { fs::path("tests") / name };
|
||||
t.config.GetInterfacesAndImplementations(empty, impls);
|
||||
cfg.tests.push_back(std::move(t));
|
||||
};
|
||||
|
||||
addOuterDriverTest("Incremental");
|
||||
addOuterDriverTest("BuildError");
|
||||
addOuterDriverTest("Libraries");
|
||||
addOuterDriverTest("RunnerClassification");
|
||||
addOuterDriverTest("QemuUser");
|
||||
addOuterDriverTest("SshRunner");
|
||||
addOuterDriverTest("CrossArchAarch64");
|
||||
addOuterDriverTest("WindowsViaSsh");
|
||||
|
||||
return cfg;
|
||||
}
|
||||
|
|
|
|||
46
tests/BuildError.cpp
Normal file
46
tests/BuildError.cpp
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
Crafter® Build
|
||||
Copyright (C) 2026 Catcrafts®
|
||||
Catcrafts.net
|
||||
|
||||
LGPL-3.0-only.
|
||||
*/
|
||||
|
||||
import std;
|
||||
#include "TestUtil.h"
|
||||
namespace fs = std::filesystem;
|
||||
using namespace TestUtil;
|
||||
|
||||
int main() {
|
||||
try {
|
||||
fs::path projectRoot = fs::current_path();
|
||||
fs::path src = projectRoot / "tests" / "fixtures" / "build-error";
|
||||
fs::path crafterBuild = projectRoot / "bin" / "crafter-build";
|
||||
|
||||
fs::path work = CopyFixtureToTemp("BuildError", src);
|
||||
|
||||
auto build = RunInDir(work, std::format("'{}'", crafterBuild.string()));
|
||||
if (build.exitCode == 0) {
|
||||
std::println(std::cerr, "expected nonzero exit, got 0; build output:\n{}", build.output);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// diagnostic must surface the unresolved name; fragile-ish but recognizable
|
||||
if (build.output.find("undefined_symbol_xyzzy_oqv") == std::string::npos) {
|
||||
std::println(std::cerr, "diagnostic missing unresolved-name reference:\n{}", build.output);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// and the artifact must NOT have been produced
|
||||
fs::path artifact = work / "bin" / "broken-x86_64-pc-linux-gnu-native" / "broken";
|
||||
if (fs::exists(artifact)) {
|
||||
std::println(std::cerr, "artifact unexpectedly produced at {}", artifact.string());
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
} catch (const std::exception& e) {
|
||||
std::println(std::cerr, "test exception: {}", e.what());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
86
tests/CrossArchAarch64.cpp
Normal file
86
tests/CrossArchAarch64.cpp
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
Crafter® Build
|
||||
Copyright (C) 2026 Catcrafts®
|
||||
Catcrafts.net
|
||||
|
||||
LGPL-3.0-only.
|
||||
|
||||
End-to-end cross-arch build through the V2 pipeline:
|
||||
the fixture's project.cpp targets aarch64-linux-gnu with cfg.sysroot pointing at
|
||||
the Arch Linux ARM rootfs at /opt/aarch64-rootfs. crafter-build cross-compiles
|
||||
the C++ source (with the libc++ std module from the sysroot), produces a real
|
||||
aarch64 ELF, and qemu-aarch64 (with QEMU_LD_PREFIX pointing at the sysroot so it
|
||||
can find ld-linux-aarch64.so.1) executes it.
|
||||
*/
|
||||
|
||||
import std;
|
||||
#include "TestUtil.h"
|
||||
namespace fs = std::filesystem;
|
||||
using namespace TestUtil;
|
||||
|
||||
namespace {
|
||||
bool ToolPresent(std::string_view name) {
|
||||
std::string cmd = std::format("which {} > /dev/null 2>&1", name);
|
||||
return std::system(cmd.c_str()) == 0;
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
const fs::path sysroot = "/opt/aarch64-rootfs";
|
||||
if (!fs::exists(sysroot / "usr/share/libc++/v1/std.cppm")) {
|
||||
std::println("(skipped: aarch64 sysroot missing at {} — see README)", sysroot.string());
|
||||
return 0;
|
||||
}
|
||||
if (!ToolPresent("qemu-aarch64")) {
|
||||
std::println("(skipped: qemu-aarch64 not on PATH)");
|
||||
return 0;
|
||||
}
|
||||
|
||||
fs::path projectRoot = fs::current_path();
|
||||
fs::path src = projectRoot / "tests" / "fixtures" / "cross-arch-aarch64";
|
||||
fs::path crafterBuild = projectRoot / "bin" / "crafter-build";
|
||||
|
||||
fs::path work = CopyFixtureToTemp("CrossArchAarch64", src);
|
||||
|
||||
// Build through the V2 pipeline. The fixture's project.cpp pins
|
||||
// cfg.target = "aarch64-linux-gnu" and cfg.sysroot = "/opt/aarch64-rootfs".
|
||||
auto build = RunInDir(work, std::format("'{}'", crafterBuild.string()));
|
||||
if (build.exitCode != 0) {
|
||||
std::println(std::cerr, "build failed (rc={}):\n{}", build.exitCode, build.output);
|
||||
return 1;
|
||||
}
|
||||
|
||||
fs::path artifact = work / "bin" / "aarch-hello-aarch64-linux-gnu-armv8-a" / "aarch-hello";
|
||||
if (!fs::exists(artifact)) {
|
||||
std::println(std::cerr, "expected artifact missing at {}\nbuild log:\n{}",
|
||||
artifact.string(), build.output);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Defend against silent host-arch fallback.
|
||||
auto probe = RunInDir(work, std::format("file '{}'", artifact.string()));
|
||||
if (probe.output.find("ARM aarch64") == std::string::npos) {
|
||||
std::println(std::cerr, "artifact is not ARM aarch64 ELF:\n{}", probe.output);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Run via qemu-aarch64. QEMU_LD_PREFIX tells qemu where the target's
|
||||
// dynamic linker (ld-linux-aarch64.so.1) and shared libs live.
|
||||
auto run = RunInDir(work, std::format(
|
||||
"QEMU_LD_PREFIX={} qemu-aarch64 '{}'",
|
||||
sysroot.string(), artifact.string()));
|
||||
if (run.exitCode != 0) {
|
||||
std::println(std::cerr, "qemu-aarch64 run failed (rc={}):\n{}", run.exitCode, run.output);
|
||||
return 1;
|
||||
}
|
||||
if (run.output != "hi from 64-bit aarch64\n") {
|
||||
std::println(std::cerr, "output mismatch:\n expected: \"hi from 64-bit aarch64\\n\"\n got: {:?}", run.output);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
} catch (const std::exception& e) {
|
||||
std::println(std::cerr, "test exception: {}", e.what());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
76
tests/Incremental.cpp
Normal file
76
tests/Incremental.cpp
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
Crafter® Build
|
||||
Copyright (C) 2026 Catcrafts®
|
||||
Catcrafts.net
|
||||
|
||||
LGPL-3.0-only.
|
||||
*/
|
||||
|
||||
import std;
|
||||
#include "TestUtil.h"
|
||||
namespace fs = std::filesystem;
|
||||
using namespace std::chrono_literals;
|
||||
using namespace TestUtil;
|
||||
|
||||
int main() {
|
||||
try {
|
||||
fs::path projectRoot = fs::current_path();
|
||||
fs::path src = projectRoot / "tests" / "fixtures" / "incremental";
|
||||
fs::path crafterBuild = projectRoot / "bin" / "crafter-build";
|
||||
|
||||
fs::path work = CopyFixtureToTemp("Incremental", src);
|
||||
fs::path buildDir = work / "build" / "hello-mod-x86_64-pc-linux-gnu-native";
|
||||
fs::path greeterObj = buildDir / "Greeter.o";
|
||||
fs::path mainObj = buildDir / "main_impl.o";
|
||||
std::string buildCmd = std::format("'{}'", crafterBuild.string());
|
||||
|
||||
// 1. cold build
|
||||
if (auto r = RunInDir(work, buildCmd); r.exitCode != 0) {
|
||||
std::println(std::cerr, "cold build failed (rc={}):\n{}", r.exitCode, r.output);
|
||||
return 1;
|
||||
}
|
||||
if (!fs::exists(greeterObj) || !fs::exists(mainObj)) {
|
||||
std::println(std::cerr, "expected .o files missing after cold build");
|
||||
return 1;
|
||||
}
|
||||
auto greeter_t1 = fs::last_write_time(greeterObj);
|
||||
auto main_t1 = fs::last_write_time(mainObj);
|
||||
|
||||
// 2. no-op rebuild: nothing should be regenerated
|
||||
if (auto r = RunInDir(work, buildCmd); r.exitCode != 0) {
|
||||
std::println(std::cerr, "no-op rebuild failed (rc={}):\n{}", r.exitCode, r.output);
|
||||
return 1;
|
||||
}
|
||||
auto greeter_t2 = fs::last_write_time(greeterObj);
|
||||
auto main_t2 = fs::last_write_time(mainObj);
|
||||
if (greeter_t2 != greeter_t1) {
|
||||
std::println(std::cerr, "no-op rebuild regenerated Greeter.o");
|
||||
return 1;
|
||||
}
|
||||
if (main_t2 != main_t1) {
|
||||
std::println(std::cerr, "no-op rebuild regenerated main_impl.o");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// 3. touch main.cpp: only main_impl.o should regenerate
|
||||
fs::last_write_time(work / "main.cpp", std::chrono::file_clock::now() + 2s);
|
||||
if (auto r = RunInDir(work, buildCmd); r.exitCode != 0) {
|
||||
std::println(std::cerr, "rebuild after touch failed (rc={}):\n{}", r.exitCode, r.output);
|
||||
return 1;
|
||||
}
|
||||
auto greeter_t3 = fs::last_write_time(greeterObj);
|
||||
auto main_t3 = fs::last_write_time(mainObj);
|
||||
if (greeter_t3 != greeter_t1) {
|
||||
std::println(std::cerr, "touching main.cpp unnecessarily rebuilt Greeter.o");
|
||||
return 1;
|
||||
}
|
||||
if (main_t3 <= main_t1) {
|
||||
std::println(std::cerr, "touching main.cpp did NOT rebuild main_impl.o");
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
} catch (const std::exception& e) {
|
||||
std::println(std::cerr, "test exception: {}", e.what());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
66
tests/Libraries.cpp
Normal file
66
tests/Libraries.cpp
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
Crafter® Build
|
||||
Copyright (C) 2026 Catcrafts®
|
||||
Catcrafts.net
|
||||
|
||||
LGPL-3.0-only.
|
||||
*/
|
||||
|
||||
import std;
|
||||
#include "TestUtil.h"
|
||||
namespace fs = std::filesystem;
|
||||
using namespace TestUtil;
|
||||
|
||||
int main() {
|
||||
try {
|
||||
fs::path projectRoot = fs::current_path();
|
||||
fs::path src = projectRoot / "tests" / "fixtures" / "libraries";
|
||||
fs::path crafterBuild = projectRoot / "bin" / "crafter-build";
|
||||
|
||||
fs::path work = CopyFixtureToTemp("Libraries", src);
|
||||
|
||||
auto build = RunInDir(work, std::format("'{}'", crafterBuild.string()));
|
||||
if (build.exitCode != 0) {
|
||||
std::println(std::cerr, "build failed (rc={}):\n{}", build.exitCode, build.output);
|
||||
return 1;
|
||||
}
|
||||
|
||||
fs::path staticArchive = work / "mathlib" / "bin" / "MathLib-x86_64-pc-linux-gnu-native" / "libMathLib.a";
|
||||
fs::path dynamicSO = work / "greetlib" / "bin" / "GreetLib-x86_64-pc-linux-gnu-native" / "libGreetLib.so";
|
||||
fs::path artifact = work / "bin" / "libs-app-x86_64-pc-linux-gnu-native" / "libs-app";
|
||||
|
||||
if (!fs::exists(staticArchive)) {
|
||||
std::println(std::cerr, "static archive missing at {}", staticArchive.string());
|
||||
return 1;
|
||||
}
|
||||
if (!fs::exists(dynamicSO)) {
|
||||
std::println(std::cerr, "dynamic .so missing at {}", dynamicSO.string());
|
||||
return 1;
|
||||
}
|
||||
if (!fs::exists(artifact)) {
|
||||
std::println(std::cerr, "exe missing at {}\nbuild log:\n{}", artifact.string(), build.output);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// The exe linked against a dynamic .so needs LD_LIBRARY_PATH or rpath to find it.
|
||||
// Build() already passes -Wl,-rpath,'$ORIGIN' for shared libs, but the .so lives in
|
||||
// greetlib/bin/... while the exe lives in bin/libs-app-... — different dirs. Set
|
||||
// LD_LIBRARY_PATH explicitly.
|
||||
auto run = RunInDir(work, std::format(
|
||||
"LD_LIBRARY_PATH='{}' '{}'",
|
||||
dynamicSO.parent_path().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 != "hi=42\n") {
|
||||
std::println(std::cerr, "output mismatch:\n expected: \"hi=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;
|
||||
}
|
||||
}
|
||||
65
tests/QemuUser.cpp
Normal file
65
tests/QemuUser.cpp
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
Crafter® Build
|
||||
Copyright (C) 2026 Catcrafts®
|
||||
Catcrafts.net
|
||||
|
||||
LGPL-3.0-only.
|
||||
*/
|
||||
|
||||
import std;
|
||||
#include "TestUtil.h"
|
||||
namespace fs = std::filesystem;
|
||||
using namespace TestUtil;
|
||||
|
||||
namespace {
|
||||
bool Contains(std::string_view haystack, std::string_view needle) {
|
||||
return haystack.find(needle) != std::string_view::npos;
|
||||
}
|
||||
|
||||
std::string PickQemu() {
|
||||
if (const char* v = std::getenv("CRAFTER_TEST_QEMU"); v && *v) return v;
|
||||
return "qemu-x86_64";
|
||||
}
|
||||
|
||||
bool QemuPresent(const std::string& qemu) {
|
||||
std::string cmd = std::format("which {} > /dev/null 2>&1", qemu);
|
||||
return std::system(cmd.c_str()) == 0;
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
std::string qemu = PickQemu();
|
||||
if (!QemuPresent(qemu)) {
|
||||
std::println("(skipped: {} not on PATH)", qemu);
|
||||
return 0;
|
||||
}
|
||||
|
||||
fs::path projectRoot = fs::current_path();
|
||||
fs::path src = projectRoot / "tests" / "fixtures" / "qemu-runner";
|
||||
fs::path crafterBuild = projectRoot / "bin" / "crafter-build";
|
||||
|
||||
fs::path work = CopyFixtureToTemp("QemuUser", src);
|
||||
|
||||
// Tell the inner crafter-build to use qemu for the host triple via the
|
||||
// FromEnv mechanism that the fixture's project.cpp opted into.
|
||||
auto run = RunInDir(work, std::format(
|
||||
"CRAFTER_BUILD_RUNNER_x86_64_pc_linux_gnu='qemu:{}' '{}' test",
|
||||
qemu, crafterBuild.string()));
|
||||
|
||||
if (run.exitCode != 0) {
|
||||
std::println(std::cerr, "inner runner failed (rc={}):\n{}", run.exitCode, run.output);
|
||||
return 1;
|
||||
}
|
||||
if (!Contains(run.output, std::format("\xE2\x9C\x85 Hello (qemu:{})", qemu))) {
|
||||
std::println(std::cerr,
|
||||
"expected '✅ Hello (qemu:{})' marker not found in inner output:\n{}",
|
||||
qemu, run.output);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
} catch (const std::exception& e) {
|
||||
std::println(std::cerr, "test exception: {}", e.what());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
61
tests/RunnerClassification.cpp
Normal file
61
tests/RunnerClassification.cpp
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
Crafter® Build
|
||||
Copyright (C) 2026 Catcrafts®
|
||||
Catcrafts.net
|
||||
|
||||
LGPL-3.0-only.
|
||||
*/
|
||||
|
||||
import std;
|
||||
#include "TestUtil.h"
|
||||
namespace fs = std::filesystem;
|
||||
using namespace TestUtil;
|
||||
|
||||
namespace {
|
||||
bool Contains(std::string_view haystack, std::string_view needle) {
|
||||
return haystack.find(needle) != std::string_view::npos;
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
fs::path projectRoot = fs::current_path();
|
||||
fs::path src = projectRoot / "tests" / "fixtures" / "runner-classification";
|
||||
fs::path crafterBuild = projectRoot / "bin" / "crafter-build";
|
||||
|
||||
fs::path work = CopyFixtureToTemp("RunnerClassification", src);
|
||||
|
||||
// Run the inner crafter-build test with a short timeout for the Hang test.
|
||||
auto run = RunInDir(work, std::format("'{}' test --timeout=2", crafterBuild.string()));
|
||||
|
||||
// Inner runner must report 1 passed + 1 failed + 1 crashed + 1 timed out.
|
||||
// Therefore exit code must be nonzero.
|
||||
if (run.exitCode == 0) {
|
||||
std::println(std::cerr, "inner runner unexpectedly succeeded:\n{}", run.output);
|
||||
return 1;
|
||||
}
|
||||
|
||||
struct Check { std::string_view name; std::string_view marker; };
|
||||
Check checks[] = {
|
||||
{"Pass", "\xE2\x9C\x85 Pass"}, // ✅ Pass
|
||||
{"Fail", "\xE2\x9D\x8C Fail"}, // ❌ Fail
|
||||
{"Crash", "Crash"}, // any line mentioning Crash
|
||||
{"crashed", "crashed:"}, // crash classifier line
|
||||
{"Hang", "Hang"}, // any line mentioning Hang
|
||||
{"timeout", "timeout"}, // timeout classifier word
|
||||
{"summary", "1 passed, 1 failed, 1 crashed, 1 timed out"},
|
||||
};
|
||||
|
||||
for (auto& c : checks) {
|
||||
if (!Contains(run.output, c.marker)) {
|
||||
std::println(std::cerr, "expected marker {:?} ({}) not found in inner output:\n{}",
|
||||
c.marker, c.name, run.output);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
} catch (const std::exception& e) {
|
||||
std::println(std::cerr, "test exception: {}", e.what());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
/*
|
||||
Crafter® Build
|
||||
Copyright (C) 2026 Catcrafts®
|
||||
Catcrafts.net
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License version 3.0 as published by the Free Software Foundation;
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
import Crafter.Build;
|
||||
import std;
|
||||
using namespace Crafter;
|
||||
|
||||
extern "C" {
|
||||
std::string* RunTest() {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
63
tests/SshRunner.cpp
Normal file
63
tests/SshRunner.cpp
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
Crafter® Build
|
||||
Copyright (C) 2026 Catcrafts®
|
||||
Catcrafts.net
|
||||
|
||||
LGPL-3.0-only.
|
||||
*/
|
||||
|
||||
import std;
|
||||
#include "TestUtil.h"
|
||||
namespace fs = std::filesystem;
|
||||
using namespace TestUtil;
|
||||
|
||||
namespace {
|
||||
bool Contains(std::string_view haystack, std::string_view needle) {
|
||||
return haystack.find(needle) != std::string_view::npos;
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
const char* hostEnv = std::getenv("CRAFTER_TEST_SSH_HOST");
|
||||
if (!hostEnv || !*hostEnv) {
|
||||
std::println("(skipped: set CRAFTER_TEST_SSH_HOST to enable)");
|
||||
return 0;
|
||||
}
|
||||
std::string host = hostEnv;
|
||||
|
||||
// Confirm the host is actually reachable; otherwise skip rather than fail
|
||||
// (tests should not depend on transient network state).
|
||||
std::string probe = std::format("ssh -o BatchMode=yes -o ConnectTimeout=5 {} true > /dev/null 2>&1", host);
|
||||
if (std::system(probe.c_str()) != 0) {
|
||||
std::println("(skipped: ssh {} not reachable)", host);
|
||||
return 0;
|
||||
}
|
||||
|
||||
fs::path projectRoot = fs::current_path();
|
||||
fs::path src = projectRoot / "tests" / "fixtures" / "ssh-runner";
|
||||
fs::path crafterBuild = projectRoot / "bin" / "crafter-build";
|
||||
|
||||
fs::path work = CopyFixtureToTemp("SshRunner", src);
|
||||
|
||||
std::string remoteDir = "/tmp/crafter-test-ssh-runner";
|
||||
auto run = RunInDir(work, std::format(
|
||||
"CRAFTER_BUILD_RUNNER_x86_64_pc_linux_gnu='ssh:{}:{}' '{}' test",
|
||||
host, remoteDir, crafterBuild.string()));
|
||||
|
||||
if (run.exitCode != 0) {
|
||||
std::println(std::cerr, "inner runner failed (rc={}):\n{}", run.exitCode, run.output);
|
||||
return 1;
|
||||
}
|
||||
if (!Contains(run.output, std::format("\xE2\x9C\x85 Hello (ssh:{})", host))) {
|
||||
std::println(std::cerr,
|
||||
"expected '✅ Hello (ssh:{})' marker not found in inner output:\n{}",
|
||||
host, run.output);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
} catch (const std::exception& e) {
|
||||
std::println(std::cerr, "test exception: {}", e.what());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
36
tests/TestUtil.h
Normal file
36
tests/TestUtil.h
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
#pragma once
|
||||
|
||||
namespace TestUtil {
|
||||
inline std::string ReadFile(const std::filesystem::path& p) {
|
||||
std::ifstream f(p);
|
||||
std::stringstream ss;
|
||||
ss << f.rdbuf();
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
inline std::filesystem::path CopyFixtureToTemp(std::string_view testName, const std::filesystem::path& source) {
|
||||
namespace fs = std::filesystem;
|
||||
fs::path tmp = fs::temp_directory_path() / std::format("crafter-test-{}", testName);
|
||||
fs::remove_all(tmp);
|
||||
fs::copy(source, tmp, fs::copy_options::recursive);
|
||||
return tmp;
|
||||
}
|
||||
|
||||
struct CmdResult {
|
||||
int exitCode;
|
||||
std::string output;
|
||||
};
|
||||
|
||||
inline CmdResult RunInDir(const std::filesystem::path& cwd, std::string_view command) {
|
||||
namespace fs = std::filesystem;
|
||||
// Log inside cwd so parallel test drivers don't trample each other.
|
||||
fs::path log = cwd / ".crafter-cmd-output.log";
|
||||
std::string cmd = std::format("cd '{}' && {} > '{}' 2>&1",
|
||||
cwd.string(), command, log.string());
|
||||
int rc = std::system(cmd.c_str());
|
||||
std::string out = ReadFile(log);
|
||||
std::error_code ec;
|
||||
fs::remove(log, ec);
|
||||
return {rc, std::move(out)};
|
||||
}
|
||||
}
|
||||
85
tests/WindowsViaSsh.cpp
Normal file
85
tests/WindowsViaSsh.cpp
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
Crafter® Build
|
||||
Copyright (C) 2026 Catcrafts®
|
||||
Catcrafts.net
|
||||
|
||||
LGPL-3.0-only.
|
||||
|
||||
End-to-end Linux→Windows via SSH:
|
||||
the fixture cross-compiles main.cpp for x86_64-w64-mingw32 (V2's existing MinGW
|
||||
build path), the resulting .exe + runtime DLLs (which Build() already copies
|
||||
into the output dir for the mingw target) get scp'd to a Windows host (winvm by
|
||||
default), then ssh runs the .exe under cmd.exe and we capture stdout. Exercises
|
||||
the same build-system features HelloWorld covers, but for the Windows target
|
||||
path. Gated on:
|
||||
- mingw cross-toolchain installed on the host (x86_64-w64-mingw32-g++)
|
||||
- CRAFTER_TEST_WIN_SSH_HOST env var set (defaults to no-skip if "winvm" is
|
||||
reachable, but explicit opt-in keeps CI green by default)
|
||||
*/
|
||||
|
||||
import std;
|
||||
#include "TestUtil.h"
|
||||
namespace fs = std::filesystem;
|
||||
using namespace TestUtil;
|
||||
|
||||
namespace {
|
||||
bool ToolPresent(std::string_view name) {
|
||||
std::string cmd = std::format("which {} > /dev/null 2>&1", name);
|
||||
return std::system(cmd.c_str()) == 0;
|
||||
}
|
||||
|
||||
bool Contains(std::string_view haystack, std::string_view needle) {
|
||||
return haystack.find(needle) != std::string_view::npos;
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
const char* hostEnv = std::getenv("CRAFTER_TEST_WIN_SSH_HOST");
|
||||
if (!hostEnv || !*hostEnv) {
|
||||
std::println("(skipped: set CRAFTER_TEST_WIN_SSH_HOST to enable, e.g. winvm)");
|
||||
return 0;
|
||||
}
|
||||
std::string host = hostEnv;
|
||||
|
||||
if (!ToolPresent("x86_64-w64-mingw32-g++")) {
|
||||
std::println("(skipped: mingw cross-toolchain not on PATH)");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Probe the SSH host; skip on transient unreachability rather than fail.
|
||||
std::string probe = std::format(
|
||||
"ssh -o BatchMode=yes -o ConnectTimeout=5 {} \"ver\" > /dev/null 2>&1", host);
|
||||
if (std::system(probe.c_str()) != 0) {
|
||||
std::println("(skipped: ssh {} not reachable)", host);
|
||||
return 0;
|
||||
}
|
||||
|
||||
fs::path projectRoot = fs::current_path();
|
||||
fs::path src = projectRoot / "tests" / "fixtures" / "windows-via-ssh";
|
||||
fs::path crafterBuild = projectRoot / "bin" / "crafter-build";
|
||||
|
||||
fs::path work = CopyFixtureToTemp("WindowsViaSsh", src);
|
||||
|
||||
std::string remoteDir = "C:/temp/crafter-test-winhello";
|
||||
auto run = RunInDir(work, std::format(
|
||||
"CRAFTER_BUILD_RUNNER_x86_64_w64_mingw32='sshwin:{}:{}' '{}' test",
|
||||
host, remoteDir, crafterBuild.string()));
|
||||
|
||||
if (run.exitCode != 0) {
|
||||
std::println(std::cerr, "inner runner failed (rc={}):\n{}", run.exitCode, run.output);
|
||||
return 1;
|
||||
}
|
||||
std::string marker = std::format("\xE2\x9C\x85 winhello (sshwin:{})", host);
|
||||
if (!Contains(run.output, marker)) {
|
||||
std::println(std::cerr,
|
||||
"expected marker {:?} not found in inner output:\n{}",
|
||||
marker, run.output);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
} catch (const std::exception& e) {
|
||||
std::println(std::cerr, "test exception: {}", e.what());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
3
tests/fixtures/build-error/main.cpp
vendored
Normal file
3
tests/fixtures/build-error/main.cpp
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
int main() {
|
||||
return undefined_symbol_xyzzy_oqv;
|
||||
}
|
||||
22
tests/fixtures/build-error/project.cpp
vendored
Normal file
22
tests/fixtures/build-error/project.cpp
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import std;
|
||||
import Crafter.Build;
|
||||
namespace fs = std::filesystem;
|
||||
using namespace Crafter;
|
||||
|
||||
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view>) {
|
||||
Configuration cfg;
|
||||
cfg.path = "./";
|
||||
cfg.name = "broken";
|
||||
cfg.outputName = "broken";
|
||||
cfg.target = "x86_64-pc-linux-gnu";
|
||||
cfg.type = ConfigurationType::Executable;
|
||||
|
||||
std::array<fs::path, 0> ifaces = {};
|
||||
std::array<fs::path, 1> impls = { "main" };
|
||||
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
||||
|
||||
cfg.linkFlags.push_back("-Wl,--export-dynamic");
|
||||
cfg.linkFlags.push_back("-ldl");
|
||||
|
||||
return cfg;
|
||||
}
|
||||
6
tests/fixtures/cross-arch-aarch64/main.cpp
vendored
Normal file
6
tests/fixtures/cross-arch-aarch64/main.cpp
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import std;
|
||||
|
||||
int main() {
|
||||
std::println("hi from {}-bit aarch64", sizeof(void*) * 8);
|
||||
return 0;
|
||||
}
|
||||
24
tests/fixtures/cross-arch-aarch64/project.cpp
vendored
Normal file
24
tests/fixtures/cross-arch-aarch64/project.cpp
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import std;
|
||||
import Crafter.Build;
|
||||
namespace fs = std::filesystem;
|
||||
using namespace Crafter;
|
||||
|
||||
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view>) {
|
||||
Configuration cfg;
|
||||
cfg.path = "./";
|
||||
cfg.name = "aarch-hello";
|
||||
cfg.outputName = "aarch-hello";
|
||||
cfg.target = "aarch64-linux-gnu";
|
||||
cfg.march = "armv8-a";
|
||||
cfg.mtune = "generic";
|
||||
cfg.sysroot = "/opt/aarch64-rootfs";
|
||||
cfg.type = ConfigurationType::Executable;
|
||||
|
||||
std::array<fs::path, 0> ifaces = {};
|
||||
std::array<fs::path, 1> impls = { "main" };
|
||||
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
||||
|
||||
cfg.linkFlags.push_back("-fuse-ld=lld");
|
||||
|
||||
return cfg;
|
||||
}
|
||||
5
tests/fixtures/cross-project/lib/Foo.cppm
vendored
Normal file
5
tests/fixtures/cross-project/lib/Foo.cppm
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export module Foo;
|
||||
|
||||
export int Compute() {
|
||||
return 7 * 6;
|
||||
}
|
||||
7
tests/fixtures/cross-project/main.cpp
vendored
Normal file
7
tests/fixtures/cross-project/main.cpp
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import std;
|
||||
import Foo;
|
||||
|
||||
int main() {
|
||||
if (Compute() != 42) return 1;
|
||||
return 0;
|
||||
}
|
||||
8
tests/fixtures/defines/main.cpp
vendored
Normal file
8
tests/fixtures/defines/main.cpp
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import std;
|
||||
|
||||
static_assert(CRAFTER_TEST_FOO == 42, "CRAFTER_TEST_FOO should be 42");
|
||||
|
||||
int main() {
|
||||
if (CRAFTER_TEST_FOO != 42) return 1;
|
||||
return 0;
|
||||
}
|
||||
6
tests/fixtures/diamond/B/B.cppm
vendored
Normal file
6
tests/fixtures/diamond/B/B.cppm
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export module B;
|
||||
import X;
|
||||
|
||||
export int BValue() {
|
||||
return XValue() * 2;
|
||||
}
|
||||
6
tests/fixtures/diamond/C/C.cppm
vendored
Normal file
6
tests/fixtures/diamond/C/C.cppm
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export module C;
|
||||
import X;
|
||||
|
||||
export int CValue() {
|
||||
return XValue() * 3;
|
||||
}
|
||||
5
tests/fixtures/diamond/X/X.cppm
vendored
Normal file
5
tests/fixtures/diamond/X/X.cppm
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export module X;
|
||||
|
||||
export int XValue() {
|
||||
return 7;
|
||||
}
|
||||
9
tests/fixtures/diamond/main.cpp
vendored
Normal file
9
tests/fixtures/diamond/main.cpp
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import std;
|
||||
import B;
|
||||
import C;
|
||||
|
||||
int main() {
|
||||
if (BValue() != 14) return 1; // X(7) * 2
|
||||
if (CValue() != 21) return 1; // X(7) * 3
|
||||
return 0;
|
||||
}
|
||||
7
tests/fixtures/hello-world/main.cpp
vendored
Normal file
7
tests/fixtures/hello-world/main.cpp
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import std;
|
||||
|
||||
int main() {
|
||||
// hello-world is degenerate: the runner reports ✅ on exit 0, which is the
|
||||
// signal that the build produced a runnable binary.
|
||||
return 0;
|
||||
}
|
||||
6
tests/fixtures/incremental/interfaces/Greeter.cppm
vendored
Normal file
6
tests/fixtures/incremental/interfaces/Greeter.cppm
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export module Greeter;
|
||||
import std;
|
||||
|
||||
export std::string Greet(std::string_view name) {
|
||||
return std::format("hello, {}!", name);
|
||||
}
|
||||
7
tests/fixtures/incremental/main.cpp
vendored
Normal file
7
tests/fixtures/incremental/main.cpp
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import std;
|
||||
import Greeter;
|
||||
|
||||
int main() {
|
||||
std::println("{}", Greet("crafter"));
|
||||
return 0;
|
||||
}
|
||||
22
tests/fixtures/incremental/project.cpp
vendored
Normal file
22
tests/fixtures/incremental/project.cpp
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import std;
|
||||
import Crafter.Build;
|
||||
namespace fs = std::filesystem;
|
||||
using namespace Crafter;
|
||||
|
||||
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view>) {
|
||||
Configuration cfg;
|
||||
cfg.path = "./";
|
||||
cfg.name = "hello-mod";
|
||||
cfg.outputName = "hello-mod";
|
||||
cfg.target = "x86_64-pc-linux-gnu";
|
||||
cfg.type = ConfigurationType::Executable;
|
||||
|
||||
std::array<fs::path, 1> ifaces = { "interfaces/Greeter" };
|
||||
std::array<fs::path, 1> impls = { "main" };
|
||||
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
||||
|
||||
cfg.linkFlags.push_back("-Wl,--export-dynamic");
|
||||
cfg.linkFlags.push_back("-ldl");
|
||||
|
||||
return cfg;
|
||||
}
|
||||
6
tests/fixtures/libraries/greetlib/GreetLib.cppm
vendored
Normal file
6
tests/fixtures/libraries/greetlib/GreetLib.cppm
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export module GreetLib;
|
||||
import std;
|
||||
|
||||
export std::string Greet() {
|
||||
return "hi";
|
||||
}
|
||||
8
tests/fixtures/libraries/main.cpp
vendored
Normal file
8
tests/fixtures/libraries/main.cpp
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import std;
|
||||
import MathLib;
|
||||
import GreetLib;
|
||||
|
||||
int main() {
|
||||
std::println("{}={}", Greet(), Add(40, 2));
|
||||
return 0;
|
||||
}
|
||||
5
tests/fixtures/libraries/mathlib/MathLib.cppm
vendored
Normal file
5
tests/fixtures/libraries/mathlib/MathLib.cppm
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export module MathLib;
|
||||
|
||||
export int Add(int a, int b) {
|
||||
return a + b;
|
||||
}
|
||||
47
tests/fixtures/libraries/project.cpp
vendored
Normal file
47
tests/fixtures/libraries/project.cpp
vendored
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import std;
|
||||
import Crafter.Build;
|
||||
namespace fs = std::filesystem;
|
||||
using namespace Crafter;
|
||||
|
||||
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view>) {
|
||||
static auto MathStatic = std::make_unique<Configuration>();
|
||||
MathStatic->path = "./mathlib/";
|
||||
MathStatic->name = "MathLib";
|
||||
MathStatic->outputName = "MathLib";
|
||||
MathStatic->target = "x86_64-pc-linux-gnu";
|
||||
MathStatic->type = ConfigurationType::LibraryStatic;
|
||||
{
|
||||
std::array<fs::path, 1> ifaces = { "MathLib" };
|
||||
std::array<fs::path, 0> impls = {};
|
||||
MathStatic->GetInterfacesAndImplementations(ifaces, impls);
|
||||
}
|
||||
|
||||
static auto GreetDynamic = std::make_unique<Configuration>();
|
||||
GreetDynamic->path = "./greetlib/";
|
||||
GreetDynamic->name = "GreetLib";
|
||||
GreetDynamic->outputName = "GreetLib";
|
||||
GreetDynamic->target = "x86_64-pc-linux-gnu";
|
||||
GreetDynamic->type = ConfigurationType::LibraryDynamic;
|
||||
{
|
||||
std::array<fs::path, 1> ifaces = { "GreetLib" };
|
||||
std::array<fs::path, 0> impls = {};
|
||||
GreetDynamic->GetInterfacesAndImplementations(ifaces, impls);
|
||||
}
|
||||
|
||||
Configuration app;
|
||||
app.path = "./";
|
||||
app.name = "libs-app";
|
||||
app.outputName = "libs-app";
|
||||
app.target = "x86_64-pc-linux-gnu";
|
||||
app.type = ConfigurationType::Executable;
|
||||
app.dependencies = { MathStatic.get(), GreetDynamic.get() };
|
||||
{
|
||||
std::array<fs::path, 0> ifaces = {};
|
||||
std::array<fs::path, 1> impls = { "main" };
|
||||
app.GetInterfacesAndImplementations(ifaces, impls);
|
||||
}
|
||||
app.linkFlags.push_back("-Wl,--export-dynamic");
|
||||
app.linkFlags.push_back("-ldl");
|
||||
|
||||
return app;
|
||||
}
|
||||
27
tests/fixtures/qemu-runner/project.cpp
vendored
Normal file
27
tests/fixtures/qemu-runner/project.cpp
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import std;
|
||||
import Crafter.Build;
|
||||
namespace fs = std::filesystem;
|
||||
using namespace Crafter;
|
||||
|
||||
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view>) {
|
||||
Configuration cfg;
|
||||
cfg.path = "./";
|
||||
cfg.name = "qemu-meta";
|
||||
cfg.outputName = "qemu-meta";
|
||||
cfg.target = "x86_64-pc-linux-gnu";
|
||||
cfg.type = ConfigurationType::Executable;
|
||||
|
||||
Test t;
|
||||
t.config.path = "./";
|
||||
t.config.name = "Hello";
|
||||
t.config.outputName = "Hello";
|
||||
t.config.target = "x86_64-pc-linux-gnu";
|
||||
t.config.type = ConfigurationType::Executable;
|
||||
std::array<fs::path, 0> ifaces = {};
|
||||
std::array<fs::path, 1> impls = { "tests/Hello" };
|
||||
t.config.GetInterfacesAndImplementations(ifaces, impls);
|
||||
t.runner = TestRunner::FromEnv(t.config.target);
|
||||
cfg.tests.push_back(std::move(t));
|
||||
|
||||
return cfg;
|
||||
}
|
||||
6
tests/fixtures/qemu-runner/tests/Hello.cpp
vendored
Normal file
6
tests/fixtures/qemu-runner/tests/Hello.cpp
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import std;
|
||||
|
||||
int main() {
|
||||
std::println("hello-from-qemu");
|
||||
return 0;
|
||||
}
|
||||
33
tests/fixtures/runner-classification/project.cpp
vendored
Normal file
33
tests/fixtures/runner-classification/project.cpp
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import std;
|
||||
import Crafter.Build;
|
||||
namespace fs = std::filesystem;
|
||||
using namespace Crafter;
|
||||
|
||||
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view>) {
|
||||
Configuration cfg;
|
||||
cfg.path = "./";
|
||||
cfg.name = "rc-meta";
|
||||
cfg.outputName = "rc-meta";
|
||||
cfg.target = "x86_64-pc-linux-gnu";
|
||||
cfg.type = ConfigurationType::Executable;
|
||||
|
||||
auto addTest = [&](std::string name) {
|
||||
Test t;
|
||||
t.config.path = "./";
|
||||
t.config.name = name;
|
||||
t.config.outputName = name;
|
||||
t.config.target = "x86_64-pc-linux-gnu";
|
||||
t.config.type = ConfigurationType::Executable;
|
||||
std::array<fs::path, 0> empty = {};
|
||||
std::array<fs::path, 1> impls = { fs::path("tests") / name };
|
||||
t.config.GetInterfacesAndImplementations(empty, impls);
|
||||
cfg.tests.push_back(std::move(t));
|
||||
};
|
||||
|
||||
addTest("Pass");
|
||||
addTest("Fail");
|
||||
addTest("Crash");
|
||||
addTest("Hang");
|
||||
|
||||
return cfg;
|
||||
}
|
||||
4
tests/fixtures/runner-classification/tests/Crash.cpp
vendored
Normal file
4
tests/fixtures/runner-classification/tests/Crash.cpp
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
int main() {
|
||||
*(volatile int*)0 = 0;
|
||||
return 0;
|
||||
}
|
||||
1
tests/fixtures/runner-classification/tests/Fail.cpp
vendored
Normal file
1
tests/fixtures/runner-classification/tests/Fail.cpp
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
int main() { return 1; }
|
||||
3
tests/fixtures/runner-classification/tests/Hang.cpp
vendored
Normal file
3
tests/fixtures/runner-classification/tests/Hang.cpp
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
int main() {
|
||||
for (;;) {}
|
||||
}
|
||||
1
tests/fixtures/runner-classification/tests/Pass.cpp
vendored
Normal file
1
tests/fixtures/runner-classification/tests/Pass.cpp
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
int main() { return 0; }
|
||||
27
tests/fixtures/ssh-runner/project.cpp
vendored
Normal file
27
tests/fixtures/ssh-runner/project.cpp
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import std;
|
||||
import Crafter.Build;
|
||||
namespace fs = std::filesystem;
|
||||
using namespace Crafter;
|
||||
|
||||
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view>) {
|
||||
Configuration cfg;
|
||||
cfg.path = "./";
|
||||
cfg.name = "ssh-meta";
|
||||
cfg.outputName = "ssh-meta";
|
||||
cfg.target = "x86_64-pc-linux-gnu";
|
||||
cfg.type = ConfigurationType::Executable;
|
||||
|
||||
Test t;
|
||||
t.config.path = "./";
|
||||
t.config.name = "Hello";
|
||||
t.config.outputName = "Hello";
|
||||
t.config.target = "x86_64-pc-linux-gnu";
|
||||
t.config.type = ConfigurationType::Executable;
|
||||
std::array<fs::path, 0> ifaces = {};
|
||||
std::array<fs::path, 1> impls = { "tests/Hello" };
|
||||
t.config.GetInterfacesAndImplementations(ifaces, impls);
|
||||
t.runner = TestRunner::FromEnv(t.config.target);
|
||||
cfg.tests.push_back(std::move(t));
|
||||
|
||||
return cfg;
|
||||
}
|
||||
6
tests/fixtures/ssh-runner/tests/Hello.cpp
vendored
Normal file
6
tests/fixtures/ssh-runner/tests/Hello.cpp
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import std;
|
||||
|
||||
int main() {
|
||||
std::println("hello-from-ssh");
|
||||
return 0;
|
||||
}
|
||||
6
tests/fixtures/windows-via-ssh/main.cpp
vendored
Normal file
6
tests/fixtures/windows-via-ssh/main.cpp
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import std;
|
||||
|
||||
int main() {
|
||||
std::println("hi from windows");
|
||||
return 0;
|
||||
}
|
||||
30
tests/fixtures/windows-via-ssh/project.cpp
vendored
Normal file
30
tests/fixtures/windows-via-ssh/project.cpp
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import std;
|
||||
import Crafter.Build;
|
||||
namespace fs = std::filesystem;
|
||||
using namespace Crafter;
|
||||
|
||||
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view>) {
|
||||
Configuration cfg;
|
||||
cfg.path = "./";
|
||||
cfg.name = "winhello-meta";
|
||||
cfg.outputName = "winhello-meta";
|
||||
cfg.target = "x86_64-pc-linux-gnu";
|
||||
cfg.type = ConfigurationType::Executable;
|
||||
|
||||
Test t;
|
||||
t.config.path = "./";
|
||||
t.config.name = "winhello";
|
||||
t.config.outputName = "winhello";
|
||||
t.config.target = "x86_64-w64-mingw32";
|
||||
t.config.type = ConfigurationType::Executable;
|
||||
std::array<fs::path, 0> ifaces = {};
|
||||
std::array<fs::path, 1> impls = { "main" };
|
||||
t.config.GetInterfacesAndImplementations(ifaces, impls);
|
||||
t.config.linkFlags.push_back("-fuse-ld=lld");
|
||||
t.config.linkFlags.push_back("-lstdc++exp");
|
||||
|
||||
t.runner = TestRunner::FromEnv(t.config.target);
|
||||
|
||||
cfg.tests.push_back(std::move(t));
|
||||
return cfg;
|
||||
}
|
||||
6
tests/fixtures/with-module/interfaces/Greeter.cppm
vendored
Normal file
6
tests/fixtures/with-module/interfaces/Greeter.cppm
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export module Greeter;
|
||||
import std;
|
||||
|
||||
export std::string Greet(std::string_view name) {
|
||||
return std::format("hello, {}!", name);
|
||||
}
|
||||
7
tests/fixtures/with-module/main.cpp
vendored
Normal file
7
tests/fixtures/with-module/main.cpp
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import std;
|
||||
import Greeter;
|
||||
|
||||
int main() {
|
||||
if (Greet("crafter") != "hello, crafter!") return 1;
|
||||
return 0;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue