1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-29 10:39:10 -05:00
denoland-deno/core/runtime/tests.rs
Matt Mastracci ec8e9d4f5b
chore(core): Refactor runtime and split out tests (#19491)
This is a quick first refactoring to split the tests out of runtime and
move runtime-related code to a top-level runtime module.

There will be a followup to refactor imports a bit, but this is the
major change that will most likely conflict with other work and I want
to merge it early.
2023-06-14 02:03:10 +00:00

2306 lines
61 KiB
Rust

// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
use crate::ascii_str;
use crate::error::custom_error;
use crate::error::generic_error;
use crate::error::AnyError;
use crate::error::JsError;
use crate::extensions::OpDecl;
use crate::include_ascii_string;
use crate::module_specifier::ModuleSpecifier;
use crate::modules::AssertedModuleType;
use crate::modules::ModuleCode;
use crate::modules::ModuleInfo;
use crate::modules::ModuleLoadId;
use crate::modules::ModuleLoader;
use crate::modules::ModuleSource;
use crate::modules::ModuleSourceFuture;
use crate::modules::ModuleType;
use crate::modules::ResolutionKind;
use crate::modules::SymbolicModule;
use crate::Extension;
use crate::ZeroCopyBuf;
use crate::*;
use anyhow::Error;
use deno_ops::op;
use futures::future::poll_fn;
use futures::future::Future;
use futures::FutureExt;
use std::cell::RefCell;
use std::pin::Pin;
use std::rc::Rc;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::task::Context;
use std::task::Poll;
// deno_ops macros generate code assuming deno_core in scope.
mod deno_core {
pub use crate::*;
}
#[derive(Copy, Clone)]
pub enum Mode {
Async,
AsyncDeferred,
AsyncZeroCopy(bool),
}
struct TestState {
mode: Mode,
dispatch_count: Arc<AtomicUsize>,
}
#[op]
async fn op_test(
rc_op_state: Rc<RefCell<OpState>>,
control: u8,
buf: Option<ZeroCopyBuf>,
) -> Result<u8, AnyError> {
#![allow(clippy::await_holding_refcell_ref)] // False positive.
let op_state_ = rc_op_state.borrow();
let test_state = op_state_.borrow::<TestState>();
test_state.dispatch_count.fetch_add(1, Ordering::Relaxed);
let mode = test_state.mode;
drop(op_state_);
match mode {
Mode::Async => {
assert_eq!(control, 42);
Ok(43)
}
Mode::AsyncDeferred => {
tokio::task::yield_now().await;
assert_eq!(control, 42);
Ok(43)
}
Mode::AsyncZeroCopy(has_buffer) => {
assert_eq!(buf.is_some(), has_buffer);
if let Some(buf) = buf {
assert_eq!(buf.len(), 1);
}
Ok(43)
}
}
}
fn setup(mode: Mode) -> (JsRuntime, Arc<AtomicUsize>) {
let dispatch_count = Arc::new(AtomicUsize::new(0));
deno_core::extension!(
test_ext,
ops = [op_test],
options = {
mode: Mode,
dispatch_count: Arc<AtomicUsize>,
},
state = |state, options| {
state.put(TestState {
mode: options.mode,
dispatch_count: options.dispatch_count
})
}
);
let mut runtime = JsRuntime::new(RuntimeOptions {
extensions: vec![test_ext::init_ops(mode, dispatch_count.clone())],
get_error_class_fn: Some(&|error| {
crate::error::get_custom_error_class(error).unwrap()
}),
..Default::default()
});
runtime
.execute_script_static(
"setup.js",
r#"
function assert(cond) {
if (!cond) {
throw Error("assert");
}
}
"#,
)
.unwrap();
assert_eq!(dispatch_count.load(Ordering::Relaxed), 0);
(runtime, dispatch_count)
}
#[tokio::test]
async fn test_ref_unref_ops() {
let (mut runtime, _dispatch_count) = setup(Mode::AsyncDeferred);
runtime
.execute_script_static(
"filename.js",
r#"
var promiseIdSymbol = Symbol.for("Deno.core.internalPromiseId");
var p1 = Deno.core.opAsync("op_test", 42);
var p2 = Deno.core.opAsync("op_test", 42);
"#,
)
.unwrap();
{
let realm = runtime.global_realm();
assert_eq!(realm.num_pending_ops(), 2);
assert_eq!(realm.num_unrefed_ops(), 0);
}
runtime
.execute_script_static(
"filename.js",
r#"
Deno.core.ops.op_unref_op(p1[promiseIdSymbol]);
Deno.core.ops.op_unref_op(p2[promiseIdSymbol]);
"#,
)
.unwrap();
{
let realm = runtime.global_realm();
assert_eq!(realm.num_pending_ops(), 2);
assert_eq!(realm.num_unrefed_ops(), 2);
}
runtime
.execute_script_static(
"filename.js",
r#"
Deno.core.ops.op_ref_op(p1[promiseIdSymbol]);
Deno.core.ops.op_ref_op(p2[promiseIdSymbol]);
"#,
)
.unwrap();
{
let realm = runtime.global_realm();
assert_eq!(realm.num_pending_ops(), 2);
assert_eq!(realm.num_unrefed_ops(), 0);
}
}
#[test]
fn test_dispatch() {
let (mut runtime, dispatch_count) = setup(Mode::Async);
runtime
.execute_script_static(
"filename.js",
r#"
let control = 42;
Deno.core.opAsync("op_test", control);
async function main() {
Deno.core.opAsync("op_test", control);
}
main();
"#,
)
.unwrap();
assert_eq!(dispatch_count.load(Ordering::Relaxed), 2);
}
#[test]
fn test_op_async_promise_id() {
let (mut runtime, _dispatch_count) = setup(Mode::Async);
runtime
.execute_script_static(
"filename.js",
r#"
const p = Deno.core.opAsync("op_test", 42);
if (p[Symbol.for("Deno.core.internalPromiseId")] == undefined) {
throw new Error("missing id on returned promise");
}
"#,
)
.unwrap();
}
#[test]
fn test_dispatch_no_zero_copy_buf() {
let (mut runtime, dispatch_count) = setup(Mode::AsyncZeroCopy(false));
runtime
.execute_script_static(
"filename.js",
r#"
Deno.core.opAsync("op_test");
"#,
)
.unwrap();
assert_eq!(dispatch_count.load(Ordering::Relaxed), 1);
}
#[test]
fn test_dispatch_stack_zero_copy_bufs() {
let (mut runtime, dispatch_count) = setup(Mode::AsyncZeroCopy(true));
runtime
.execute_script_static(
"filename.js",
r#"
const { op_test } = Deno.core.ensureFastOps();
let zero_copy_a = new Uint8Array([0]);
op_test(null, zero_copy_a);
"#,
)
.unwrap();
assert_eq!(dispatch_count.load(Ordering::Relaxed), 1);
}
#[test]
fn test_execute_script_return_value() {
let mut runtime = JsRuntime::new(Default::default());
let value_global =
runtime.execute_script_static("a.js", "a = 1 + 2").unwrap();
{
let scope = &mut runtime.handle_scope();
let value = value_global.open(scope);
assert_eq!(value.integer_value(scope).unwrap(), 3);
}
let value_global = runtime
.execute_script_static("b.js", "b = 'foobar'")
.unwrap();
{
let scope = &mut runtime.handle_scope();
let value = value_global.open(scope);
assert!(value.is_string());
assert_eq!(
value.to_string(scope).unwrap().to_rust_string_lossy(scope),
"foobar"
);
}
}
#[tokio::test]
async fn test_poll_value() {
let mut runtime = JsRuntime::new(Default::default());
poll_fn(move |cx| {
let value_global = runtime
.execute_script_static("a.js", "Promise.resolve(1 + 2)")
.unwrap();
let v = runtime.poll_value(&value_global, cx);
{
let scope = &mut runtime.handle_scope();
assert!(
matches!(v, Poll::Ready(Ok(v)) if v.open(scope).integer_value(scope).unwrap() == 3)
);
}
let value_global = runtime
.execute_script_static(
"a.js",
"Promise.resolve(new Promise(resolve => resolve(2 + 2)))",
)
.unwrap();
let v = runtime.poll_value(&value_global, cx);
{
let scope = &mut runtime.handle_scope();
assert!(
matches!(v, Poll::Ready(Ok(v)) if v.open(scope).integer_value(scope).unwrap() == 4)
);
}
let value_global = runtime
.execute_script_static("a.js", "Promise.reject(new Error('fail'))")
.unwrap();
let v = runtime.poll_value(&value_global, cx);
assert!(
matches!(v, Poll::Ready(Err(e)) if e.downcast_ref::<JsError>().unwrap().exception_message == "Uncaught Error: fail")
);
let value_global = runtime
.execute_script_static("a.js", "new Promise(resolve => {})")
.unwrap();
let v = runtime.poll_value(&value_global, cx);
matches!(v, Poll::Ready(Err(e)) if e.to_string() == "Promise resolution is still pending but the event loop has already resolved.");
Poll::Ready(())
}).await;
}
#[tokio::test]
async fn test_resolve_value() {
let mut runtime = JsRuntime::new(Default::default());
let value_global = runtime
.execute_script_static("a.js", "Promise.resolve(1 + 2)")
.unwrap();
let result_global = runtime.resolve_value(value_global).await.unwrap();
{
let scope = &mut runtime.handle_scope();
let value = result_global.open(scope);
assert_eq!(value.integer_value(scope).unwrap(), 3);
}
let value_global = runtime
.execute_script_static(
"a.js",
"Promise.resolve(new Promise(resolve => resolve(2 + 2)))",
)
.unwrap();
let result_global = runtime.resolve_value(value_global).await.unwrap();
{
let scope = &mut runtime.handle_scope();
let value = result_global.open(scope);
assert_eq!(value.integer_value(scope).unwrap(), 4);
}
let value_global = runtime
.execute_script_static("a.js", "Promise.reject(new Error('fail'))")
.unwrap();
let err = runtime.resolve_value(value_global).await.unwrap_err();
assert_eq!(
"Uncaught Error: fail",
err.downcast::<JsError>().unwrap().exception_message
);
let value_global = runtime
.execute_script_static("a.js", "new Promise(resolve => {})")
.unwrap();
let error_string = runtime
.resolve_value(value_global)
.await
.unwrap_err()
.to_string();
assert_eq!(
"Promise resolution is still pending but the event loop has already resolved.",
error_string,
);
}
#[test]
fn terminate_execution_webassembly() {
let (mut runtime, _dispatch_count) = setup(Mode::Async);
let v8_isolate_handle = runtime.v8_isolate().thread_safe_handle();
// Run an infinite loop in Webassemby code, which should be terminated.
let promise = runtime.execute_script_static("infinite_wasm_loop.js",
r#"
(async () => {
const wasmCode = new Uint8Array([
0, 97, 115, 109, 1, 0, 0, 0, 1, 4, 1,
96, 0, 0, 3, 2, 1, 0, 7, 17, 1, 13,
105, 110, 102, 105, 110, 105, 116, 101, 95, 108, 111,
111, 112, 0, 0, 10, 9, 1, 7, 0, 3, 64,
12, 0, 11, 11,
]);
const wasmModule = await WebAssembly.compile(wasmCode);
globalThis.wasmInstance = new WebAssembly.Instance(wasmModule);
})()
"#).unwrap();
futures::executor::block_on(runtime.resolve_value(promise)).unwrap();
let terminator_thread = std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(1000));
// terminate execution
let ok = v8_isolate_handle.terminate_execution();
assert!(ok);
});
let err = runtime
.execute_script_static(
"infinite_wasm_loop2.js",
"globalThis.wasmInstance.exports.infinite_loop();",
)
.unwrap_err();
assert_eq!(err.to_string(), "Uncaught Error: execution terminated");
// Cancel the execution-terminating exception in order to allow script
// execution again.
let ok = runtime.v8_isolate().cancel_terminate_execution();
assert!(ok);
// Verify that the isolate usable again.
runtime
.execute_script_static("simple.js", "1 + 1")
.expect("execution should be possible again");
terminator_thread.join().unwrap();
}
#[test]
fn terminate_execution() {
let (mut isolate, _dispatch_count) = setup(Mode::Async);
let v8_isolate_handle = isolate.v8_isolate().thread_safe_handle();
let terminator_thread = std::thread::spawn(move || {
// allow deno to boot and run
std::thread::sleep(std::time::Duration::from_millis(100));
// terminate execution
let ok = v8_isolate_handle.terminate_execution();
assert!(ok);
});
// Rn an infinite loop, which should be terminated.
match isolate.execute_script_static("infinite_loop.js", "for(;;) {}") {
Ok(_) => panic!("execution should be terminated"),
Err(e) => {
assert_eq!(e.to_string(), "Uncaught Error: execution terminated")
}
};
// Cancel the execution-terminating exception in order to allow script
// execution again.
let ok = isolate.v8_isolate().cancel_terminate_execution();
assert!(ok);
// Verify that the isolate usable again.
isolate
.execute_script_static("simple.js", "1 + 1")
.expect("execution should be possible again");
terminator_thread.join().unwrap();
}
#[test]
fn dangling_shared_isolate() {
let v8_isolate_handle = {
// isolate is dropped at the end of this block
let (mut runtime, _dispatch_count) = setup(Mode::Async);
runtime.v8_isolate().thread_safe_handle()
};
// this should not SEGFAULT
v8_isolate_handle.terminate_execution();
}
#[test]
fn syntax_error() {
let mut runtime = JsRuntime::new(Default::default());
let src = "hocuspocus(";
let r = runtime.execute_script_static("i.js", src);
let e = r.unwrap_err();
let js_error = e.downcast::<JsError>().unwrap();
let frame = js_error.frames.first().unwrap();
assert_eq!(frame.column_number, Some(12));
}
#[tokio::test]
async fn test_encode_decode() {
let (mut runtime, _dispatch_count) = setup(Mode::Async);
poll_fn(move |cx| {
runtime
.execute_script(
"encode_decode_test.js",
// Note: We make this to_owned because it contains non-ASCII chars
include_str!("encode_decode_test.js").to_owned().into(),
)
.unwrap();
if let Poll::Ready(Err(_)) = runtime.poll_event_loop(cx, false) {
unreachable!();
}
Poll::Ready(())
})
.await;
}
#[tokio::test]
async fn test_serialize_deserialize() {
let (mut runtime, _dispatch_count) = setup(Mode::Async);
poll_fn(move |cx| {
runtime
.execute_script(
"serialize_deserialize_test.js",
include_ascii_string!("serialize_deserialize_test.js"),
)
.unwrap();
if let Poll::Ready(Err(_)) = runtime.poll_event_loop(cx, false) {
unreachable!();
}
Poll::Ready(())
})
.await;
}
#[tokio::test]
async fn test_error_builder() {
#[op]
fn op_err() -> Result<(), Error> {
Err(custom_error("DOMExceptionOperationError", "abc"))
}
pub fn get_error_class_name(_: &Error) -> &'static str {
"DOMExceptionOperationError"
}
deno_core::extension!(test_ext, ops = [op_err]);
let mut runtime = JsRuntime::new(RuntimeOptions {
extensions: vec![test_ext::init_ops()],
get_error_class_fn: Some(&get_error_class_name),
..Default::default()
});
poll_fn(move |cx| {
runtime
.execute_script_static(
"error_builder_test.js",
include_str!("error_builder_test.js"),
)
.unwrap();
if let Poll::Ready(Err(_)) = runtime.poll_event_loop(cx, false) {
unreachable!();
}
Poll::Ready(())
})
.await;
}
/// Ensure that putting the inspector into OpState doesn't cause crashes. The only valid place we currently allow
/// the inspector to be stashed without cleanup is the OpState, and this should not actually cause crashes.
#[test]
fn inspector() {
let mut runtime = JsRuntime::new(RuntimeOptions {
inspector: true,
..Default::default()
});
// This was causing a crash
runtime.op_state().borrow_mut().put(runtime.inspector());
runtime.execute_script_static("check.js", "null").unwrap();
}
#[test]
fn will_snapshot() {
let snapshot = {
let mut runtime =
JsRuntimeForSnapshot::new(Default::default(), Default::default());
runtime.execute_script_static("a.js", "a = 1 + 2").unwrap();
runtime.snapshot()
};
let snapshot = Snapshot::JustCreated(snapshot);
let mut runtime2 = JsRuntime::new(RuntimeOptions {
startup_snapshot: Some(snapshot),
..Default::default()
});
runtime2
.execute_script_static("check.js", "if (a != 3) throw Error('x')")
.unwrap();
}
#[test]
fn will_snapshot2() {
let startup_data = {
let mut runtime =
JsRuntimeForSnapshot::new(Default::default(), Default::default());
runtime
.execute_script_static("a.js", "let a = 1 + 2")
.unwrap();
runtime.snapshot()
};
let snapshot = Snapshot::JustCreated(startup_data);
let mut runtime = JsRuntimeForSnapshot::new(
RuntimeOptions {
startup_snapshot: Some(snapshot),
..Default::default()
},
Default::default(),
);
let startup_data = {
runtime
.execute_script_static("check_a.js", "if (a != 3) throw Error('x')")
.unwrap();
runtime.execute_script_static("b.js", "b = 2 + 3").unwrap();
runtime.snapshot()
};
let snapshot = Snapshot::JustCreated(startup_data);
{
let mut runtime = JsRuntime::new(RuntimeOptions {
startup_snapshot: Some(snapshot),
..Default::default()
});
runtime
.execute_script_static("check_b.js", "if (b != 5) throw Error('x')")
.unwrap();
runtime
.execute_script_static("check2.js", "if (!Deno.core) throw Error('x')")
.unwrap();
}
}
#[test]
fn test_snapshot_callbacks() {
let snapshot = {
let mut runtime =
JsRuntimeForSnapshot::new(Default::default(), Default::default());
runtime
.execute_script_static(
"a.js",
r#"
Deno.core.setMacrotaskCallback(() => {
return true;
});
Deno.core.ops.op_set_format_exception_callback(()=> {
return null;
})
Deno.core.setPromiseRejectCallback(() => {
return false;
});
a = 1 + 2;
"#,
)
.unwrap();
runtime.snapshot()
};
let snapshot = Snapshot::JustCreated(snapshot);
let mut runtime2 = JsRuntime::new(RuntimeOptions {
startup_snapshot: Some(snapshot),
..Default::default()
});
runtime2
.execute_script_static("check.js", "if (a != 3) throw Error('x')")
.unwrap();
}
#[test]
fn test_from_boxed_snapshot() {
let snapshot = {
let mut runtime =
JsRuntimeForSnapshot::new(Default::default(), Default::default());
runtime.execute_script_static("a.js", "a = 1 + 2").unwrap();
let snap: &[u8] = &runtime.snapshot();
Vec::from(snap).into_boxed_slice()
};
let snapshot = Snapshot::Boxed(snapshot);
let mut runtime2 = JsRuntime::new(RuntimeOptions {
startup_snapshot: Some(snapshot),
..Default::default()
});
runtime2
.execute_script_static("check.js", "if (a != 3) throw Error('x')")
.unwrap();
}
#[test]
fn test_get_module_namespace() {
#[derive(Default)]
struct ModsLoader;
impl ModuleLoader for ModsLoader {
fn resolve(
&self,
specifier: &str,
referrer: &str,
_kind: ResolutionKind,
) -> Result<ModuleSpecifier, Error> {
assert_eq!(specifier, "file:///main.js");
assert_eq!(referrer, ".");
let s = crate::resolve_import(specifier, referrer).unwrap();
Ok(s)
}
fn load(
&self,
_module_specifier: &ModuleSpecifier,
_maybe_referrer: Option<&ModuleSpecifier>,
_is_dyn_import: bool,
) -> Pin<Box<ModuleSourceFuture>> {
async { Err(generic_error("Module loading is not supported")) }
.boxed_local()
}
}
let loader = std::rc::Rc::new(ModsLoader::default());
let mut runtime = JsRuntime::new(RuntimeOptions {
module_loader: Some(loader),
..Default::default()
});
let specifier = crate::resolve_url("file:///main.js").unwrap();
let source_code = ascii_str!(
r#"
export const a = "b";
export default 1 + 2;
"#
);
let module_id = futures::executor::block_on(
runtime.load_main_module(&specifier, Some(source_code)),
)
.unwrap();
#[allow(clippy::let_underscore_future)]
let _ = runtime.mod_evaluate(module_id);
let module_namespace = runtime.get_module_namespace(module_id).unwrap();
let scope = &mut runtime.handle_scope();
let module_namespace = v8::Local::<v8::Object>::new(scope, module_namespace);
assert!(module_namespace.is_module_namespace_object());
let unknown_export_name = v8::String::new(scope, "none").unwrap();
let binding = module_namespace.get(scope, unknown_export_name.into());
assert!(binding.is_some());
assert!(binding.unwrap().is_undefined());
let empty_export_name = v8::String::new(scope, "").unwrap();
let binding = module_namespace.get(scope, empty_export_name.into());
assert!(binding.is_some());
assert!(binding.unwrap().is_undefined());
let a_export_name = v8::String::new(scope, "a").unwrap();
let binding = module_namespace.get(scope, a_export_name.into());
assert!(binding.unwrap().is_string());
assert_eq!(binding.unwrap(), v8::String::new(scope, "b").unwrap());
let default_export_name = v8::String::new(scope, "default").unwrap();
let binding = module_namespace.get(scope, default_export_name.into());
assert!(binding.unwrap().is_number());
assert_eq!(binding.unwrap(), v8::Number::new(scope, 3_f64));
}
#[test]
fn test_heap_limits() {
let create_params =
v8::Isolate::create_params().heap_limits(0, 5 * 1024 * 1024);
let mut runtime = JsRuntime::new(RuntimeOptions {
create_params: Some(create_params),
..Default::default()
});
let cb_handle = runtime.v8_isolate().thread_safe_handle();
let callback_invoke_count = Rc::new(AtomicUsize::new(0));
let inner_invoke_count = Rc::clone(&callback_invoke_count);
runtime.add_near_heap_limit_callback(move |current_limit, _initial_limit| {
inner_invoke_count.fetch_add(1, Ordering::SeqCst);
cb_handle.terminate_execution();
current_limit * 2
});
let err = runtime
.execute_script_static(
"script name",
r#"let s = ""; while(true) { s += "Hello"; }"#,
)
.expect_err("script should fail");
assert_eq!(
"Uncaught Error: execution terminated",
err.downcast::<JsError>().unwrap().exception_message
);
assert!(callback_invoke_count.load(Ordering::SeqCst) > 0)
}
#[test]
fn test_heap_limit_cb_remove() {
let mut runtime = JsRuntime::new(Default::default());
runtime.add_near_heap_limit_callback(|current_limit, _initial_limit| {
current_limit * 2
});
runtime.remove_near_heap_limit_callback(3 * 1024 * 1024);
assert!(runtime.allocations.near_heap_limit_callback_data.is_none());
}
#[test]
fn test_heap_limit_cb_multiple() {
let create_params =
v8::Isolate::create_params().heap_limits(0, 5 * 1024 * 1024);
let mut runtime = JsRuntime::new(RuntimeOptions {
create_params: Some(create_params),
..Default::default()
});
let cb_handle = runtime.v8_isolate().thread_safe_handle();
let callback_invoke_count_first = Rc::new(AtomicUsize::new(0));
let inner_invoke_count_first = Rc::clone(&callback_invoke_count_first);
runtime.add_near_heap_limit_callback(move |current_limit, _initial_limit| {
inner_invoke_count_first.fetch_add(1, Ordering::SeqCst);
current_limit * 2
});
let callback_invoke_count_second = Rc::new(AtomicUsize::new(0));
let inner_invoke_count_second = Rc::clone(&callback_invoke_count_second);
runtime.add_near_heap_limit_callback(move |current_limit, _initial_limit| {
inner_invoke_count_second.fetch_add(1, Ordering::SeqCst);
cb_handle.terminate_execution();
current_limit * 2
});
let err = runtime
.execute_script_static(
"script name",
r#"let s = ""; while(true) { s += "Hello"; }"#,
)
.expect_err("script should fail");
assert_eq!(
"Uncaught Error: execution terminated",
err.downcast::<JsError>().unwrap().exception_message
);
assert_eq!(0, callback_invoke_count_first.load(Ordering::SeqCst));
assert!(callback_invoke_count_second.load(Ordering::SeqCst) > 0);
}
#[test]
fn es_snapshot() {
#[derive(Default)]
struct ModsLoader;
impl ModuleLoader for ModsLoader {
fn resolve(
&self,
specifier: &str,
referrer: &str,
_kind: ResolutionKind,
) -> Result<ModuleSpecifier, Error> {
let s = crate::resolve_import(specifier, referrer).unwrap();
Ok(s)
}
fn load(
&self,
_module_specifier: &ModuleSpecifier,
_maybe_referrer: Option<&ModuleSpecifier>,
_is_dyn_import: bool,
) -> Pin<Box<ModuleSourceFuture>> {
eprintln!("load() should not be called");
unreachable!()
}
}
fn create_module(
runtime: &mut JsRuntime,
i: usize,
main: bool,
) -> ModuleInfo {
let specifier = crate::resolve_url(&format!("file:///{i}.js")).unwrap();
let prev = i - 1;
let source_code = format!(
r#"
import {{ f{prev} }} from "file:///{prev}.js";
export function f{i}() {{ return f{prev}() }}
"#
)
.into();
let id = if main {
futures::executor::block_on(
runtime.load_main_module(&specifier, Some(source_code)),
)
.unwrap()
} else {
futures::executor::block_on(
runtime.load_side_module(&specifier, Some(source_code)),
)
.unwrap()
};
assert_eq!(i, id);
#[allow(clippy::let_underscore_future)]
let _ = runtime.mod_evaluate(id);
futures::executor::block_on(runtime.run_event_loop(false)).unwrap();
ModuleInfo {
id,
main,
name: specifier.into(),
requests: vec![crate::modules::ModuleRequest {
specifier: format!("file:///{prev}.js"),
asserted_module_type: AssertedModuleType::JavaScriptOrWasm,
}],
module_type: ModuleType::JavaScript,
}
}
fn assert_module_map(runtime: &mut JsRuntime, modules: &Vec<ModuleInfo>) {
let module_map = runtime.module_map.borrow();
assert_eq!(module_map.handles.len(), modules.len());
assert_eq!(module_map.info.len(), modules.len());
assert_eq!(
module_map.by_name(AssertedModuleType::Json).len()
+ module_map
.by_name(AssertedModuleType::JavaScriptOrWasm)
.len(),
modules.len()
);
assert_eq!(module_map.next_load_id, (modules.len() + 1) as ModuleLoadId);
for info in modules {
assert!(module_map.handles.get(info.id).is_some());
assert_eq!(module_map.info.get(info.id).unwrap(), info);
assert_eq!(
module_map
.by_name(AssertedModuleType::JavaScriptOrWasm)
.get(&info.name)
.unwrap(),
&SymbolicModule::Mod(info.id)
);
}
}
#[op]
fn op_test() -> Result<String, Error> {
Ok(String::from("test"))
}
let loader = Rc::new(ModsLoader::default());
let mut runtime = JsRuntimeForSnapshot::new(
RuntimeOptions {
module_loader: Some(loader.clone()),
extensions: vec![Extension::builder("text_ext")
.ops(vec![op_test::decl()])
.build()],
..Default::default()
},
Default::default(),
);
let specifier = crate::resolve_url("file:///0.js").unwrap();
let source_code =
ascii_str!(r#"export function f0() { return "hello world" }"#);
let id = futures::executor::block_on(
runtime.load_side_module(&specifier, Some(source_code)),
)
.unwrap();
#[allow(clippy::let_underscore_future)]
let _ = runtime.mod_evaluate(id);
futures::executor::block_on(runtime.run_event_loop(false)).unwrap();
let mut modules = vec![];
modules.push(ModuleInfo {
id,
main: false,
name: specifier.into(),
requests: vec![],
module_type: ModuleType::JavaScript,
});
modules.extend((1..200).map(|i| create_module(&mut runtime, i, false)));
assert_module_map(&mut runtime, &modules);
let snapshot = runtime.snapshot();
let mut runtime2 = JsRuntimeForSnapshot::new(
RuntimeOptions {
module_loader: Some(loader.clone()),
startup_snapshot: Some(Snapshot::JustCreated(snapshot)),
extensions: vec![Extension::builder("text_ext")
.ops(vec![op_test::decl()])
.build()],
..Default::default()
},
Default::default(),
);
assert_module_map(&mut runtime2, &modules);
modules.extend((200..400).map(|i| create_module(&mut runtime2, i, false)));
modules.push(create_module(&mut runtime2, 400, true));
assert_module_map(&mut runtime2, &modules);
let snapshot2 = runtime2.snapshot();
let mut runtime3 = JsRuntime::new(RuntimeOptions {
module_loader: Some(loader),
startup_snapshot: Some(Snapshot::JustCreated(snapshot2)),
extensions: vec![Extension::builder("text_ext")
.ops(vec![op_test::decl()])
.build()],
..Default::default()
});
assert_module_map(&mut runtime3, &modules);
let source_code = r#"(async () => {
const mod = await import("file:///400.js");
return mod.f400() + " " + Deno.core.ops.op_test();
})();"#;
let val = runtime3.execute_script_static(".", source_code).unwrap();
let val = futures::executor::block_on(runtime3.resolve_value(val)).unwrap();
{
let scope = &mut runtime3.handle_scope();
let value = v8::Local::new(scope, val);
let str_ = value.to_string(scope).unwrap().to_rust_string_lossy(scope);
assert_eq!(str_, "hello world test");
}
}
#[test]
fn test_error_without_stack() {
let mut runtime = JsRuntime::new(RuntimeOptions::default());
// SyntaxError
let result = runtime.execute_script_static(
"error_without_stack.js",
r#"
function main() {
console.log("asdf);
}
main();
"#,
);
let expected_error = r#"Uncaught SyntaxError: Invalid or unexpected token
at error_without_stack.js:3:15"#;
assert_eq!(result.unwrap_err().to_string(), expected_error);
}
#[test]
fn test_error_stack() {
let mut runtime = JsRuntime::new(RuntimeOptions::default());
let result = runtime.execute_script_static(
"error_stack.js",
r#"
function assert(cond) {
if (!cond) {
throw Error("assert");
}
}
function main() {
assert(false);
}
main();
"#,
);
let expected_error = r#"Error: assert
at assert (error_stack.js:4:11)
at main (error_stack.js:8:3)
at error_stack.js:10:1"#;
assert_eq!(result.unwrap_err().to_string(), expected_error);
}
#[tokio::test]
async fn test_error_async_stack() {
let mut runtime = JsRuntime::new(RuntimeOptions::default());
poll_fn(move |cx| {
runtime
.execute_script_static(
"error_async_stack.js",
r#"
(async () => {
const p = (async () => {
await Promise.resolve().then(() => {
throw new Error("async");
});
})();
try {
await p;
} catch (error) {
console.log(error.stack);
throw error;
}
})();"#,
)
.unwrap();
let expected_error = r#"Error: async
at error_async_stack.js:5:13
at async error_async_stack.js:4:5
at async error_async_stack.js:9:5"#;
match runtime.poll_event_loop(cx, false) {
Poll::Ready(Err(e)) => {
assert_eq!(e.to_string(), expected_error);
}
_ => panic!(),
};
Poll::Ready(())
})
.await;
}
#[tokio::test]
async fn test_error_context() {
use anyhow::anyhow;
#[op]
fn op_err_sync() -> Result<(), Error> {
Err(anyhow!("original sync error").context("higher-level sync error"))
}
#[op]
async fn op_err_async() -> Result<(), Error> {
Err(anyhow!("original async error").context("higher-level async error"))
}
deno_core::extension!(test_ext, ops = [op_err_sync, op_err_async]);
let mut runtime = JsRuntime::new(RuntimeOptions {
extensions: vec![test_ext::init_ops()],
..Default::default()
});
poll_fn(move |cx| {
runtime
.execute_script_static(
"test_error_context_sync.js",
r#"
let errMessage;
try {
Deno.core.ops.op_err_sync();
} catch (err) {
errMessage = err.message;
}
if (errMessage !== "higher-level sync error: original sync error") {
throw new Error("unexpected error message from op_err_sync: " + errMessage);
}
"#,
)
.unwrap();
let promise = runtime
.execute_script_static(
"test_error_context_async.js",
r#"
(async () => {
let errMessage;
try {
await Deno.core.opAsync("op_err_async");
} catch (err) {
errMessage = err.message;
}
if (errMessage !== "higher-level async error: original async error") {
throw new Error("unexpected error message from op_err_async: " + errMessage);
}
})()
"#,
)
.unwrap();
match runtime.poll_value(&promise, cx) {
Poll::Ready(Ok(_)) => {}
Poll::Ready(Err(err)) => panic!("{err:?}"),
_ => panic!(),
}
Poll::Ready(())
})
.await;
}
#[tokio::test]
async fn test_pump_message_loop() {
let mut runtime = JsRuntime::new(RuntimeOptions::default());
poll_fn(move |cx| {
runtime
.execute_script_static(
"pump_message_loop.js",
r#"
function assertEquals(a, b) {
if (a === b) return;
throw a + " does not equal " + b;
}
const sab = new SharedArrayBuffer(16);
const i32a = new Int32Array(sab);
globalThis.resolved = false;
(function() {
const result = Atomics.waitAsync(i32a, 0, 0);
result.value.then(
(value) => { assertEquals("ok", value); globalThis.resolved = true; },
() => { assertUnreachable();
});
})();
const notify_return_value = Atomics.notify(i32a, 0, 1);
assertEquals(1, notify_return_value);
"#,
)
.unwrap();
match runtime.poll_event_loop(cx, false) {
Poll::Ready(Ok(())) => {}
_ => panic!(),
};
// noop script, will resolve promise from first script
runtime
.execute_script_static("pump_message_loop2.js", r#"assertEquals(1, 1);"#)
.unwrap();
// check that promise from `Atomics.waitAsync` has been resolved
runtime
.execute_script_static(
"pump_message_loop3.js",
r#"assertEquals(globalThis.resolved, true);"#,
)
.unwrap();
Poll::Ready(())
})
.await;
}
#[test]
fn test_v8_platform() {
let options = RuntimeOptions {
v8_platform: Some(v8::new_default_platform(0, false).make_shared()),
..Default::default()
};
let mut runtime = JsRuntime::new(options);
runtime.execute_script_static("<none>", "").unwrap();
}
#[ignore] // TODO(@littledivy): Fast API ops when snapshot is not loaded.
#[test]
fn test_is_proxy() {
let mut runtime = JsRuntime::new(RuntimeOptions::default());
let all_true: v8::Global<v8::Value> = runtime
.execute_script_static(
"is_proxy.js",
r#"
(function () {
const o = { a: 1, b: 2};
const p = new Proxy(o, {});
return Deno.core.ops.op_is_proxy(p) && !Deno.core.ops.op_is_proxy(o) && !Deno.core.ops.op_is_proxy(42);
})()
"#,
)
.unwrap();
let mut scope = runtime.handle_scope();
let all_true = v8::Local::<v8::Value>::new(&mut scope, &all_true);
assert!(all_true.is_true());
}
#[tokio::test]
async fn test_async_opstate_borrow() {
struct InnerState(u64);
#[op]
async fn op_async_borrow(
op_state: Rc<RefCell<OpState>>,
) -> Result<(), Error> {
let n = {
let op_state = op_state.borrow();
let inner_state = op_state.borrow::<InnerState>();
inner_state.0
};
// Future must be Poll::Pending on first call
tokio::time::sleep(std::time::Duration::from_millis(1)).await;
if n != 42 {
unreachable!();
}
Ok(())
}
deno_core::extension!(
test_ext,
ops = [op_async_borrow],
state = |state| state.put(InnerState(42))
);
let mut runtime = JsRuntime::new(RuntimeOptions {
extensions: vec![test_ext::init_ops()],
..Default::default()
});
runtime
.execute_script_static(
"op_async_borrow.js",
"Deno.core.opAsync(\"op_async_borrow\")",
)
.unwrap();
runtime.run_event_loop(false).await.unwrap();
}
#[tokio::test]
async fn test_sync_op_serialize_object_with_numbers_as_keys() {
#[op]
fn op_sync_serialize_object_with_numbers_as_keys(
value: serde_json::Value,
) -> Result<(), Error> {
assert_eq!(
value.to_string(),
r#"{"lines":{"100":{"unit":"m"},"200":{"unit":"cm"}}}"#
);
Ok(())
}
deno_core::extension!(
test_ext,
ops = [op_sync_serialize_object_with_numbers_as_keys]
);
let mut runtime = JsRuntime::new(RuntimeOptions {
extensions: vec![test_ext::init_ops()],
..Default::default()
});
runtime
.execute_script_static(
"op_sync_serialize_object_with_numbers_as_keys.js",
r#"
Deno.core.ops.op_sync_serialize_object_with_numbers_as_keys({
lines: {
100: {
unit: "m"
},
200: {
unit: "cm"
}
}
})
"#,
)
.unwrap();
runtime.run_event_loop(false).await.unwrap();
}
#[tokio::test]
async fn test_async_op_serialize_object_with_numbers_as_keys() {
#[op]
async fn op_async_serialize_object_with_numbers_as_keys(
value: serde_json::Value,
) -> Result<(), Error> {
assert_eq!(
value.to_string(),
r#"{"lines":{"100":{"unit":"m"},"200":{"unit":"cm"}}}"#
);
Ok(())
}
deno_core::extension!(
test_ext,
ops = [op_async_serialize_object_with_numbers_as_keys]
);
let mut runtime = JsRuntime::new(RuntimeOptions {
extensions: vec![test_ext::init_ops()],
..Default::default()
});
runtime
.execute_script_static(
"op_async_serialize_object_with_numbers_as_keys.js",
r#"
Deno.core.opAsync("op_async_serialize_object_with_numbers_as_keys", {
lines: {
100: {
unit: "m"
},
200: {
unit: "cm"
}
}
})
"#,
)
.unwrap();
runtime.run_event_loop(false).await.unwrap();
}
#[tokio::test]
async fn test_set_macrotask_callback_set_next_tick_callback() {
#[op]
async fn op_async_sleep() -> Result<(), Error> {
// Future must be Poll::Pending on first call
tokio::time::sleep(std::time::Duration::from_millis(1)).await;
Ok(())
}
deno_core::extension!(test_ext, ops = [op_async_sleep]);
let mut runtime = JsRuntime::new(RuntimeOptions {
extensions: vec![test_ext::init_ops()],
..Default::default()
});
runtime
.execute_script_static(
"macrotasks_and_nextticks.js",
r#"
(async function () {
const results = [];
Deno.core.setMacrotaskCallback(() => {
results.push("macrotask");
return true;
});
Deno.core.setNextTickCallback(() => {
results.push("nextTick");
Deno.core.ops.op_set_has_tick_scheduled(false);
});
Deno.core.ops.op_set_has_tick_scheduled(true);
await Deno.core.opAsync('op_async_sleep');
if (results[0] != "nextTick") {
throw new Error(`expected nextTick, got: ${results[0]}`);
}
if (results[1] != "macrotask") {
throw new Error(`expected macrotask, got: ${results[1]}`);
}
})();
"#,
)
.unwrap();
runtime.run_event_loop(false).await.unwrap();
}
#[test]
fn test_has_tick_scheduled() {
use futures::task::ArcWake;
static MACROTASK: AtomicUsize = AtomicUsize::new(0);
static NEXT_TICK: AtomicUsize = AtomicUsize::new(0);
#[op]
fn op_macrotask() -> Result<(), AnyError> {
MACROTASK.fetch_add(1, Ordering::Relaxed);
Ok(())
}
#[op]
fn op_next_tick() -> Result<(), AnyError> {
NEXT_TICK.fetch_add(1, Ordering::Relaxed);
Ok(())
}
deno_core::extension!(test_ext, ops = [op_macrotask, op_next_tick]);
let mut runtime = JsRuntime::new(RuntimeOptions {
extensions: vec![test_ext::init_ops()],
..Default::default()
});
runtime
.execute_script_static(
"has_tick_scheduled.js",
r#"
Deno.core.setMacrotaskCallback(() => {
Deno.core.ops.op_macrotask();
return true; // We're done.
});
Deno.core.setNextTickCallback(() => Deno.core.ops.op_next_tick());
Deno.core.ops.op_set_has_tick_scheduled(true);
"#,
)
.unwrap();
struct ArcWakeImpl(Arc<AtomicUsize>);
impl ArcWake for ArcWakeImpl {
fn wake_by_ref(arc_self: &Arc<Self>) {
arc_self.0.fetch_add(1, Ordering::Relaxed);
}
}
let awoken_times = Arc::new(AtomicUsize::new(0));
let waker = futures::task::waker(Arc::new(ArcWakeImpl(awoken_times.clone())));
let cx = &mut Context::from_waker(&waker);
assert!(matches!(runtime.poll_event_loop(cx, false), Poll::Pending));
assert_eq!(1, MACROTASK.load(Ordering::Relaxed));
assert_eq!(1, NEXT_TICK.load(Ordering::Relaxed));
assert_eq!(awoken_times.swap(0, Ordering::Relaxed), 1);
assert!(matches!(runtime.poll_event_loop(cx, false), Poll::Pending));
assert_eq!(awoken_times.swap(0, Ordering::Relaxed), 1);
assert!(matches!(runtime.poll_event_loop(cx, false), Poll::Pending));
assert_eq!(awoken_times.swap(0, Ordering::Relaxed), 1);
assert!(matches!(runtime.poll_event_loop(cx, false), Poll::Pending));
assert_eq!(awoken_times.swap(0, Ordering::Relaxed), 1);
runtime.inner.state.borrow_mut().has_tick_scheduled = false;
assert!(matches!(
runtime.poll_event_loop(cx, false),
Poll::Ready(Ok(()))
));
assert_eq!(awoken_times.load(Ordering::Relaxed), 0);
assert!(matches!(
runtime.poll_event_loop(cx, false),
Poll::Ready(Ok(()))
));
assert_eq!(awoken_times.load(Ordering::Relaxed), 0);
}
#[test]
fn terminate_during_module_eval() {
#[derive(Default)]
struct ModsLoader;
impl ModuleLoader for ModsLoader {
fn resolve(
&self,
specifier: &str,
referrer: &str,
_kind: ResolutionKind,
) -> Result<ModuleSpecifier, Error> {
assert_eq!(specifier, "file:///main.js");
assert_eq!(referrer, ".");
let s = crate::resolve_import(specifier, referrer).unwrap();
Ok(s)
}
fn load(
&self,
_module_specifier: &ModuleSpecifier,
_maybe_referrer: Option<&ModuleSpecifier>,
_is_dyn_import: bool,
) -> Pin<Box<ModuleSourceFuture>> {
async move {
Ok(ModuleSource::for_test(
"console.log('hello world');",
"file:///main.js",
))
}
.boxed_local()
}
}
let loader = std::rc::Rc::new(ModsLoader::default());
let mut runtime = JsRuntime::new(RuntimeOptions {
module_loader: Some(loader),
..Default::default()
});
let specifier = crate::resolve_url("file:///main.js").unwrap();
let source_code = ascii_str!("Deno.core.print('hello\\n')");
let module_id = futures::executor::block_on(
runtime.load_main_module(&specifier, Some(source_code)),
)
.unwrap();
runtime.v8_isolate().terminate_execution();
let mod_result =
futures::executor::block_on(runtime.mod_evaluate(module_id)).unwrap();
assert!(mod_result
.unwrap_err()
.to_string()
.contains("JavaScript execution has been terminated"));
}
#[tokio::test]
async fn test_unhandled_rejection_order() {
let mut runtime = JsRuntime::new(Default::default());
runtime
.execute_script_static(
"",
r#"
for (let i = 0; i < 100; i++) {
Promise.reject(i);
}
"#,
)
.unwrap();
let err = runtime.run_event_loop(false).await.unwrap_err();
assert_eq!(err.to_string(), "Uncaught (in promise) 0");
}
#[tokio::test]
async fn test_set_promise_reject_callback() {
static PROMISE_REJECT: AtomicUsize = AtomicUsize::new(0);
#[op]
fn op_promise_reject() -> Result<(), AnyError> {
PROMISE_REJECT.fetch_add(1, Ordering::Relaxed);
Ok(())
}
deno_core::extension!(test_ext, ops = [op_promise_reject]);
let mut runtime = JsRuntime::new(RuntimeOptions {
extensions: vec![test_ext::init_ops()],
..Default::default()
});
runtime
.execute_script_static(
"promise_reject_callback.js",
r#"
// Note: |promise| is not the promise created below, it's a child.
Deno.core.ops.op_set_promise_reject_callback((type, promise, reason) => {
if (type !== /* PromiseRejectWithNoHandler */ 0) {
throw Error("unexpected type: " + type);
}
if (reason.message !== "reject") {
throw Error("unexpected reason: " + reason);
}
Deno.core.ops.op_store_pending_promise_rejection(promise);
Deno.core.ops.op_promise_reject();
});
new Promise((_, reject) => reject(Error("reject")));
"#,
)
.unwrap();
runtime.run_event_loop(false).await.unwrap_err();
assert_eq!(1, PROMISE_REJECT.load(Ordering::Relaxed));
runtime
.execute_script_static(
"promise_reject_callback.js",
r#"
{
const prev = Deno.core.ops.op_set_promise_reject_callback((...args) => {
prev(...args);
});
}
new Promise((_, reject) => reject(Error("reject")));
"#,
)
.unwrap();
runtime.run_event_loop(false).await.unwrap_err();
assert_eq!(2, PROMISE_REJECT.load(Ordering::Relaxed));
}
#[tokio::test]
async fn test_set_promise_reject_callback_realms() {
let mut runtime = JsRuntime::new(RuntimeOptions::default());
let global_realm = runtime.global_realm();
let realm1 = runtime.create_realm().unwrap();
let realm2 = runtime.create_realm().unwrap();
let realm_expectations = &[
(&global_realm, "global_realm", 42),
(&realm1, "realm1", 140),
(&realm2, "realm2", 720),
];
// Set up promise reject callbacks.
for (realm, realm_name, number) in realm_expectations {
realm
.execute_script(
runtime.v8_isolate(),
"",
format!(
r#"
globalThis.rejectValue = undefined;
Deno.core.setPromiseRejectCallback((_type, _promise, reason) => {{
globalThis.rejectValue = `{realm_name}/${{reason}}`;
}});
Deno.core.opAsync("op_void_async").then(() => Promise.reject({number}));
"#
).into()
)
.unwrap();
}
runtime.run_event_loop(false).await.unwrap();
for (realm, realm_name, number) in realm_expectations {
let reject_value = realm
.execute_script_static(runtime.v8_isolate(), "", "globalThis.rejectValue")
.unwrap();
let scope = &mut realm.handle_scope(runtime.v8_isolate());
let reject_value = v8::Local::new(scope, reject_value);
assert!(reject_value.is_string());
let reject_value_string = reject_value.to_rust_string_lossy(scope);
assert_eq!(reject_value_string, format!("{realm_name}/{number}"));
}
}
#[tokio::test]
async fn test_set_promise_reject_callback_top_level_await() {
static PROMISE_REJECT: AtomicUsize = AtomicUsize::new(0);
#[op]
fn op_promise_reject() -> Result<(), AnyError> {
PROMISE_REJECT.fetch_add(1, Ordering::Relaxed);
Ok(())
}
deno_core::extension!(test_ext, ops = [op_promise_reject]);
#[derive(Default)]
struct ModsLoader;
impl ModuleLoader for ModsLoader {
fn resolve(
&self,
specifier: &str,
referrer: &str,
_kind: ResolutionKind,
) -> Result<ModuleSpecifier, Error> {
assert_eq!(specifier, "file:///main.js");
assert_eq!(referrer, ".");
let s = crate::resolve_import(specifier, referrer).unwrap();
Ok(s)
}
fn load(
&self,
_module_specifier: &ModuleSpecifier,
_maybe_referrer: Option<&ModuleSpecifier>,
_is_dyn_import: bool,
) -> Pin<Box<ModuleSourceFuture>> {
let code = r#"
Deno.core.ops.op_set_promise_reject_callback((type, promise, reason) => {
Deno.core.ops.op_promise_reject();
});
throw new Error('top level throw');
"#;
async move { Ok(ModuleSource::for_test(code, "file:///main.js")) }
.boxed_local()
}
}
let mut runtime = JsRuntime::new(RuntimeOptions {
extensions: vec![test_ext::init_ops()],
module_loader: Some(Rc::new(ModsLoader)),
..Default::default()
});
let id = runtime
.load_main_module(&crate::resolve_url("file:///main.js").unwrap(), None)
.await
.unwrap();
let receiver = runtime.mod_evaluate(id);
runtime.run_event_loop(false).await.unwrap();
receiver.await.unwrap().unwrap_err();
assert_eq!(1, PROMISE_REJECT.load(Ordering::Relaxed));
}
#[test]
fn test_op_return_serde_v8_error() {
#[op]
fn op_err() -> Result<std::collections::BTreeMap<u64, u64>, anyhow::Error> {
Ok([(1, 2), (3, 4)].into_iter().collect()) // Maps can't have non-string keys in serde_v8
}
deno_core::extension!(test_ext, ops = [op_err]);
let mut runtime = JsRuntime::new(RuntimeOptions {
extensions: vec![test_ext::init_ops()],
..Default::default()
});
assert!(runtime
.execute_script_static(
"test_op_return_serde_v8_error.js",
"Deno.core.ops.op_err()"
)
.is_err());
}
#[test]
fn test_op_high_arity() {
#[op]
fn op_add_4(
x1: i64,
x2: i64,
x3: i64,
x4: i64,
) -> Result<i64, anyhow::Error> {
Ok(x1 + x2 + x3 + x4)
}
deno_core::extension!(test_ext, ops = [op_add_4]);
let mut runtime = JsRuntime::new(RuntimeOptions {
extensions: vec![test_ext::init_ops()],
..Default::default()
});
let r = runtime
.execute_script_static("test.js", "Deno.core.ops.op_add_4(1, 2, 3, 4)")
.unwrap();
let scope = &mut runtime.handle_scope();
assert_eq!(r.open(scope).integer_value(scope), Some(10));
}
#[test]
fn test_op_disabled() {
#[op]
fn op_foo() -> Result<i64, anyhow::Error> {
Ok(42)
}
fn ops() -> Vec<OpDecl> {
vec![op_foo::decl().disable()]
}
deno_core::extension!(test_ext, ops_fn = ops);
let mut runtime = JsRuntime::new(RuntimeOptions {
extensions: vec![test_ext::init_ops()],
..Default::default()
});
let err = runtime
.execute_script_static("test.js", "Deno.core.ops.op_foo()")
.unwrap_err();
assert!(err
.to_string()
.contains("TypeError: Deno.core.ops.op_foo is not a function"));
}
#[test]
fn test_op_detached_buffer() {
use serde_v8::DetachedBuffer;
#[op]
fn op_sum_take(b: DetachedBuffer) -> Result<u64, anyhow::Error> {
Ok(b.as_ref().iter().clone().map(|x| *x as u64).sum())
}
#[op]
fn op_boomerang(b: DetachedBuffer) -> Result<DetachedBuffer, anyhow::Error> {
Ok(b)
}
deno_core::extension!(test_ext, ops = [op_sum_take, op_boomerang]);
let mut runtime = JsRuntime::new(RuntimeOptions {
extensions: vec![test_ext::init_ops()],
..Default::default()
});
runtime
.execute_script_static(
"test.js",
r#"
const a1 = new Uint8Array([1,2,3]);
const a1b = a1.subarray(0, 3);
const a2 = new Uint8Array([5,10,15]);
const a2b = a2.subarray(0, 3);
if (!(a1.length > 0 && a1b.length > 0)) {
throw new Error("a1 & a1b should have a length");
}
let sum = Deno.core.ops.op_sum_take(a1b);
if (sum !== 6) {
throw new Error(`Bad sum: ${sum}`);
}
if (a1.length > 0 || a1b.length > 0) {
throw new Error("expecting a1 & a1b to be detached");
}
const a3 = Deno.core.ops.op_boomerang(a2b);
if (a3.byteLength != 3) {
throw new Error(`Expected a3.byteLength === 3, got ${a3.byteLength}`);
}
if (a3[0] !== 5 || a3[1] !== 10) {
throw new Error(`Invalid a3: ${a3[0]}, ${a3[1]}`);
}
if (a2.byteLength > 0 || a2b.byteLength > 0) {
throw new Error("expecting a2 & a2b to be detached, a3 re-attached");
}
const wmem = new WebAssembly.Memory({ initial: 1, maximum: 2 });
const w32 = new Uint32Array(wmem.buffer);
w32[0] = 1; w32[1] = 2; w32[2] = 3;
const assertWasmThrow = (() => {
try {
let sum = Deno.core.ops.op_sum_take(w32.subarray(0, 2));
return false;
} catch(e) {
return e.message.includes('invalid type; expected: detachable');
}
});
if (!assertWasmThrow()) {
throw new Error("expected wasm mem to not be detachable");
}
"#,
)
.unwrap();
}
#[test]
fn test_op_unstable_disabling() {
#[op]
fn op_foo() -> Result<i64, anyhow::Error> {
Ok(42)
}
#[op(unstable)]
fn op_bar() -> Result<i64, anyhow::Error> {
Ok(42)
}
deno_core::extension!(
test_ext,
ops = [op_foo, op_bar],
middleware = |op| if op.is_unstable { op.disable() } else { op }
);
let mut runtime = JsRuntime::new(RuntimeOptions {
extensions: vec![test_ext::init_ops()],
..Default::default()
});
runtime
.execute_script_static(
"test.js",
r#"
if (Deno.core.ops.op_foo() !== 42) {
throw new Error("Exptected op_foo() === 42");
}
if (typeof Deno.core.ops.op_bar !== "undefined") {
throw new Error("Expected op_bar to be disabled")
}
"#,
)
.unwrap();
}
#[test]
fn js_realm_simple() {
let mut runtime = JsRuntime::new(Default::default());
let main_context = runtime.global_context();
let main_global = {
let scope = &mut runtime.handle_scope();
let local_global = main_context.open(scope).global(scope);
v8::Global::new(scope, local_global)
};
let realm = runtime.create_realm().unwrap();
assert_ne!(realm.context(), &main_context);
assert_ne!(realm.global_object(runtime.v8_isolate()), main_global);
let main_object = runtime.execute_script_static("", "Object").unwrap();
let realm_object = realm
.execute_script_static(runtime.v8_isolate(), "", "Object")
.unwrap();
assert_ne!(main_object, realm_object);
}
#[test]
fn js_realm_init() {
#[op]
fn op_test() -> Result<String, Error> {
Ok(String::from("Test"))
}
deno_core::extension!(test_ext, ops = [op_test]);
let mut runtime = JsRuntime::new(RuntimeOptions {
extensions: vec![test_ext::init_ops()],
..Default::default()
});
let realm = runtime.create_realm().unwrap();
let ret = realm
.execute_script_static(runtime.v8_isolate(), "", "Deno.core.ops.op_test()")
.unwrap();
let scope = &mut realm.handle_scope(runtime.v8_isolate());
assert_eq!(ret, serde_v8::to_v8(scope, "Test").unwrap());
}
#[test]
fn js_realm_init_snapshot() {
let snapshot = {
let runtime =
JsRuntimeForSnapshot::new(Default::default(), Default::default());
let snap: &[u8] = &runtime.snapshot();
Vec::from(snap).into_boxed_slice()
};
#[op]
fn op_test() -> Result<String, Error> {
Ok(String::from("Test"))
}
deno_core::extension!(test_ext, ops = [op_test]);
let mut runtime = JsRuntime::new(RuntimeOptions {
startup_snapshot: Some(Snapshot::Boxed(snapshot)),
extensions: vec![test_ext::init_ops()],
..Default::default()
});
let realm = runtime.create_realm().unwrap();
let ret = realm
.execute_script_static(runtime.v8_isolate(), "", "Deno.core.ops.op_test()")
.unwrap();
let scope = &mut realm.handle_scope(runtime.v8_isolate());
assert_eq!(ret, serde_v8::to_v8(scope, "Test").unwrap());
}
#[test]
fn js_realm_sync_ops() {
// Test that returning a ZeroCopyBuf and throwing an exception from a sync
// op result in objects with prototypes from the right realm. Note that we
// don't test the result of returning structs, because they will be
// serialized to objects with null prototype.
#[op]
fn op_test(fail: bool) -> Result<ZeroCopyBuf, Error> {
if !fail {
Ok(ZeroCopyBuf::empty())
} else {
Err(crate::error::type_error("Test"))
}
}
deno_core::extension!(test_ext, ops = [op_test]);
let mut runtime = JsRuntime::new(RuntimeOptions {
extensions: vec![test_ext::init_ops()],
get_error_class_fn: Some(&|error| {
crate::error::get_custom_error_class(error).unwrap()
}),
..Default::default()
});
let new_realm = runtime.create_realm().unwrap();
// Test in both realms
for realm in [runtime.global_realm(), new_realm].into_iter() {
let ret = realm
.execute_script_static(
runtime.v8_isolate(),
"",
r#"
const buf = Deno.core.ops.op_test(false);
try {
Deno.core.ops.op_test(true);
} catch(e) {
err = e;
}
buf instanceof Uint8Array && buf.byteLength === 0 &&
err instanceof TypeError && err.message === "Test"
"#,
)
.unwrap();
assert!(ret.open(runtime.v8_isolate()).is_true());
}
}
#[tokio::test]
async fn js_realm_async_ops() {
// Test that returning a ZeroCopyBuf and throwing an exception from a async
// op result in objects with prototypes from the right realm. Note that we
// don't test the result of returning structs, because they will be
// serialized to objects with null prototype.
#[op]
async fn op_test(fail: bool) -> Result<ZeroCopyBuf, Error> {
if !fail {
Ok(ZeroCopyBuf::empty())
} else {
Err(crate::error::type_error("Test"))
}
}
deno_core::extension!(test_ext, ops = [op_test]);
let mut runtime = JsRuntime::new(RuntimeOptions {
extensions: vec![test_ext::init_ops()],
get_error_class_fn: Some(&|error| {
crate::error::get_custom_error_class(error).unwrap()
}),
..Default::default()
});
let global_realm = runtime.global_realm();
let new_realm = runtime.create_realm().unwrap();
let mut rets = vec![];
// Test in both realms
for realm in [global_realm, new_realm].into_iter() {
let ret = realm
.execute_script_static(
runtime.v8_isolate(),
"",
r#"
(async function () {
const buf = await Deno.core.opAsync("op_test", false);
let err;
try {
await Deno.core.opAsync("op_test", true);
} catch(e) {
err = e;
}
return buf instanceof Uint8Array && buf.byteLength === 0 &&
err instanceof TypeError && err.message === "Test" ;
})();
"#,
)
.unwrap();
rets.push((realm, ret));
}
runtime.run_event_loop(false).await.unwrap();
for ret in rets {
let scope = &mut ret.0.handle_scope(runtime.v8_isolate());
let value = v8::Local::new(scope, ret.1);
let promise = v8::Local::<v8::Promise>::try_from(value).unwrap();
let result = promise.result(scope);
assert!(result.is_boolean() && result.is_true());
}
}
#[ignore]
#[tokio::test]
async fn js_realm_gc() {
static INVOKE_COUNT: AtomicUsize = AtomicUsize::new(0);
struct PendingFuture {}
impl Future for PendingFuture {
type Output = ();
fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<()> {
Poll::Pending
}
}
impl Drop for PendingFuture {
fn drop(&mut self) {
assert_eq!(INVOKE_COUNT.fetch_sub(1, Ordering::SeqCst), 1);
}
}
// Never resolves.
#[op]
async fn op_pending() {
assert_eq!(INVOKE_COUNT.fetch_add(1, Ordering::SeqCst), 0);
PendingFuture {}.await
}
deno_core::extension!(test_ext, ops = [op_pending]);
let mut runtime = JsRuntime::new(RuntimeOptions {
extensions: vec![test_ext::init_ops()],
..Default::default()
});
// Detect a drop in OpState
let opstate_drop_detect = Rc::new(());
runtime
.op_state()
.borrow_mut()
.put(opstate_drop_detect.clone());
assert_eq!(Rc::strong_count(&opstate_drop_detect), 2);
let other_realm = runtime.create_realm().unwrap();
other_realm
.execute_script(
runtime.v8_isolate(),
"future",
ModuleCode::from_static("Deno.core.opAsync('op_pending')"),
)
.unwrap();
while INVOKE_COUNT.load(Ordering::SeqCst) == 0 {
poll_fn(|cx: &mut Context| runtime.poll_event_loop(cx, false))
.await
.unwrap();
}
drop(other_realm);
while INVOKE_COUNT.load(Ordering::SeqCst) == 1 {
poll_fn(|cx| runtime.poll_event_loop(cx, false))
.await
.unwrap();
}
drop(runtime);
// Make sure the OpState was dropped properly when the runtime dropped
assert_eq!(Rc::strong_count(&opstate_drop_detect), 1);
}
#[tokio::test]
async fn js_realm_ref_unref_ops() {
// Never resolves.
#[op]
async fn op_pending() {
futures::future::pending().await
}
deno_core::extension!(test_ext, ops = [op_pending]);
let mut runtime = JsRuntime::new(RuntimeOptions {
extensions: vec![test_ext::init_ops()],
..Default::default()
});
poll_fn(move |cx| {
let main_realm = runtime.global_realm();
let other_realm = runtime.create_realm().unwrap();
main_realm
.execute_script_static(
runtime.v8_isolate(),
"",
r#"
var promise = Deno.core.opAsync("op_pending");
"#,
)
.unwrap();
other_realm
.execute_script_static(
runtime.v8_isolate(),
"",
r#"
var promise = Deno.core.opAsync("op_pending");
"#,
)
.unwrap();
assert!(matches!(runtime.poll_event_loop(cx, false), Poll::Pending));
main_realm
.execute_script_static(
runtime.v8_isolate(),
"",
r#"
let promiseIdSymbol = Symbol.for("Deno.core.internalPromiseId");
Deno.core.unrefOp(promise[promiseIdSymbol]);
"#,
)
.unwrap();
assert!(matches!(runtime.poll_event_loop(cx, false), Poll::Pending));
other_realm
.execute_script_static(
runtime.v8_isolate(),
"",
r#"
let promiseIdSymbol = Symbol.for("Deno.core.internalPromiseId");
Deno.core.unrefOp(promise[promiseIdSymbol]);
"#,
)
.unwrap();
assert!(matches!(
runtime.poll_event_loop(cx, false),
Poll::Ready(Ok(()))
));
Poll::Ready(())
})
.await;
}
#[test]
fn test_array_by_copy() {
// Verify that "array by copy" proposal is enabled (https://github.com/tc39/proposal-change-array-by-copy)
let mut runtime = JsRuntime::new(Default::default());
assert!(runtime
.execute_script_static(
"test_array_by_copy.js",
"const a = [1, 2, 3];
const b = a.toReversed();
if (!(a[0] === 1 && a[1] === 2 && a[2] === 3)) {
throw new Error('Expected a to be intact');
}
if (!(b[0] === 3 && b[1] === 2 && b[2] === 1)) {
throw new Error('Expected b to be reversed');
}",
)
.is_ok());
}
#[cfg(debug_assertions)]
#[test]
#[should_panic(expected = "Found ops with duplicate names:")]
fn duplicate_op_names() {
mod a {
use super::*;
#[op]
fn op_test() -> Result<String, Error> {
Ok(String::from("Test"))
}
}
#[op]
fn op_test() -> Result<String, Error> {
Ok(String::from("Test"))
}
deno_core::extension!(test_ext, ops = [a::op_test, op_test]);
JsRuntime::new(RuntimeOptions {
extensions: vec![test_ext::init_ops()],
..Default::default()
});
}
#[test]
fn ops_in_js_have_proper_names() {
#[op]
fn op_test_sync() -> Result<String, Error> {
Ok(String::from("Test"))
}
#[op]
async fn op_test_async() -> Result<String, Error> {
Ok(String::from("Test"))
}
deno_core::extension!(test_ext, ops = [op_test_sync, op_test_async]);
let mut runtime = JsRuntime::new(RuntimeOptions {
extensions: vec![test_ext::init_ops()],
..Default::default()
});
let src = r#"
if (Deno.core.ops.op_test_sync.name !== "op_test_sync") {
throw new Error();
}
if (Deno.core.ops.op_test_async.name !== "op_test_async") {
throw new Error();
}
const { op_test_async } = Deno.core.ensureFastOps();
if (op_test_async.name !== "op_test_async") {
throw new Error();
}
"#;
runtime.execute_script_static("test", src).unwrap();
}