// 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; }