diff options
Diffstat (limited to 'core/runtime/tests.rs')
-rw-r--r-- | core/runtime/tests.rs | 2306 |
1 files changed, 2306 insertions, 0 deletions
diff --git a/core/runtime/tests.rs b/core/runtime/tests.rs new file mode 100644 index 000000000..857290b80 --- /dev/null +++ b/core/runtime/tests.rs @@ -0,0 +1,2306 @@ +// 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(); +} |