Skip to content

Commit

Permalink
TLA (#28)
Browse files Browse the repository at this point in the history
  • Loading branch information
guybedford authored Apr 19, 2024
1 parent e15b39f commit 60c2468
Show file tree
Hide file tree
Showing 27 changed files with 169 additions and 83 deletions.
1 change: 0 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,6 @@

# Rust compilation output
/target

/tests/cases/*/*.wasm
/tests/cases/*/*.log
9 changes: 3 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down
14 changes: 6 additions & 8 deletions builtins/web/fetch/fetch_event.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down
2 changes: 1 addition & 1 deletion include/extension-api.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 -
Expand Down
26 changes: 17 additions & 9 deletions runtime/engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand Down Expand Up @@ -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); }
Expand Down
5 changes: 2 additions & 3 deletions runtime/event_loop.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion runtime/event_loop.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
16 changes: 5 additions & 11 deletions runtime/js.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
9 changes: 3 additions & 6 deletions runtime/script_loader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion runtime/script_loader.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<mozilla::Utf8Unit> &script);
};

Expand Down
File renamed without changes.
Empty file.
2 changes: 2 additions & 0 deletions tests/cases/smoke/expect_serve_stdout.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
stdout [0] :: Log: chaining from http://localhost:8181/ to /chained
stdout [1] :: Log: post resp [object Response]
Empty file.
1 change: 1 addition & 0 deletions tests/cases/syntax-err/expect_wizer_stderr.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Exception while pre-initializing: (new SyntaxError("expected expression, got end of script", "syntax-err.js", 2))
1 change: 1 addition & 0 deletions tests/cases/syntax-err/syntax-err.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
a?
Empty file.
1 change: 1 addition & 0 deletions tests/cases/tla-err/expect_wizer_stderr.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Exception while pre-initializing: (new Error("blah", "tla-err.js", 3))
6 changes: 6 additions & 0 deletions tests/cases/tla-err/tla-err.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
let reject;
Promise.resolve().then(() => {
reject(new Error('blah'));
});

await new Promise((_, _reject) => void (reject = _reject));
1 change: 1 addition & 0 deletions tests/cases/tla-runtime-resolve/expect_serve_body.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
hello world
1 change: 1 addition & 0 deletions tests/cases/tla-runtime-resolve/expect_serve_stdout.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
stdout [0] :: Log: YO
18 changes: 18 additions & 0 deletions tests/cases/tla-runtime-resolve/tla-runtime-resolve.js
Original file line number Diff line number Diff line change
@@ -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');
1 change: 1 addition & 0 deletions tests/cases/tla/expect_serve_body.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
hello world
10 changes: 10 additions & 0 deletions tests/cases/tla/tla.js
Original file line number Diff line number Diff line change
@@ -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' }});
})()));
103 changes: 79 additions & 24 deletions tests/test.sh
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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
Loading

0 comments on commit 60c2468

Please sign in to comment.