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
|
|
@ -271,6 +271,7 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
cfg.AddTest("WasmHelloRuns").Dependencies({ CrafterBuildLib.get() });
|
cfg.AddTest("WasmHelloRuns").Dependencies({ CrafterBuildLib.get() });
|
||||||
cfg.AddTest("WasmExceptions").Dependencies({ CrafterBuildLib.get() });
|
cfg.AddTest("WasmExceptions").Dependencies({ CrafterBuildLib.get() });
|
||||||
cfg.AddTest("WasiSysrootWithoutExceptions").Dependencies({ CrafterBuildLib.get() });
|
cfg.AddTest("WasiSysrootWithoutExceptions").Dependencies({ CrafterBuildLib.get() });
|
||||||
|
cfg.AddTest("WasmBuildRefusals").Dependencies({ CrafterBuildLib.get() });
|
||||||
cfg.AddTest("RunSingleTestExit").Dependencies({ CrafterBuildLib.get() });
|
cfg.AddTest("RunSingleTestExit").Dependencies({ CrafterBuildLib.get() });
|
||||||
// LoadProject dlopens the synthesized project.so, which references
|
// LoadProject dlopens the synthesized project.so, which references
|
||||||
// Crafter:: symbols (HostTarget, Configuration ctors) that have to be
|
// Crafter:: symbols (HostTarget, Configuration ctors) that have to be
|
||||||
|
|
|
||||||
|
|
@ -12,16 +12,20 @@ using namespace Crafter;
|
||||||
// a sysroot that lacks them (an older wasi-sdk, one built without its
|
// a sysroot that lacks them (an older wasi-sdk, one built without its
|
||||||
// exceptions option) otherwise compiles and then fails the first link with
|
// exceptions option) otherwise compiles and then fails the first link with
|
||||||
// "unable to find library -lunwind", which does not say why it was asked
|
// "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
|
// for. A sysroot that is not there at all, or has nothing for the target,
|
||||||
// needs no working wasm toolchain.
|
// gets a message of its own rather than that one. The checks run before
|
||||||
|
// anything is made, so a refused build leaves no bin/ behind, and they need
|
||||||
|
// no working wasm toolchain.
|
||||||
//
|
//
|
||||||
// A scratch sysroot is taken through three states: no libunwind; libunwind
|
// A scratch sysroot is taken through these states: missing; without
|
||||||
// under eh/; libunwind in the triple's own directory, which the driver
|
// lib/wasm32-wasip1; without libunwind, for an executable and for a static
|
||||||
// searches after eh/. The last two pass the check and fail later on the
|
// library, which links nothing and passes; with libunwind under eh/; with
|
||||||
// sysroot's empty std module source, which is not what is under test. The
|
// libunwind in the triple's own directory, which the driver searches after
|
||||||
// cache is redirected into the scratch directory as well: the std PCM cache
|
// eh/. The cases that pass the check fail later on the sysroot's empty std
|
||||||
// is not keyed on the sysroot, and a PCM built from the fake source must not
|
// module source, which is not what is under test. The cache is redirected
|
||||||
// land where the real wasm tests read theirs.
|
// 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);
|
extern "C" int setenv(const char* name, const char* value, int overwrite);
|
||||||
|
|
||||||
|
|
@ -35,14 +39,14 @@ namespace {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BuildResult BuildAgainst(const fs::path& project, const fs::path& sysroot) {
|
BuildResult BuildAgainst(const fs::path& project, const fs::path& sysroot, ConfigurationType type = ConfigurationType::Executable) {
|
||||||
Configuration app;
|
Configuration app;
|
||||||
app.path = project;
|
app.path = project;
|
||||||
app.name = "hello";
|
app.name = "hello";
|
||||||
app.outputName = "hello";
|
app.outputName = "hello";
|
||||||
app.target = "wasm32-wasip1";
|
app.target = "wasm32-wasip1";
|
||||||
app.sysroot = sysroot.string();
|
app.sysroot = sysroot.string();
|
||||||
app.type = ConfigurationType::Executable;
|
app.type = type;
|
||||||
std::array<fs::path, 0> ifaces = {};
|
std::array<fs::path, 0> ifaces = {};
|
||||||
std::array<fs::path, 1> impls = { "main" };
|
std::array<fs::path, 1> impls = { "main" };
|
||||||
app.GetInterfacesAndImplementations(ifaces, impls);
|
app.GetInterfacesAndImplementations(ifaces, impls);
|
||||||
|
|
@ -68,16 +72,27 @@ int main() {
|
||||||
|
|
||||||
const fs::path sysroot = scratch / "sysroot";
|
const fs::path sysroot = scratch / "sysroot";
|
||||||
const fs::path libDir = sysroot / "lib" / "wasm32-wasip1";
|
const fs::path libDir = sysroot / "lib" / "wasm32-wasip1";
|
||||||
fs::create_directories(libDir / "eh");
|
{
|
||||||
|
const BuildResult r = BuildAgainst(project, sysroot);
|
||||||
|
Check(r.result.contains(std::format("no WASI sysroot at {}", sysroot.string())), std::format("a missing sysroot is named as missing: {}", r.result));
|
||||||
|
}
|
||||||
|
|
||||||
fs::create_directories(sysroot / "share" / "libc++" / "v1");
|
fs::create_directories(sysroot / "share" / "libc++" / "v1");
|
||||||
std::ofstream(sysroot / "share" / "libc++" / "v1" / "std.cppm");
|
std::ofstream(sysroot / "share" / "libc++" / "v1" / "std.cppm");
|
||||||
|
{
|
||||||
|
const BuildResult r = BuildAgainst(project, sysroot);
|
||||||
|
Check(r.result.contains(std::format("has nothing for wasm32-wasip1 (no {})", libDir.string())), std::format("a sysroot without the target's directory says so: {}", r.result));
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::create_directories(libDir / "eh");
|
||||||
constexpr std::string_view missing = "has no C++ exception support";
|
constexpr std::string_view missing = "has no C++ exception support";
|
||||||
{
|
{
|
||||||
const BuildResult r = BuildAgainst(project, sysroot);
|
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(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));
|
Check(r.result.contains((libDir / "eh" / "libunwind.a").string()), std::format("the message names the library it looked for: {}", r.result));
|
||||||
|
Check(!fs::exists(project / "bin") && !fs::exists(project / "build"), "a refused build leaves no bin/ or build/ behind");
|
||||||
}
|
}
|
||||||
|
Check(!BuildAgainst(project, sysroot, ConfigurationType::LibraryStatic).result.contains(missing), "a static library links nothing and passes without libunwind");
|
||||||
|
|
||||||
std::ofstream(libDir / "eh" / "libunwind.a");
|
std::ofstream(libDir / "eh" / "libunwind.a");
|
||||||
Check(!BuildAgainst(project, sysroot).result.contains(missing), "libunwind under eh/ passes the check");
|
Check(!BuildAgainst(project, sysroot).result.contains(missing), "libunwind under eh/ passes the check");
|
||||||
|
|
|
||||||
6
tests/WasmBuildRefusals/fixture/main.cpp
Normal file
6
tests/WasmBuildRefusals/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;
|
||||||
|
}
|
||||||
85
tests/WasmBuildRefusals/main.cpp
Normal file
85
tests/WasmBuildRefusals/main.cpp
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// What a wasm32 build refuses before it starts, now that every wasm unit has
|
||||||
|
// C++ exceptions, each with a message naming the cause:
|
||||||
|
// - -flto in compileFlags or linkFlags. LTO code generation drops the
|
||||||
|
// exception handling, and the build used to succeed with every throw
|
||||||
|
// escaping its catch (WebAssembly/wasi-sdk#629).
|
||||||
|
// - -fno-exceptions. The std module is built with exceptions, and every
|
||||||
|
// `import std;` failed on a BMI configuration mismatch.
|
||||||
|
// - A shared library. The exception-enabled libc++ is static archives
|
||||||
|
// only, and the link failed on a relocation that wants -fPIC.
|
||||||
|
// None of them leaves bin/ or build/ behind, and none needs a working wasm
|
||||||
|
// toolchain. The sysroot given does not exist, so a refusal that did not
|
||||||
|
// fire would show up as the missing sysroot instead.
|
||||||
|
namespace {
|
||||||
|
std::int32_t failures = 0;
|
||||||
|
|
||||||
|
void Check(bool cond, std::string_view msg) {
|
||||||
|
if (!cond) {
|
||||||
|
std::println(std::cerr, "FAIL: {}", msg);
|
||||||
|
++failures;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Refusal {
|
||||||
|
std::string_view what;
|
||||||
|
std::vector<std::string> compileFlags;
|
||||||
|
std::vector<std::string> linkFlags;
|
||||||
|
ConfigurationType type;
|
||||||
|
std::string_view expected;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
// Build() reports a missing compiler before it gets to any of this.
|
||||||
|
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-wasm-build-refusals";
|
||||||
|
fs::remove_all(scratch);
|
||||||
|
fs::create_directories(scratch);
|
||||||
|
const fs::path project = scratch / "project";
|
||||||
|
fs::copy(fs::current_path() / "tests" / "WasmBuildRefusals" / "fixture", project, fs::copy_options::recursive);
|
||||||
|
|
||||||
|
const std::array refusals = {
|
||||||
|
Refusal{ "-flto in compileFlags", { "-flto=thin" }, {}, ConfigurationType::Executable, "-flto=thin is not supported on wasm32: LTO code generation drops C++ exception handling" },
|
||||||
|
Refusal{ "-flto in linkFlags", {}, { "-flto" }, ConfigurationType::Executable, "-flto is not supported on wasm32" },
|
||||||
|
Refusal{ "-fno-exceptions", { "-fno-exceptions" }, {}, ConfigurationType::Executable, "-fno-exceptions is not supported on wasm32" },
|
||||||
|
Refusal{ "a shared library", {}, {}, ConfigurationType::LibraryDynamic, "wasm32 shared libraries are not supported" },
|
||||||
|
};
|
||||||
|
for (const Refusal& refusal : refusals) {
|
||||||
|
Configuration app;
|
||||||
|
app.path = project;
|
||||||
|
app.name = "hello";
|
||||||
|
app.outputName = "hello";
|
||||||
|
app.target = "wasm32-wasip1";
|
||||||
|
app.sysroot = (scratch / "no-sysroot").string();
|
||||||
|
app.type = refusal.type;
|
||||||
|
app.compileFlags = refusal.compileFlags;
|
||||||
|
app.linkFlags = refusal.linkFlags;
|
||||||
|
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;
|
||||||
|
const BuildResult r = Build(app, depResults, depMutex);
|
||||||
|
Check(r.result.contains(refusal.expected), std::format("{} is refused with its cause: {}", refusal.what, r.result));
|
||||||
|
Check(!fs::exists(project / "bin") && !fs::exists(project / "build"), std::format("refusing {} leaves no bin/ or build/ behind", refusal.what));
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::remove_all(scratch);
|
||||||
|
if (failures > 0) {
|
||||||
|
std::println(std::cerr, "{} assertions failed", failures);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
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;
|
import std;
|
||||||
|
|
||||||
|
// jump.c, compiled as C.
|
||||||
|
extern "C" std::int32_t JumpRoundTrip();
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
std::int32_t unwound = 0;
|
std::int32_t unwound = 0;
|
||||||
|
|
||||||
|
|
@ -31,5 +34,13 @@ int main(int argc, char** argv) {
|
||||||
} catch (const std::out_of_range&) {
|
} catch (const std::out_of_range&) {
|
||||||
std::println("caught: out_of_range from libc++");
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,17 +8,20 @@ using namespace Crafter;
|
||||||
|
|
||||||
// C++ exceptions in a wasm build, run under wasmtime: the fixture throws
|
// C++ exceptions in a wasm build, run under wasmtime: the fixture throws
|
||||||
// through frames whose destructors have to run, catches the exception,
|
// through frames whose destructors have to run, catches the exception,
|
||||||
// catches one that libc++ throws itself (vector::at), and with an argument
|
// catches one from a libc++ header (vector::at) and one thrown inside
|
||||||
// lets one escape main, which has to fail the run rather than exit 0.
|
// 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
|
// 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
|
// 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
|
// 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
|
// built without exceptions (clang rejects the BMI), a link without libunwind
|
||||||
// (undefined __cpp_exception), or LLVM's default legacy encoding, which
|
// (undefined __cpp_exception), the libc++ built without exceptions (stoi
|
||||||
// wasmtime refuses to load. Skips like WasmHelloRuns when there is no
|
// aborts), or LLVM's default legacy encoding, which wasmtime refuses to load.
|
||||||
// exception-enabled WASI sysroot or no wasmtime; inside the Flatpak both are
|
// Skips like WasmHelloRuns when there is no exception-enabled WASI sysroot or
|
||||||
// bundled.
|
// no wasmtime; inside the Flatpak both are bundled.
|
||||||
namespace {
|
namespace {
|
||||||
std::int32_t failures = 0;
|
std::int32_t failures = 0;
|
||||||
|
|
||||||
|
|
@ -51,6 +54,10 @@ int main() {
|
||||||
std::array<fs::path, 0> ifaces = {};
|
std::array<fs::path, 0> ifaces = {};
|
||||||
std::array<fs::path, 1> impls = { "main" };
|
std::array<fs::path, 1> impls = { "main" };
|
||||||
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
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::unordered_map<fs::path, std::shared_future<BuildResult>> depResults;
|
||||||
std::mutex depMutex;
|
std::mutex depMutex;
|
||||||
|
|
@ -64,10 +71,12 @@ int main() {
|
||||||
const auto caught = RunCommandWithTimeout(std::format("wasmtime {}", wasm.string()), std::chrono::seconds(60));
|
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.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: 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));
|
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) {
|
if (failures > 0) {
|
||||||
std::println(std::cerr, "{} assertions failed", failures);
|
std::println(std::cerr, "{} assertions failed", failures);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue