test(wasi-runtime): exit and park, under node
runtime.js had no test that runs it. WasiBrowserRuntimeExit runs the real file under node, once per scenario, against a module assembled in the harness. Between _start and the call that ends it the module has the frames that caught the old exit: a destructor (catch_all_ref, then throw_ref), a catch (...) and a noexcept function, each reporting when it runs. An exit has to run none of them and record its status; a park has to run none of them, nor the code after it, and leave the program callable with its state intact; an exit from a later call has to end the program too, a return from main has to give status 0, and a crash or an exception nothing caught has to stay an error. With the JS throw put back into proc_exit, the exit from main and the exit from a later call both end in the noexcept frame's terminate. It skips without node, or with a node whose engine lacks the standardized exception handling. The Flatpak has no node, so CI does not run it; the native loop does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
0dabce0b74
commit
fec206839e
3 changed files with 243 additions and 0 deletions
75
tests/WasiBrowserRuntimeExit/main.cpp
Normal file
75
tests/WasiBrowserRuntimeExit/main.cpp
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
// 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;
|
||||
|
||||
// The browser runtime has to end a call into the program without running any
|
||||
// more of it: when the program exits (exit, _Exit, a nonzero return from
|
||||
// main), and when it parks to hand the thread to the browser for good, which
|
||||
// Crafter::Window::StartSync does on DOM. runtime.js used to throw a
|
||||
// JavaScript error out of proc_exit, and a program built with
|
||||
// -fwasm-exceptions catches that. An exit then ran the destructors of every
|
||||
// frame it left, a catch (...) swallowed it and carried on, and a noexcept
|
||||
// function turned it into std::terminate, while the page went on calling
|
||||
// into a program whose state those destructors had torn down.
|
||||
//
|
||||
// fixture/harness.mjs runs the real runtime.js under node against a module
|
||||
// with the frames that catch between _start and the call that ends it: a
|
||||
// destructor, a catch (...) and a noexcept function, each reporting when it
|
||||
// runs. It checks that an exit runs none of them and records its status;
|
||||
// that a park runs none of them either, nor the code after it, and leaves the
|
||||
// program callable with its state intact; that an exit from a later call
|
||||
// ends the program too; that returning from main ends it with status 0; and
|
||||
// that a crash or an exception nothing caught still surfaces as an error.
|
||||
// Skips without node, or with a node whose engine lacks exception handling.
|
||||
// The Flatpak has no node, so this one runs in the native loop.
|
||||
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() {
|
||||
if (std::system("which node > /dev/null 2>&1") != 0) {
|
||||
std::println(std::cerr, "SKIP: node not on PATH");
|
||||
return 77;
|
||||
}
|
||||
|
||||
const fs::path scratch = fs::temp_directory_path() / "crafter-build-wasi-browser-runtime-exit";
|
||||
fs::remove_all(scratch);
|
||||
fs::create_directories(scratch);
|
||||
// node runs a .js file as a CommonJS script, which cannot await at its
|
||||
// top level the way runtime.js does.
|
||||
const fs::path runtime = scratch / "runtime.mjs";
|
||||
fs::copy_file(fs::current_path() / "wasi-runtime" / "runtime.js", runtime);
|
||||
const fs::path harness = fs::current_path() / "tests" / "WasiBrowserRuntimeExit" / "fixture" / "harness.mjs";
|
||||
|
||||
const auto run = RunCommandWithTimeout(std::format("node {} {}", ShellQuote(harness.string()), ShellQuote(runtime.string())), std::chrono::seconds(60));
|
||||
fs::remove_all(scratch);
|
||||
if (run.output.contains("SKIP:")) {
|
||||
std::println(std::cerr, "{}", run.output);
|
||||
return 77;
|
||||
}
|
||||
|
||||
Check(run.exitCode == 0, std::format("the harness ran to the end (exit {})", run.exitCode));
|
||||
Check(run.output.contains("exit: ended, exitCode=3 parked=false trace=[]"), "an exit runs no destructor, catch (...) or terminate, and its status is recorded");
|
||||
Check(run.output.contains("park: ended, exitCode=null parked=true trace=[]; frame()=42"), "a park runs none of them nor the code after it, and the program stays callable with its state");
|
||||
Check(run.output.contains("quit(7) threw RuntimeError, exitCode=7 trace=[]"), "an exit from a later call ends the program the same way");
|
||||
Check(run.output.contains("return: ended, exitCode=0 parked=false trace=[]"), "returning from main ends the program with status 0");
|
||||
Check(run.output.contains("uncaught exception: rejected with Exception, exitCode=null"), "an exception nothing caught is not taken for an exit");
|
||||
Check(run.output.contains("crash: rejected with RuntimeError, exitCode=null"), "a crash is not taken for an exit");
|
||||
|
||||
if (failures > 0) {
|
||||
std::println(std::cerr, "{} assertions failed; harness output:\n{}", failures, run.output);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
Loading…
Reference in a new issue