From 60c246886f71316729bc1669446ac3ba94685495 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Fri, 19 Apr 2024 10:38:27 -0700 Subject: [PATCH] TLA (#28) --- .github/workflows/main.yml | 1 - .gitignore | 3 + README.md | 9 +- builtins/web/fetch/fetch_event.cpp | 14 +-- include/extension-api.h | 2 +- runtime/engine.cpp | 26 +++-- runtime/event_loop.cpp | 5 +- runtime/event_loop.h | 2 +- runtime/js.cpp | 16 +-- runtime/script_loader.cpp | 9 +- runtime/script_loader.h | 2 +- ...{expectation.txt => expect_serve_body.txt} | 0 tests/cases/smoke/expect_serve_stderr.txt | 0 tests/cases/smoke/expect_serve_stdout.txt | 2 + tests/cases/syntax-err/expect_wizer_fail | 0 .../cases/syntax-err/expect_wizer_stderr.txt | 1 + tests/cases/syntax-err/syntax-err.js | 1 + tests/cases/tla-err/expect_wizer_fail | 0 tests/cases/tla-err/expect_wizer_stderr.txt | 1 + tests/cases/tla-err/tla-err.js | 6 + .../tla-runtime-resolve/expect_serve_body.txt | 1 + .../expect_serve_stdout.txt | 1 + .../tla-runtime-resolve.js | 18 +++ tests/cases/tla/expect_serve_body.txt | 1 + tests/cases/tla/tla.js | 10 ++ tests/test.sh | 103 ++++++++++++++---- tests/tests.cmake | 18 +-- 27 files changed, 169 insertions(+), 83 deletions(-) rename tests/cases/smoke/{expectation.txt => expect_serve_body.txt} (100%) create mode 100644 tests/cases/smoke/expect_serve_stderr.txt create mode 100644 tests/cases/smoke/expect_serve_stdout.txt create mode 100644 tests/cases/syntax-err/expect_wizer_fail create mode 100644 tests/cases/syntax-err/expect_wizer_stderr.txt create mode 100644 tests/cases/syntax-err/syntax-err.js create mode 100644 tests/cases/tla-err/expect_wizer_fail create mode 100644 tests/cases/tla-err/expect_wizer_stderr.txt create mode 100644 tests/cases/tla-err/tla-err.js create mode 100644 tests/cases/tla-runtime-resolve/expect_serve_body.txt create mode 100644 tests/cases/tla-runtime-resolve/expect_serve_stdout.txt create mode 100644 tests/cases/tla-runtime-resolve/tla-runtime-resolve.js create mode 100644 tests/cases/tla/expect_serve_body.txt create mode 100644 tests/cases/tla/tla.js diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b38fe85..0a8d1a1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -44,5 +44,4 @@ jobs: - name: StarlingMonkey Integration Tests run: | - cmake --build cmake-build-debug --target test-cases CTEST_OUTPUT_ON_FAILURE=1 make -C cmake-build-debug test diff --git a/.gitignore b/.gitignore index ac43b1c..0767024 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,6 @@ # Rust compilation output /target + +/tests/cases/*/*.wasm +/tests/cases/*/*.log diff --git a/README.md b/README.md index 20455bf..26da896 100644 --- a/README.md +++ b/README.md @@ -47,22 +47,19 @@ Integration tests require [`Wasmtime`](https://wasmtime.dev/) to be installed (` Before tests can be run, the cases must first be built, and then they can be tested: ```bash -cmake --build cmake-build-debug --parallel 8 --target test-cases CTEST_OUTPUT_ON_FAILURE=1 make -C cmake-build-debug test ``` Individual tests can also be run from within the build folder using `ctest` directly: ```bash -cd cmake-build-debug -ctest smoke +ctest --test-dir cmake-build-debug -R smoke ``` -Or, to directly run the tests on Wasmtime, use `wasmtime serve` directly via: +Or, to directly run the tests on Wasmtime, use `wasmtime serve` via: ```bash -cd cmake-build-debug -wasmtime serve -S common test-smoke.wasm +wasmtime serve -S common tests/cases/smoke/smoke.wasm ``` Then visit http://0.0.0.0:8080/ diff --git a/builtins/web/fetch/fetch_event.cpp b/builtins/web/fetch/fetch_event.cpp index 93e9b7d..593a386 100644 --- a/builtins/web/fetch/fetch_event.cpp +++ b/builtins/web/fetch/fetch_event.cpp @@ -589,16 +589,14 @@ void exports_wasi_http_incoming_handler(exports_wasi_http_incoming_request reque dispatch_fetch_event(fetch_event, &total_compute); - RootedValue result(ENGINE->cx()); - if (!ENGINE->run_event_loop(&result)) { - fflush(stdout); - fprintf(stderr, "Error running event loop: "); - ENGINE->dump_value(result, stderr); - return; - } + bool success = ENGINE->run_event_loop(); if (JS_IsExceptionPending(ENGINE->cx())) { - ENGINE->dump_pending_exception("Error evaluating code: "); + ENGINE->dump_pending_exception("evaluating incoming request"); + } + + if (!success) { + fprintf(stderr, "Internal error."); } if (ENGINE->debug_logging_enabled() && ENGINE->has_pending_async_tasks()) { diff --git a/include/extension-api.h b/include/extension-api.h index df045a8..5ae60c2 100644 --- a/include/extension-api.h +++ b/include/extension-api.h @@ -48,7 +48,7 @@ class Engine { void enable_module_mode(bool enable); bool eval_toplevel(const char *path, MutableHandleValue result); - bool run_event_loop(MutableHandleValue result); + bool run_event_loop(); /** * Get the JS value associated with the top-level script execution - diff --git a/runtime/engine.cpp b/runtime/engine.cpp index 2b2dda0..76d4251 100644 --- a/runtime/engine.cpp +++ b/runtime/engine.cpp @@ -353,19 +353,27 @@ void api::Engine::abort(const char *reason) { ::abort(CONTEXT, reason); } bool api::Engine::eval_toplevel(const char *path, MutableHandleValue result) { JSContext *cx = CONTEXT; RootedValue ns(cx); - if (!scriptLoader->load_top_level_script(path, &ns)) { + RootedValue tla_promise(cx); + if (!scriptLoader->load_top_level_script(path, &ns, &tla_promise)) { return false; } SCRIPT_VALUE.init(cx, ns); - // Ensure that any pending promise reactions are run before taking the - // snapshot. - while (js::HasJobsPending(cx)) { - js::RunJobs(cx); + if (!core::EventLoop::run_event_loop(this, 0)) { + return false; + } - if (JS_IsExceptionPending(cx)) { - abort("running Promise reactions"); + // TLA rejections during pre-initialization are treated as top-level exceptions. + // TLA may remain unresolved, in which case it will continue tasks at runtime. + // Rejections after pre-intialization remain unhandled rejections for now. + if (tla_promise.isObject()) { + RootedObject promise_obj(cx, &tla_promise.toObject()); + JS::PromiseState state = JS::GetPromiseState(promise_obj); + if (state == JS::PromiseState::Rejected) { + RootedValue err(cx, JS::GetPromiseResult(promise_obj)); + JS_SetPendingException(cx, err); + return false; } } @@ -405,8 +413,8 @@ bool api::Engine::eval_toplevel(const char *path, MutableHandleValue result) { return true; } -bool api::Engine::run_event_loop(MutableHandleValue result) { - return core::EventLoop::run_event_loop(this, 0, result); +bool api::Engine::run_event_loop() { + return core::EventLoop::run_event_loop(this, 0); } bool api::Engine::dump_value(JS::Value val, FILE *fp) { return ::dump_value(CONTEXT, val, fp); } diff --git a/runtime/event_loop.cpp b/runtime/event_loop.cpp index 8d5b1c2..db8f9a5 100644 --- a/runtime/event_loop.cpp +++ b/runtime/event_loop.cpp @@ -38,8 +38,7 @@ bool EventLoop::cancel_async_task(api::Engine *engine, const int32_t id) { bool EventLoop::has_pending_async_tasks() { return !queue.get().tasks.empty(); } -bool EventLoop::run_event_loop(api::Engine *engine, double total_compute, - MutableHandleValue result) { +bool EventLoop::run_event_loop(api::Engine *engine, double total_compute) { // Loop until no more resolved promises or backend requests are pending. // LOG("Start processing async jobs ...\n"); @@ -51,7 +50,7 @@ bool EventLoop::run_event_loop(api::Engine *engine, double total_compute, js::RunJobs(cx); if (JS_IsExceptionPending(cx)) - engine->abort("running Promise reactions"); + return false; } // TODO: add general mechanism for extending the event loop duration. diff --git a/runtime/event_loop.h b/runtime/event_loop.h index c5f7b97..0548e5f 100644 --- a/runtime/event_loop.h +++ b/runtime/event_loop.h @@ -33,7 +33,7 @@ class EventLoop { * * The loop terminates once both of these steps are null-ops. */ - static bool run_event_loop(api::Engine *engine, double total_compute, MutableHandleValue result); + static bool run_event_loop(api::Engine *engine, double total_compute); /** * Queue a new async task. diff --git a/runtime/js.cpp b/runtime/js.cpp index 9ebc5df..ba050fb 100644 --- a/runtime/js.cpp +++ b/runtime/js.cpp @@ -41,17 +41,11 @@ bool initialize(const char *filename) { #endif RootedValue result(engine.cx()); - bool success = engine.eval_toplevel(filename, &result); - success = success && engine.run_event_loop(&result); - - if (JS_IsExceptionPending(engine.cx())) { - engine.dump_pending_exception("Error evaluating code: "); - } - - if (!success) { - fflush(stdout); - fprintf(stderr, "Error running event loop: "); - engine.dump_value(result, stderr); + + if (!engine.eval_toplevel(filename, &result)) { + if (JS_IsExceptionPending(engine.cx())) { + engine.dump_pending_exception("pre-initializing"); + } return false; } diff --git a/runtime/script_loader.cpp b/runtime/script_loader.cpp index 48d0954..269204b 100644 --- a/runtime/script_loader.cpp +++ b/runtime/script_loader.cpp @@ -221,7 +221,7 @@ bool ScriptLoader::load_script(JSContext *cx, const char *script_path, return ::load_script(cx, script_path, resolved_path, script); } -bool ScriptLoader::load_top_level_script(const char *path, MutableHandleValue result) { +bool ScriptLoader::load_top_level_script(const char *path, MutableHandleValue result, MutableHandleValue tla_promise) { JSContext *cx = CONTEXT; MOZ_ASSERT(!BASE_PATH); @@ -286,13 +286,10 @@ bool ScriptLoader::load_top_level_script(const char *path, MutableHandleValue re return JS_ExecuteScript(cx, script, result); } - if (!ModuleEvaluate(cx, module, result)) { + if (!ModuleEvaluate(cx, module, tla_promise)) { return false; } - - // modules return the top-level await promise in the result value - // we don't currently support TLA, instead we reassign result - // with the module namespace + JS::RootedObject ns(cx, JS::GetModuleNamespace(cx, module)); result.setObject(*ns); return true; diff --git a/runtime/script_loader.h b/runtime/script_loader.h index 329b8fc..c3b791d 100644 --- a/runtime/script_loader.h +++ b/runtime/script_loader.h @@ -18,7 +18,7 @@ class ScriptLoader { ~ScriptLoader(); void enable_module_mode(bool enable); - bool load_top_level_script(const char *path, MutableHandleValue result); + bool load_top_level_script(const char *path, MutableHandleValue result, MutableHandleValue tla_promise); bool load_script(JSContext* cx, const char *script_path, JS::SourceText &script); }; diff --git a/tests/cases/smoke/expectation.txt b/tests/cases/smoke/expect_serve_body.txt similarity index 100% rename from tests/cases/smoke/expectation.txt rename to tests/cases/smoke/expect_serve_body.txt diff --git a/tests/cases/smoke/expect_serve_stderr.txt b/tests/cases/smoke/expect_serve_stderr.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/smoke/expect_serve_stdout.txt b/tests/cases/smoke/expect_serve_stdout.txt new file mode 100644 index 0000000..ede5502 --- /dev/null +++ b/tests/cases/smoke/expect_serve_stdout.txt @@ -0,0 +1,2 @@ +stdout [0] :: Log: chaining from http://localhost:8181/ to /chained +stdout [1] :: Log: post resp [object Response] diff --git a/tests/cases/syntax-err/expect_wizer_fail b/tests/cases/syntax-err/expect_wizer_fail new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/syntax-err/expect_wizer_stderr.txt b/tests/cases/syntax-err/expect_wizer_stderr.txt new file mode 100644 index 0000000..d503818 --- /dev/null +++ b/tests/cases/syntax-err/expect_wizer_stderr.txt @@ -0,0 +1 @@ +Exception while pre-initializing: (new SyntaxError("expected expression, got end of script", "syntax-err.js", 2)) diff --git a/tests/cases/syntax-err/syntax-err.js b/tests/cases/syntax-err/syntax-err.js new file mode 100644 index 0000000..1b76532 --- /dev/null +++ b/tests/cases/syntax-err/syntax-err.js @@ -0,0 +1 @@ +a? diff --git a/tests/cases/tla-err/expect_wizer_fail b/tests/cases/tla-err/expect_wizer_fail new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/tla-err/expect_wizer_stderr.txt b/tests/cases/tla-err/expect_wizer_stderr.txt new file mode 100644 index 0000000..abf5c1a --- /dev/null +++ b/tests/cases/tla-err/expect_wizer_stderr.txt @@ -0,0 +1 @@ +Exception while pre-initializing: (new Error("blah", "tla-err.js", 3)) diff --git a/tests/cases/tla-err/tla-err.js b/tests/cases/tla-err/tla-err.js new file mode 100644 index 0000000..d76f469 --- /dev/null +++ b/tests/cases/tla-err/tla-err.js @@ -0,0 +1,6 @@ +let reject; +Promise.resolve().then(() => { + reject(new Error('blah')); +}); + +await new Promise((_, _reject) => void (reject = _reject)); diff --git a/tests/cases/tla-runtime-resolve/expect_serve_body.txt b/tests/cases/tla-runtime-resolve/expect_serve_body.txt new file mode 100644 index 0000000..95d09f2 --- /dev/null +++ b/tests/cases/tla-runtime-resolve/expect_serve_body.txt @@ -0,0 +1 @@ +hello world \ No newline at end of file diff --git a/tests/cases/tla-runtime-resolve/expect_serve_stdout.txt b/tests/cases/tla-runtime-resolve/expect_serve_stdout.txt new file mode 100644 index 0000000..de1f798 --- /dev/null +++ b/tests/cases/tla-runtime-resolve/expect_serve_stdout.txt @@ -0,0 +1 @@ +stdout [0] :: Log: YO diff --git a/tests/cases/tla-runtime-resolve/tla-runtime-resolve.js b/tests/cases/tla-runtime-resolve/tla-runtime-resolve.js new file mode 100644 index 0000000..eace8f3 --- /dev/null +++ b/tests/cases/tla-runtime-resolve/tla-runtime-resolve.js @@ -0,0 +1,18 @@ +let resolve; +Promise.resolve().then(() => { + resolve(); +}); + +await new Promise(_resolve => void (resolve = _resolve)); + +let runtimePromiseResolve; +let runtimePromise = new Promise(resolve => runtimePromiseResolve = resolve); + +addEventListener('fetch', evt => evt.respondWith((async () => { + runtimePromiseResolve(); + return new Response(`hello world`, { headers: { hello: 'world' }}); +})())); + +await runtimePromise; + +console.log('YO'); diff --git a/tests/cases/tla/expect_serve_body.txt b/tests/cases/tla/expect_serve_body.txt new file mode 100644 index 0000000..95d09f2 --- /dev/null +++ b/tests/cases/tla/expect_serve_body.txt @@ -0,0 +1 @@ +hello world \ No newline at end of file diff --git a/tests/cases/tla/tla.js b/tests/cases/tla/tla.js new file mode 100644 index 0000000..2b69bcd --- /dev/null +++ b/tests/cases/tla/tla.js @@ -0,0 +1,10 @@ +let resolve; +Promise.resolve().then(() => { + resolve(); +}); + +await new Promise(_resolve => void (resolve = _resolve)); + +addEventListener('fetch', evt => evt.respondWith((async () => { + return new Response(`hello world`, { headers: { hello: 'world' }}); +})())); diff --git a/tests/test.sh b/tests/test.sh index cccd2aa..9e940c7 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -1,26 +1,68 @@ set -euo pipefail -test_component=$1 -test_expectation=$2 +test_name="$2" +test_runtime="$1" +test_dir="$(dirname "$0")/cases/$test_name" wasmtime="${WASMTIME:-wasmtime}" -output_dir=$(dirname $test_expectation) +# Load test expectation fails to check, only those defined apply +test_wizer_fail_expectation="$test_dir/expect_wizer_fail" +test_wizer_stdout_expectation="$test_dir/expect_wizer_stdout.txt" +test_wizer_stderr_expectation="$test_dir/expect_wizer_stderr.txt" +test_serve_body_expectation="$test_dir/expect_serve_body.txt" +test_serve_stdout_expectation="$test_dir/expect_serve_stdout.txt" +test_serve_stderr_expectation="$test_dir/expect_serve_stderr.txt" +test_serve_status_expectation=$(cat "$test_dir/expect_serve_status.txt" 2> /dev/null || echo "200") -if [ ! -f $test_component ]; then - echo "Test component $test_component does not exist." +test_component="$test_dir/$test_name.wasm" + +body_log="$test_dir/body.log" +stdout_log="$test_dir/stdout.log" +stderr_log="$test_dir/stderr.log" + +set +e +"$test_runtime/componentize.sh" "$test_dir/$test_name.js" "$test_component" 1> "$stdout_log" 2> "$stderr_log" +wizer_result=$? +set -e + +if [ -f "$test_wizer_fail_expectation" ] && [ $wizer_result -eq 0 ]; then + echo "Expected Wizer to fail, but it succeeded." + exit 1 +elif [ ! -f "$test_wizer_fail_expectation" ] && [ ! $wizer_result -eq 0 ]; then + echo "Wizering failed." + >&2 cat "$stderr_log" + >&2 cat "$stdout_log" exit 1 fi -if [ ! -f $test_expectation ]; then - echo "Test expectation $test_expectation does not exist." - exit 1 +if [ -f "$test_wizer_stdout_expectation" ]; then + cmp "$stdout_log" "$test_wizer_stdout_expectation" +fi + +if [ -f "$test_wizer_stderr_expectation" ]; then + mv "$stderr_log" "$stderr_log.orig" + cat "$stderr_log.orig" | head -n $(cat "$test_wizer_stderr_expectation" | wc -l) > "$stderr_log" + rm "$stderr_log.orig" + cmp "$stderr_log" "$test_wizer_stderr_expectation" fi -wasmtime_log="$output_dir/wasmtime.log" -response_body_log="$output_dir/response.log" +if [ ! -f "$test_component" ] || [ ! -s "$test_component" ]; then + if [ -f "$test_serve_body_expectation" ] || [ -f "$test_serve_stdout_expectation" ] || [ -f "$test_serve_stderr_expectation" ] || [ -f "$test_dir/expect_serve_status.txt" ]; then + echo "Test component $test_component does not exist, cannot verify serve expectations." + exit 1 + else + echo "Test component $test_component does not exist, exiting." + rm "$stdout_log" + rm "$stderr_log" + if [ -f "$test_component" ]; then + rm "$test_component" + fi + exit 0 + fi +fi -$wasmtime serve -S common --addr 0.0.0.0:8181 $test_component 2> "$wasmtime_log" & +$wasmtime serve -S common --addr 0.0.0.0:8181 "$test_component" 1> "$stdout_log" 2> "$stderr_log" & wasmtime_pid="$!" function cleanup { @@ -29,35 +71,48 @@ function cleanup { trap cleanup EXIT -until cat $wasmtime_log | grep -m 1 "Serving HTTP" >/dev/null || ! ps -p ${wasmtime_pid} >/dev/null; do : ; done +until cat "$stderr_log" | grep -m 1 "Serving HTTP" >/dev/null || ! ps -p ${wasmtime_pid} >/dev/null; do : ; done if ! ps -p ${wasmtime_pid} >/dev/null; then echo "Wasmtime exited early" - >&2 cat "$wasmtime_log" + >&2 cat "$stderr_log" exit 1 fi -if ! cat "$wasmtime_log" | grep -m 1 "Serving HTTP"; then +if ! cat "$stderr_log" | grep -m 1 "Serving HTTP"; then echo "Unexpected Wasmtime output" - >&2 cat "$wasmtime_log" + >&2 cat "$stderr_log" exit 1 fi -status_code=$(curl --write-out %{http_code} --silent --output "$response_body_log" http://localhost:8181) +status_code=$(curl --write-out %{http_code} --silent --output "$body_log" http://localhost:8181) -if [ ! "$status_code" = "200" ]; then - echo "Bad status code $status_code" - >&2 cat "$wasmtime_log" - >&2 cat "$response_body_log" +if [ ! "$status_code" = "$test_serve_status_expectation" ]; then + echo "Bad status code $status_code, expected $test_serve_status_expectation" + >&2 cat "$stderr_log" + >&2 cat "$stdout_log" + >&2 cat "$body_log" exit 1 fi -cmp "$response_body_log" "$test_expectation" -rm "$response_body_log" +if [ -f "$test_serve_body_expectation" ]; then + cmp "$body_log" "$test_serve_body_expectation" +fi -trap '' EXIT +if [ -f "$test_serve_stdout_expectation" ]; then + cmp "$stdout_log" "$test_serve_stdout_expectation" +fi +if [ -f "$test_serve_stderr_expectation" ]; then + tail -n +2 "$stderr_log" > "$stderr_log" + cmp "$stderr_log" "$test_serve_stderr_expectation" +fi + +rm "$body_log" +rm "$stdout_log" +rm "$stderr_log" + +trap '' EXIT echo "Test Completed Successfully" kill -9 ${wasmtime_pid} -rm $wasmtime_log exit 0 diff --git a/tests/tests.cmake b/tests/tests.cmake index f8ea191..865a82c 100644 --- a/tests/tests.cmake +++ b/tests/tests.cmake @@ -6,18 +6,12 @@ include("wasmtime") function(test TEST_NAME) get_target_property(RUNTIME_DIR starling.wasm BINARY_DIR) - - add_custom_command( - OUTPUT test-${TEST_NAME}.wasm - WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} - COMMAND ${CMAKE_COMMAND} -E env "WASM_TOOLS=${WASM_TOOLS_DIR}/wasm-tools" env "WIZER=${WIZER_DIR}/wizer" ${RUNTIME_DIR}/componentize.sh ${CMAKE_SOURCE_DIR}/tests/cases/${TEST_NAME}/${TEST_NAME}.js test-${TEST_NAME}.wasm - DEPENDS ${ARG_SOURCES} ${RUNTIME_DIR}/componentize.sh starling.wasm - VERBATIM - ) - add_custom_target(test-cases DEPENDS test-${TEST_NAME}.wasm) - add_test(${TEST_NAME} ${BASH_PROGRAM} ${CMAKE_SOURCE_DIR}/tests/test.sh test-${TEST_NAME}.wasm ${CMAKE_SOURCE_DIR}/tests/cases/${TEST_NAME}/expectation.txt) + add_test(${TEST_NAME} ${BASH_PROGRAM} ${CMAKE_SOURCE_DIR}/tests/test.sh ${RUNTIME_DIR} ${TEST_NAME}) + set_property(TEST ${TEST_NAME} PROPERTY ENVIRONMENT "WASMTIME=${WASMTIME};WIZER=${WIZER_DIR}/wizer;WASM_TOOLS=${WASM_TOOLS_DIR}/wasm-tools") endfunction() test(smoke) - -set_property(TEST smoke PROPERTY ENVIRONMENT "WASMTIME=${WASMTIME}") +test(tla) +test(syntax-err) +test(tla-err) +test(tla-runtime-resolve)