test(wasm): the cases the review found
WasmBuildRefusals checks that -flto in compileFlags and in linkFlags, -fno-exceptions and a shared library are each refused with their cause, and that none of them leaves bin/ or build/ behind. With the check disabled, each of them failed on a missing std.cppm instead, after the build directories had been made. WasiSysrootWithoutExceptions also takes its sysroot through missing and without the target's directory, each with a message of its own, checks that a refused build leaves nothing behind, and that a static library passes without libunwind. WasmExceptions catches an exception thrown inside libc++.a (stoi). vector::at throws from a header compiled into the fixture, so the fixture also passed against the libc++ built without exceptions. Its C source uses setjmp, which the old C flags did not compile. An exception that escapes main has to end the run as wasmtime's uncaught exception, where any nonzero exit used to pass, a trap or a timeout included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
45b9f5cbb7
commit
2a05b8e6fb
7 changed files with 171 additions and 20 deletions
24
tests/WasmExceptions/fixture/jump.c
Normal file
24
tests/WasmExceptions/fixture/jump.c
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
#include <setjmp.h>
|
||||
#include <stdint.h>
|
||||
|
||||
static jmp_buf target;
|
||||
|
||||
static void JumpBack(int32_t value) {
|
||||
longjmp(target, value);
|
||||
}
|
||||
|
||||
// setjmp's value may only be compared or switched on, never stored.
|
||||
int32_t JumpRoundTrip(void) {
|
||||
switch (setjmp(target)) {
|
||||
case 0:
|
||||
JumpBack(7);
|
||||
return 0;
|
||||
case 7:
|
||||
return 7;
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,9 @@
|
|||
|
||||
import std;
|
||||
|
||||
// jump.c, compiled as C.
|
||||
extern "C" std::int32_t JumpRoundTrip();
|
||||
|
||||
namespace {
|
||||
std::int32_t unwound = 0;
|
||||
|
||||
|
|
@ -31,5 +34,13 @@ int main(int argc, char** argv) {
|
|||
} catch (const std::out_of_range&) {
|
||||
std::println("caught: out_of_range from libc++");
|
||||
}
|
||||
// vector::at throws from a header, compiled into this unit; stoi throws
|
||||
// inside libc++.a, which therefore has to be the exception-enabled one.
|
||||
try {
|
||||
std::println("unreachable: {}", std::stoi("not a number"));
|
||||
} catch (const std::invalid_argument&) {
|
||||
std::println("caught: invalid_argument from inside libc++");
|
||||
}
|
||||
std::println("longjmp came back with {}", JumpRoundTrip());
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,17 +8,20 @@ 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.
|
||||
// catches one from a libc++ header (vector::at) and one thrown inside
|
||||
// libc++.a itself (stoi), and with an argument lets one escape main, which
|
||||
// has to end the run as an uncaught exception rather than exit 0, trap or
|
||||
// hang. Its C source uses setjmp, which compiles and links next to the C++
|
||||
// objects only when C gets the same wasm flags.
|
||||
//
|
||||
// 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.
|
||||
// (undefined __cpp_exception), the libc++ built without exceptions (stoi
|
||||
// aborts), 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;
|
||||
|
||||
|
|
@ -51,6 +54,10 @@ int main() {
|
|||
std::array<fs::path, 0> ifaces = {};
|
||||
std::array<fs::path, 1> impls = { "main" };
|
||||
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
||||
cfg.cFiles = { cfg.path / "jump" };
|
||||
// setjmp and longjmp live in libsetjmp, which the build does not link
|
||||
// on its own.
|
||||
cfg.linkFlags.push_back("-lsetjmp");
|
||||
|
||||
std::unordered_map<fs::path, std::shared_future<BuildResult>> depResults;
|
||||
std::mutex depMutex;
|
||||
|
|
@ -64,10 +71,12 @@ int main() {
|
|||
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));
|
||||
Check(caught.output.contains("caught: out_of_range from libc++"), std::format("an exception from a libc++ header is catchable: {}", caught.output));
|
||||
Check(caught.output.contains("caught: invalid_argument from inside libc++"), std::format("an exception from inside libc++.a is catchable: {}", caught.output));
|
||||
Check(caught.output.contains("longjmp came back with 7"), std::format("setjmp in a C source works next to the C++ objects: {}", 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));
|
||||
Check(escaped.exitCode != 0 && !escaped.timedOut && !escaped.crashed && escaped.output.contains("thrown Wasm exception"), std::format("an exception escaping main ends the run as an uncaught exception (exit {}): {}", escaped.exitCode, escaped.output));
|
||||
|
||||
if (failures > 0) {
|
||||
std::println(std::cerr, "{} assertions failed", failures);
|
||||
|
|
|
|||
Loading…
Reference in a new issue