75 lines
3.9 KiB
C++
75 lines
3.9 KiB
C++
|
|
// 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;
|
||
|
|
}
|