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
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