test(wasm): exceptions in a wasm build, and a sysroot without them
WasmExceptions builds a fixture for wasm32-wasip1 and runs it under
wasmtime. It throws through four frames whose destructors have to run,
catches the exception, catches one that vector::at throws, and with an
argument lets one escape main, which has to fail the run. With the
previous flags the fixture does not compile ("cannot use 'throw' with
exceptions disabled"), and each part of turning exceptions on fails it
in its own way, from a BMI built without them to the legacy encoding
wasmtime refuses.
WasiSysrootWithoutExceptions takes a scratch sysroot through three
states, no libunwind, libunwind under eh/ and libunwind in the
triple's own directory, and checks that only the first stops the
build, with the message naming the library it looked for. It needs no
working wasm toolchain, so it also runs where the others skip.
WasmHelloRuns and WasmExceptions treat a sysroot without the
exception-enabled libraries as missing and skip. The manifest tests
expect the exception-handling probe on every entry.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
63c4040000
commit
8862092b63
8 changed files with 233 additions and 5 deletions
|
|
@ -268,6 +268,8 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
|||
cfg.AddTest("WasiBrowserRuntime").Dependencies({ CrafterBuildLib.get() });
|
||||
cfg.AddTest("WasmVariants").Dependencies({ CrafterBuildLib.get() });
|
||||
cfg.AddTest("WasmHelloRuns").Dependencies({ CrafterBuildLib.get() });
|
||||
cfg.AddTest("WasmExceptions").Dependencies({ CrafterBuildLib.get() });
|
||||
cfg.AddTest("WasiSysrootWithoutExceptions").Dependencies({ CrafterBuildLib.get() });
|
||||
cfg.AddTest("RunSingleTestExit").Dependencies({ CrafterBuildLib.get() });
|
||||
// LoadProject dlopens the synthesized project.so, which references
|
||||
// Crafter:: symbols (HostTarget, Configuration ctors) that have to be
|
||||
|
|
|
|||
|
|
@ -87,7 +87,9 @@ int main() {
|
|||
}
|
||||
|
||||
// With no declared variants, variants.json is a single baseline entry
|
||||
// pointing at outputName.wasm with no probes.
|
||||
// pointing at outputName.wasm, with only the probe for what every wasm
|
||||
// build needs: exception handling, so that runtime.js can turn an engine
|
||||
// without it away with a message rather than a CompileError.
|
||||
{
|
||||
std::ifstream vin(*variants);
|
||||
std::string vjson{std::istreambuf_iterator<char>(vin), std::istreambuf_iterator<char>()};
|
||||
|
|
@ -95,6 +97,10 @@ int main() {
|
|||
std::println(std::cerr, "variants.json missing baseline url: {}", vjson);
|
||||
return 1;
|
||||
}
|
||||
if (vjson.find("\"probes\":[\"exception-handling\"]") == std::string::npos) {
|
||||
std::println(std::cerr, "variants.json baseline does not probe for exception handling: {}", vjson);
|
||||
return 1;
|
||||
}
|
||||
if (vjson.find("relaxed-simd") != std::string::npos) {
|
||||
std::println(std::cerr, "variants.json unexpectedly mentions relaxed-simd: {}", vjson);
|
||||
return 1;
|
||||
|
|
|
|||
6
tests/WasiSysrootWithoutExceptions/fixture/main.cpp
Normal file
6
tests/WasiSysrootWithoutExceptions/fixture/main.cpp
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
int main() {
|
||||
return 0;
|
||||
}
|
||||
95
tests/WasiSysrootWithoutExceptions/main.cpp
Normal file
95
tests/WasiSysrootWithoutExceptions/main.cpp
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
// 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;
|
||||
|
||||
// A WASI sysroot without exception support has to stop a wasm build with one
|
||||
// line that says so. wasm builds compile with -fwasm-exceptions and link the
|
||||
// libc++ and libunwind that wasi-sdk 33 and later ship under lib/<triple>/eh;
|
||||
// a sysroot that lacks them (an older wasi-sdk, one built without its
|
||||
// exceptions option) otherwise compiles and then fails the first link with
|
||||
// "unable to find library -lunwind", which does not say why it was asked
|
||||
// for. The check runs before the std module is built, so the first case
|
||||
// needs no working wasm toolchain.
|
||||
//
|
||||
// A scratch sysroot is taken through three states: no libunwind; libunwind
|
||||
// under eh/; libunwind in the triple's own directory, which the driver
|
||||
// searches after eh/. The last two pass the check and fail later on the
|
||||
// sysroot's empty std module source, which is not what is under test. The
|
||||
// cache is redirected into the scratch directory as well: the std PCM cache
|
||||
// is not keyed on the sysroot, and a PCM built from the fake source must not
|
||||
// land where the real wasm tests read theirs.
|
||||
|
||||
extern "C" int setenv(const char* name, const char* value, int overwrite);
|
||||
|
||||
namespace {
|
||||
std::int32_t failures = 0;
|
||||
|
||||
void Check(bool cond, std::string_view msg) {
|
||||
if (!cond) {
|
||||
std::println(std::cerr, "FAIL: {}", msg);
|
||||
++failures;
|
||||
}
|
||||
}
|
||||
|
||||
BuildResult BuildAgainst(const fs::path& project, const fs::path& sysroot) {
|
||||
Configuration app;
|
||||
app.path = project;
|
||||
app.name = "hello";
|
||||
app.outputName = "hello";
|
||||
app.target = "wasm32-wasip1";
|
||||
app.sysroot = sysroot.string();
|
||||
app.type = ConfigurationType::Executable;
|
||||
std::array<fs::path, 0> ifaces = {};
|
||||
std::array<fs::path, 1> impls = { "main" };
|
||||
app.GetInterfacesAndImplementations(ifaces, impls);
|
||||
std::unordered_map<fs::path, std::shared_future<BuildResult>> depResults;
|
||||
std::mutex depMutex;
|
||||
return Build(app, depResults, depMutex);
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
// Build() reports a missing compiler before it gets to the sysroot.
|
||||
if (const Toolchain& tc = GetToolchain(); !tc.error.empty()) {
|
||||
std::println(std::cerr, "SKIP: {}", tc.error);
|
||||
return 77;
|
||||
}
|
||||
|
||||
const fs::path scratch = fs::temp_directory_path() / "crafter-build-wasi-sysroot-without-exceptions";
|
||||
fs::remove_all(scratch);
|
||||
fs::create_directories(scratch);
|
||||
setenv("XDG_CACHE_HOME", (scratch / "cache").c_str(), 1);
|
||||
const fs::path project = scratch / "project";
|
||||
fs::copy(fs::current_path() / "tests" / "WasiSysrootWithoutExceptions" / "fixture", project, fs::copy_options::recursive);
|
||||
|
||||
const fs::path sysroot = scratch / "sysroot";
|
||||
const fs::path libDir = sysroot / "lib" / "wasm32-wasip1";
|
||||
fs::create_directories(libDir / "eh");
|
||||
fs::create_directories(sysroot / "share" / "libc++" / "v1");
|
||||
std::ofstream(sysroot / "share" / "libc++" / "v1" / "std.cppm");
|
||||
|
||||
constexpr std::string_view missing = "has no C++ exception support";
|
||||
{
|
||||
const BuildResult r = BuildAgainst(project, sysroot);
|
||||
Check(r.result.contains(missing), std::format("without libunwind the build stops on the sysroot: {}", r.result));
|
||||
Check(r.result.contains((libDir / "eh" / "libunwind.a").string()), std::format("the message names the library it looked for: {}", r.result));
|
||||
}
|
||||
|
||||
std::ofstream(libDir / "eh" / "libunwind.a");
|
||||
Check(!BuildAgainst(project, sysroot).result.contains(missing), "libunwind under eh/ passes the check");
|
||||
|
||||
fs::remove(libDir / "eh" / "libunwind.a");
|
||||
std::ofstream(libDir / "libunwind.a");
|
||||
Check(!BuildAgainst(project, sysroot).result.contains(missing), "libunwind in the triple's directory passes the check");
|
||||
|
||||
fs::remove_all(scratch);
|
||||
if (failures > 0) {
|
||||
std::println(std::cerr, "{} assertions failed", failures);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
35
tests/WasmExceptions/fixture/main.cpp
Normal file
35
tests/WasmExceptions/fixture/main.cpp
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
import std;
|
||||
|
||||
namespace {
|
||||
std::int32_t unwound = 0;
|
||||
|
||||
struct Guard {
|
||||
~Guard() { ++unwound; }
|
||||
};
|
||||
|
||||
void Throw(std::int32_t depth) {
|
||||
Guard guard;
|
||||
if (depth == 0) throw std::runtime_error("thrown in wasm");
|
||||
Throw(depth - 1);
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
// With an argument, nothing catches: the run has to fail.
|
||||
if (argc > 1) Throw(0);
|
||||
try {
|
||||
Throw(3);
|
||||
} catch (const std::runtime_error& e) {
|
||||
std::println("caught: {} after {} destructors", e.what(), unwound);
|
||||
}
|
||||
try {
|
||||
const std::vector<std::int32_t> empty;
|
||||
std::println("unreachable: {}", empty.at(1));
|
||||
} catch (const std::out_of_range&) {
|
||||
std::println("caught: out_of_range from libc++");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
77
tests/WasmExceptions/main.cpp
Normal file
77
tests/WasmExceptions/main.cpp
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
// 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;
|
||||
}
|
||||
|
|
@ -11,12 +11,15 @@ using namespace Crafter;
|
|||
// wasmtime. The other wasm tests only inspect generated files; this is the
|
||||
// one that proves the sysroot, the std module for wasm and wasm-ld all work
|
||||
// together. It skips when the sysroot or wasmtime is missing, so a
|
||||
// development box without them is not a failure. Inside the Flatpak both are
|
||||
// bundled, and there the skip would be the failure to look for.
|
||||
// development box without them is not a failure; a sysroot without the
|
||||
// exception-enabled libraries counts as missing, since wasm builds link them
|
||||
// (WasiSysrootWithoutExceptions covers that message). Inside the Flatpak both
|
||||
// are bundled, and there the skip would be the failure to look for.
|
||||
int main() {
|
||||
const Toolchain& tc = GetToolchain();
|
||||
if (!fs::exists(tc.wasiSysroot / "include")) {
|
||||
std::println(std::cerr, "SKIP: no WASI sysroot at {}", tc.wasiSysroot.string());
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -97,6 +97,10 @@ int main() {
|
|||
Check(j.find("\"label\":\"relaxed-simd\"") != std::string::npos, "manifest has relaxed-simd label");
|
||||
Check(j.find("\"wvhelper.relaxed-simd.wasm\"") != std::string::npos, "manifest has labelled url");
|
||||
Check(j.find("\"relaxed-simd\"") != std::string::npos, "manifest has relaxed-simd probe");
|
||||
// Every file is built with exceptions, so every entry needs them
|
||||
// besides its own features; the baseline needs nothing else.
|
||||
Check(j.find("\"probes\":[\"relaxed-simd\",\"exception-handling\"]") != std::string::npos, "relaxed-simd entry also probes for exception handling");
|
||||
Check(j.find("\"url\":\"wvhelper.wasm\",\"probes\":[\"exception-handling\"]") != std::string::npos, "baseline entry probes for exception handling only");
|
||||
// baseline fallback entry.
|
||||
Check(j.find("\"wvhelper.wasm\"") != std::string::npos, "manifest has baseline url");
|
||||
// The relaxed-simd variant must precede the baseline so the runtime
|
||||
|
|
|
|||
Loading…
Reference in a new issue