Crafter.Build/tests/WasmExceptions/main.cpp

77 lines
3.3 KiB
C++
Raw Normal View History

// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
import std;
import Crafter.Build;
namespace fs = std::filesystem;
using namespace Crafter;
// C++ exceptions in a wasm build, run under wasmtime: the fixture throws
// through frames whose destructors have to run, catches the exception,
// catches one that libc++ throws itself (vector::at), and with an argument
// lets one escape main, which has to fail the run rather than exit 0.
//
// wasm builds used to be compiled with -fno-exceptions, so a throw did not
// compile and libc++'s own throws aborted. Each part of turning them on fails
// in its own way, and this is the test that sees it: a std module or a unit
// built without exceptions (clang rejects the BMI), a link without libunwind
// (undefined __cpp_exception), or LLVM's default legacy encoding, which
// wasmtime refuses to load. Skips like WasmHelloRuns when there is no
// exception-enabled WASI sysroot or no wasmtime; inside the Flatpak both are
// bundled.
namespace {
std::int32_t failures = 0;
void Check(bool cond, std::string_view msg) {
if (!cond) {
std::println(std::cerr, "FAIL: {}", msg);
++failures;
}
}
}
int main() {
const Toolchain& tc = GetToolchain();
const fs::path libDir = tc.wasiSysroot / "lib" / "wasm32-wasip1";
if (!fs::exists(libDir / "eh" / "libunwind.a") && !fs::exists(libDir / "libunwind.a")) {
std::println(std::cerr, "SKIP: no exception-enabled WASI sysroot at {}", tc.wasiSysroot.string());
return 77;
}
if (std::system("which wasmtime > /dev/null 2>&1") != 0) {
std::println(std::cerr, "SKIP: wasmtime not on PATH");
return 77;
}
Configuration cfg;
cfg.path = fs::current_path() / "tests" / "WasmExceptions" / "fixture";
cfg.name = "wasm-exceptions";
cfg.outputName = "wasm-exceptions";
cfg.target = "wasm32-wasip1";
cfg.type = ConfigurationType::Executable;
std::array<fs::path, 0> ifaces = {};
std::array<fs::path, 1> impls = { "main" };
cfg.GetInterfacesAndImplementations(ifaces, impls);
std::unordered_map<fs::path, std::shared_future<BuildResult>> depResults;
std::mutex depMutex;
const BuildResult r = Build(cfg, depResults, depMutex);
if (!r.result.empty()) {
std::println(std::cerr, "build failed: {}", r.result);
return 1;
}
const fs::path wasm = ExpectedOutput(cfg);
const auto caught = RunCommandWithTimeout(std::format("wasmtime {}", wasm.string()), std::chrono::seconds(60));
Check(caught.exitCode == 0, std::format("the run that catches everything exits 0 (exit {}): {}", caught.exitCode, caught.output));
Check(caught.output.contains("caught: thrown in wasm after 4 destructors"), std::format("the exception is caught after unwinding all four frames: {}", caught.output));
Check(caught.output.contains("caught: out_of_range from libc++"), std::format("libc++'s own exception is catchable: {}", caught.output));
const auto escaped = RunCommandWithTimeout(std::format("wasmtime {} uncaught", wasm.string()), std::chrono::seconds(60));
Check(escaped.exitCode != 0, std::format("an exception escaping main fails the run: {}", escaped.output));
if (failures > 0) {
std::println(std::cerr, "{} assertions failed", failures);
return 1;
}
return 0;
}