diff --git a/core/error.rs b/core/error.rs index 16f813b896..07dc98a22f 100644 --- a/core/error.rs +++ b/core/error.rs @@ -9,8 +9,7 @@ use std::fmt::Formatter; use anyhow::Error; -use crate::realm::JsRealm; -use crate::runtime::GetErrorClassFn; +use crate::runtime::JsRealm; use crate::runtime::JsRuntime; use crate::source_map::apply_source_map; use crate::source_map::get_source_line; @@ -20,6 +19,9 @@ use crate::url::Url; // TODO(ry) Deprecate AnyError and encourage deno_core::anyhow::Error instead. pub type AnyError = anyhow::Error; +pub type JsErrorCreateFn = dyn Fn(JsError) -> Error; +pub type GetErrorClassFn = &'static dyn for<'e> Fn(&'e Error) -> &'static str; + /// Creates a new error with a caller-specified error class name and message. pub fn custom_error( class: &'static str, @@ -643,6 +645,56 @@ fn abbrev_file_name(file_name: &str) -> Option { Some(format!("{}:{},{}......{}", url.scheme(), head, start, end)) } +pub(crate) fn exception_to_err_result( + scope: &mut v8::HandleScope, + exception: v8::Local, + in_promise: bool, +) -> Result { + let state_rc = JsRuntime::state_from(scope); + + let was_terminating_execution = scope.is_execution_terminating(); + // Disable running microtasks for a moment. When upgrading to V8 v11.4 + // we discovered that canceling termination here will cause the queued + // microtasks to run which breaks some tests. + scope.set_microtasks_policy(v8::MicrotasksPolicy::Explicit); + // If TerminateExecution was called, cancel isolate termination so that the + // exception can be created. Note that `scope.is_execution_terminating()` may + // have returned false if TerminateExecution was indeed called but there was + // no JS to execute after the call. + scope.cancel_terminate_execution(); + let mut exception = exception; + { + // If termination is the result of a `op_dispatch_exception` call, we want + // to use the exception that was passed to it rather than the exception that + // was passed to this function. + let state = state_rc.borrow(); + exception = if let Some(exception) = &state.dispatched_exception { + v8::Local::new(scope, exception.clone()) + } else if was_terminating_execution && exception.is_null_or_undefined() { + let message = v8::String::new(scope, "execution terminated").unwrap(); + v8::Exception::error(scope, message) + } else { + exception + }; + } + + let mut js_error = JsError::from_v8_exception(scope, exception); + if in_promise { + js_error.exception_message = format!( + "Uncaught (in promise) {}", + js_error.exception_message.trim_start_matches("Uncaught ") + ); + } + + if was_terminating_execution { + // Resume exception termination. + scope.terminate_execution(); + } + scope.set_microtasks_policy(v8::MicrotasksPolicy::Auto); + + Err(js_error.into()) +} + #[cfg(test)] mod tests { use super::*; diff --git a/core/lib.rs b/core/lib.rs index 336d9c2b98..82cd1dd435 100644 --- a/core/lib.rs +++ b/core/lib.rs @@ -1,7 +1,6 @@ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. mod async_cancel; mod async_cell; -mod bindings; pub mod error; mod error_codes; mod extensions; @@ -18,10 +17,8 @@ mod ops_builtin; mod ops_builtin_v8; mod ops_metrics; mod path; -mod realm; mod resources; mod runtime; -pub mod snapshot_util; mod source_map; pub mod task; mod task_queue; @@ -57,6 +54,8 @@ pub use crate::async_cell::AsyncRefCell; pub use crate::async_cell::AsyncRefFuture; pub use crate::async_cell::RcLike; pub use crate::async_cell::RcRef; +pub use crate::error::GetErrorClassFn; +pub use crate::error::JsErrorCreateFn; pub use crate::extensions::Extension; pub use crate::extensions::ExtensionBuilder; pub use crate::extensions::ExtensionFileSource; @@ -103,15 +102,13 @@ pub use crate::ops_builtin::op_void_async; pub use crate::ops_builtin::op_void_sync; pub use crate::ops_metrics::OpsTracker; pub use crate::path::strip_unc_prefix; -pub use crate::realm::JsRealm; pub use crate::resources::AsyncResult; pub use crate::resources::Resource; pub use crate::resources::ResourceId; pub use crate::resources::ResourceTable; pub use crate::runtime::CompiledWasmModuleStore; pub use crate::runtime::CrossIsolateStore; -pub use crate::runtime::GetErrorClassFn; -pub use crate::runtime::JsErrorCreateFn; +pub use crate::runtime::JsRealm; pub use crate::runtime::JsRuntime; pub use crate::runtime::JsRuntimeForSnapshot; pub use crate::runtime::RuntimeOptions; @@ -130,21 +127,30 @@ pub fn v8_version() -> &'static str { /// An internal module re-exporting functions used by the #[op] (`deno_ops`) macro #[doc(hidden)] pub mod _ops { - pub use super::bindings::throw_type_error; pub use super::error_codes::get_error_code; pub use super::ops::to_op_result; pub use super::ops::OpCtx; pub use super::ops::OpResult; - pub use super::runtime::map_async_op1; - pub use super::runtime::map_async_op2; - pub use super::runtime::map_async_op3; - pub use super::runtime::map_async_op4; - pub use super::runtime::queue_async_op; - pub use super::runtime::queue_fast_async_op; + pub use super::runtime::ops::map_async_op1; + pub use super::runtime::ops::map_async_op2; + pub use super::runtime::ops::map_async_op3; + pub use super::runtime::ops::map_async_op4; + pub use super::runtime::ops::queue_async_op; + pub use super::runtime::ops::queue_fast_async_op; + pub use super::runtime::throw_type_error; pub use super::runtime::V8_WRAPPER_OBJECT_INDEX; pub use super::runtime::V8_WRAPPER_TYPE_INDEX; } +// TODO(mmastrac): Temporary while we move code around +pub mod snapshot_util { + pub use crate::runtime::create_snapshot; + pub use crate::runtime::get_js_files; + pub use crate::runtime::CreateSnapshotOptions; + pub use crate::runtime::CreateSnapshotOutput; + pub use crate::runtime::FilterFn; +} + /// A helper macro that will return a call site in Rust code. Should be /// used when executing internal one-line scripts for JsRuntime lifecycle. /// diff --git a/core/modules/map.rs b/core/modules/map.rs index 4ab1466599..828d5888b7 100644 --- a/core/modules/map.rs +++ b/core/modules/map.rs @@ -1,6 +1,4 @@ -use crate::JsRuntime; // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. -use crate::bindings; use crate::error::generic_error; use crate::fast_string::FastString; use crate::modules::get_asserted_module_type_from_assertions; @@ -20,7 +18,8 @@ use crate::modules::NoopModuleLoader; use crate::modules::PrepareLoadFuture; use crate::modules::RecursiveModuleLoad; use crate::modules::ResolutionKind; -use crate::snapshot_util::SnapshottedData; +use crate::runtime::JsRuntime; +use crate::runtime::SnapshottedData; use anyhow::Error; use futures::future::FutureExt; use futures::stream::FuturesUnordered; @@ -467,7 +466,7 @@ impl ModuleMap { let name_str = name.v8(scope); let source_str = source.v8(scope); - let origin = bindings::module_origin(scope, name_str); + let origin = module_origin(scope, name_str); let source = v8::script_compiler::Source::new(source_str, Some(&origin)); let tc_scope = &mut v8::TryCatch::new(scope); @@ -820,3 +819,22 @@ fn json_module_evaluation_steps<'a>( resolver.resolve(tc_scope, undefined.into()); Some(resolver.get_promise(tc_scope).into()) } + +pub fn module_origin<'a>( + s: &mut v8::HandleScope<'a>, + resource_name: v8::Local<'a, v8::String>, +) -> v8::ScriptOrigin<'a> { + let source_map_url = v8::String::empty(s); + v8::ScriptOrigin::new( + s, + resource_name.into(), + 0, + 0, + false, + 123, + source_map_url.into(), + true, + false, + true, + ) +} diff --git a/core/modules/tests.rs b/core/modules/tests.rs index d32d7244e4..0eb7ce5149 100644 --- a/core/modules/tests.rs +++ b/core/modules/tests.rs @@ -1,8 +1,8 @@ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. use crate::ascii_str; use crate::resolve_import; -use crate::JsRuntime; -use crate::JsRuntimeForSnapshot; +use crate::runtime::JsRuntime; +use crate::runtime::JsRuntimeForSnapshot; use crate::RuntimeOptions; use crate::Snapshot; use deno_ops::op; diff --git a/core/ops.rs b/core/ops.rs index b766eb60d2..372ffe5b25 100644 --- a/core/ops.rs +++ b/core/ops.rs @@ -1,10 +1,10 @@ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. use crate::error::AnyError; +use crate::error::GetErrorClassFn; use crate::gotham_state::GothamState; -use crate::realm::ContextState; use crate::resources::ResourceTable; -use crate::runtime::GetErrorClassFn; +use crate::runtime::ContextState; use crate::runtime::JsRuntimeState; use crate::OpDecl; use crate::OpsTracker; diff --git a/core/ops_builtin_v8.rs b/core/ops_builtin_v8.rs index 8416546cbc..9fd906291b 100644 --- a/core/ops_builtin_v8.rs +++ b/core/ops_builtin_v8.rs @@ -1,5 +1,4 @@ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. -use crate::bindings::script_origin; use crate::error::custom_error; use crate::error::is_instance_of_error; use crate::error::range_error; @@ -7,6 +6,7 @@ use crate::error::type_error; use crate::error::JsError; use crate::ops_builtin::WasmStreamingResource; use crate::resolve_url; +use crate::runtime::script_origin; use crate::serde_v8::from_v8; use crate::source_map::apply_source_map; use crate::JsRealm; diff --git a/core/bindings.js b/core/runtime/bindings.js similarity index 100% rename from core/bindings.js rename to core/runtime/bindings.js diff --git a/core/bindings.rs b/core/runtime/bindings.rs similarity index 98% rename from core/bindings.rs rename to core/runtime/bindings.rs index 2be9b35b65..4cc27592f0 100644 --- a/core/bindings.rs +++ b/core/runtime/bindings.rs @@ -78,25 +78,6 @@ pub fn script_origin<'a>( ) } -pub fn module_origin<'a>( - s: &mut v8::HandleScope<'a>, - resource_name: v8::Local<'a, v8::String>, -) -> v8::ScriptOrigin<'a> { - let source_map_url = v8::String::empty(s); - v8::ScriptOrigin::new( - s, - resource_name.into(), - 0, - 0, - false, - 123, - source_map_url.into(), - true, - false, - true, - ) -} - fn get<'s, T>( scope: &mut v8::HandleScope<'s>, from: v8::Local, diff --git a/core/encode_decode_test.js b/core/runtime/encode_decode_test.js similarity index 100% rename from core/encode_decode_test.js rename to core/runtime/encode_decode_test.js diff --git a/core/error_builder_test.js b/core/runtime/error_builder_test.js similarity index 100% rename from core/error_builder_test.js rename to core/runtime/error_builder_test.js diff --git a/core/icudtl.dat b/core/runtime/icudtl.dat similarity index 100% rename from core/icudtl.dat rename to core/runtime/icudtl.dat diff --git a/core/realm.rs b/core/runtime/jsrealm.rs similarity index 99% rename from core/realm.rs rename to core/runtime/jsrealm.rs index d18f41e662..970b3f5d59 100644 --- a/core/realm.rs +++ b/core/runtime/jsrealm.rs @@ -1,9 +1,8 @@ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. - -use crate::bindings; +use super::bindings; +use crate::error::exception_to_err_result; use crate::modules::ModuleCode; use crate::ops::OpCtx; -use crate::runtime::exception_to_err_result; use crate::runtime::JsRuntimeState; use crate::task::MaskResultAsSend; use crate::JsRuntime; diff --git a/core/runtime.rs b/core/runtime/jsruntime.rs similarity index 52% rename from core/runtime.rs rename to core/runtime/jsruntime.rs index ecfd0bd571..3b41a90f19 100644 --- a/core/runtime.rs +++ b/core/runtime/jsruntime.rs @@ -1,8 +1,12 @@ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. -use crate::bindings; +use super::bindings; +use super::jsrealm::JsRealmInner; +use super::snapshot_util; +use crate::error::exception_to_err_result; use crate::error::generic_error; use crate::error::to_v8_type_error; +use crate::error::GetErrorClassFn; use crate::error::JsError; use crate::extensions::OpDecl; use crate::extensions::OpEventLoopFn; @@ -19,10 +23,8 @@ use crate::modules::ModuleLoader; use crate::modules::ModuleMap; use crate::modules::ModuleName; use crate::ops::*; -use crate::realm::ContextState; -use crate::realm::JsRealm; -use crate::realm::JsRealmInner; -use crate::snapshot_util; +use crate::runtime::ContextState; +use crate::runtime::JsRealm; use crate::source_map::SourceMapCache; use crate::source_map::SourceMapGetter; use crate::Extension; @@ -31,16 +33,14 @@ use crate::NoopModuleLoader; use crate::OpMiddlewareFn; use crate::OpResult; use crate::OpState; -use crate::PromiseId; +use crate::V8_WRAPPER_OBJECT_INDEX; +use crate::V8_WRAPPER_TYPE_INDEX; use anyhow::Context as AnyhowContext; use anyhow::Error; use futures::channel::oneshot; use futures::future::poll_fn; use futures::future::Future; -use futures::future::FutureExt; -use futures::future::MaybeDone; use futures::stream::StreamExt; -use futures::task::noop_waker; use smallvec::SmallVec; use std::any::Any; use std::cell::RefCell; @@ -50,7 +50,6 @@ use std::mem::ManuallyDrop; use std::ops::Deref; use std::ops::DerefMut; use std::option::Option; -use std::pin::Pin; use std::rc::Rc; use std::sync::atomic::AtomicBool; use std::sync::atomic::Ordering; @@ -69,20 +68,16 @@ pub enum Snapshot { Boxed(Box<[u8]>), } -pub type JsErrorCreateFn = dyn Fn(JsError) -> Error; - -pub type GetErrorClassFn = &'static dyn for<'e> Fn(&'e Error) -> &'static str; - /// Objects that need to live as long as the isolate #[derive(Default)] -struct IsolateAllocations { - near_heap_limit_callback_data: +pub(crate) struct IsolateAllocations { + pub(crate) near_heap_limit_callback_data: Option<(Box>, v8::NearHeapLimitCallback)>, } /// ManuallyDrop> is clone, but it returns a ManuallyDrop> which is a massive /// memory-leak footgun. -struct ManuallyDropRc(ManuallyDrop>); +pub(crate) struct ManuallyDropRc(ManuallyDrop>); impl ManuallyDropRc { pub fn clone(&self) -> Rc { @@ -115,9 +110,9 @@ impl DerefMut for ManuallyDropRc { /// /// This inner struct allows us to let the outer JsRuntime drop normally without a Drop impl, while we /// control dropping more closely here using ManuallyDrop. -struct InnerIsolateState { +pub(crate) struct InnerIsolateState { will_snapshot: bool, - state: ManuallyDropRc>, + pub(crate) state: ManuallyDropRc>, v8_isolate: ManuallyDrop, } @@ -212,9 +207,9 @@ impl InitMode { /// /// Use [`JsRuntimeForSnapshot`] to be able to create a snapshot. pub struct JsRuntime { - inner: InnerIsolateState, - module_map: Rc>, - allocations: IsolateAllocations, + pub(crate) inner: InnerIsolateState, + pub(crate) module_map: Rc>, + pub(crate) allocations: IsolateAllocations, extensions: Vec, event_loop_middlewares: Vec>, init_mode: InitMode, @@ -371,9 +366,6 @@ fn v8_init( v8::V8::initialize(); } -pub const V8_WRAPPER_TYPE_INDEX: i32 = 0; -pub const V8_WRAPPER_OBJECT_INDEX: i32 = 1; - #[derive(Default)] pub struct RuntimeOptions { /// Source map reference for errors. @@ -1668,56 +1660,6 @@ impl JsRuntimeState { } } -pub(crate) fn exception_to_err_result( - scope: &mut v8::HandleScope, - exception: v8::Local, - in_promise: bool, -) -> Result { - let state_rc = JsRuntime::state_from(scope); - - let was_terminating_execution = scope.is_execution_terminating(); - // Disable running microtasks for a moment. When upgrading to V8 v11.4 - // we discovered that canceling termination here will cause the queued - // microtasks to run which breaks some tests. - scope.set_microtasks_policy(v8::MicrotasksPolicy::Explicit); - // If TerminateExecution was called, cancel isolate termination so that the - // exception can be created. Note that `scope.is_execution_terminating()` may - // have returned false if TerminateExecution was indeed called but there was - // no JS to execute after the call. - scope.cancel_terminate_execution(); - let mut exception = exception; - { - // If termination is the result of a `op_dispatch_exception` call, we want - // to use the exception that was passed to it rather than the exception that - // was passed to this function. - let state = state_rc.borrow(); - exception = if let Some(exception) = &state.dispatched_exception { - v8::Local::new(scope, exception.clone()) - } else if was_terminating_execution && exception.is_null_or_undefined() { - let message = v8::String::new(scope, "execution terminated").unwrap(); - v8::Exception::error(scope, message) - } else { - exception - }; - } - - let mut js_error = JsError::from_v8_exception(scope, exception); - if in_promise { - js_error.exception_message = format!( - "Uncaught (in promise) {}", - js_error.exception_message.trim_start_matches("Uncaught ") - ); - } - - if was_terminating_execution { - // Resume exception termination. - scope.terminate_execution(); - } - scope.set_microtasks_policy(v8::MicrotasksPolicy::Auto); - - Err(js_error.into()) -} - // Related to module loading impl JsRuntime { pub(crate) fn instantiate_module( @@ -2472,2463 +2414,3 @@ impl JsRuntime { Ok(()) } } - -#[inline] -pub fn queue_fast_async_op( - ctx: &OpCtx, - promise_id: PromiseId, - op: impl Future> + 'static, -) { - let get_class = { - let state = RefCell::borrow(&ctx.state); - state.tracker.track_async(ctx.id); - state.get_error_class_fn - }; - let fut = op - .map(|result| crate::_ops::to_op_result(get_class, result)) - .boxed_local(); - // SAFETY: this this is guaranteed to be running on a current-thread executor - ctx.context_state.borrow_mut().pending_ops.spawn(unsafe { - crate::task::MaskFutureAsSend::new(OpCall::pending(ctx, promise_id, fut)) - }); -} - -#[inline] -pub fn map_async_op1( - ctx: &OpCtx, - op: impl Future> + 'static, -) -> MaybeDone>>> { - let get_class = { - let state = RefCell::borrow(&ctx.state); - state.tracker.track_async(ctx.id); - state.get_error_class_fn - }; - - let fut = op - .map(|result| crate::_ops::to_op_result(get_class, result)) - .boxed_local(); - MaybeDone::Future(fut) -} - -#[inline] -pub fn map_async_op2( - ctx: &OpCtx, - op: impl Future + 'static, -) -> MaybeDone>>> { - let state = RefCell::borrow(&ctx.state); - state.tracker.track_async(ctx.id); - - let fut = op.map(|result| OpResult::Ok(result.into())).boxed_local(); - MaybeDone::Future(fut) -} - -#[inline] -pub fn map_async_op3( - ctx: &OpCtx, - op: Result> + 'static, Error>, -) -> MaybeDone>>> { - let get_class = { - let state = RefCell::borrow(&ctx.state); - state.tracker.track_async(ctx.id); - state.get_error_class_fn - }; - - match op { - Err(err) => MaybeDone::Done(OpResult::Err(OpError::new(get_class, err))), - Ok(fut) => MaybeDone::Future( - fut - .map(|result| crate::_ops::to_op_result(get_class, result)) - .boxed_local(), - ), - } -} - -#[inline] -pub fn map_async_op4( - ctx: &OpCtx, - op: Result + 'static, Error>, -) -> MaybeDone>>> { - let get_class = { - let state = RefCell::borrow(&ctx.state); - state.tracker.track_async(ctx.id); - state.get_error_class_fn - }; - - match op { - Err(err) => MaybeDone::Done(OpResult::Err(OpError::new(get_class, err))), - Ok(fut) => MaybeDone::Future( - fut.map(|result| OpResult::Ok(result.into())).boxed_local(), - ), - } -} - -pub fn queue_async_op<'s>( - ctx: &OpCtx, - scope: &'s mut v8::HandleScope, - deferred: bool, - promise_id: PromiseId, - mut op: MaybeDone>>>, -) -> Option> { - // An op's realm (as given by `OpCtx::realm_idx`) must match the realm in - // which it is invoked. Otherwise, we might have cross-realm object exposure. - // deno_core doesn't currently support such exposure, even though embedders - // can cause them, so we panic in debug mode (since the check is expensive). - // TODO(mmastrac): Restore this - // debug_assert_eq!( - // runtime_state.borrow().context(ctx.realm_idx as usize, scope), - // Some(scope.get_current_context()) - // ); - - // All ops are polled immediately - let waker = noop_waker(); - let mut cx = Context::from_waker(&waker); - - // Note that MaybeDone returns () from the future - let op_call = match op.poll_unpin(&mut cx) { - Poll::Pending => { - let MaybeDone::Future(fut) = op else { - unreachable!() - }; - OpCall::pending(ctx, promise_id, fut) - } - Poll::Ready(_) => { - let mut op_result = Pin::new(&mut op).take_output().unwrap(); - // If the op is ready and is not marked as deferred we can immediately return - // the result. - if !deferred { - ctx.state.borrow_mut().tracker.track_async_completed(ctx.id); - return Some(op_result.to_v8(scope).unwrap()); - } - - OpCall::ready(ctx, promise_id, op_result) - } - }; - - // Otherwise we will push it to the `pending_ops` and let it be polled again - // or resolved on the next tick of the event loop. - ctx - .context_state - .borrow_mut() - .pending_ops - // SAFETY: this this is guaranteed to be running on a current-thread executor - .spawn(unsafe { crate::task::MaskFutureAsSend::new(op_call) }); - None -} - -#[cfg(test)] -pub mod tests { - use super::*; - use crate::ascii_str; - use crate::error::custom_error; - use crate::error::AnyError; - use crate::include_ascii_string; - use crate::modules::AssertedModuleType; - use crate::modules::ModuleInfo; - use crate::modules::ModuleSource; - use crate::modules::ModuleSourceFuture; - use crate::modules::ModuleType; - use crate::modules::ResolutionKind; - use crate::modules::SymbolicModule; - use crate::ZeroCopyBuf; - use deno_ops::op; - use std::pin::Pin; - use std::rc::Rc; - use std::sync::atomic::AtomicUsize; - use std::sync::atomic::Ordering; - use std::sync::Arc; - - // 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, - } - - #[op] - async fn op_test( - rc_op_state: Rc>, - control: u8, - buf: Option, - ) -> Result { - #![allow(clippy::await_holding_refcell_ref)] // False positive. - let op_state_ = rc_op_state.borrow(); - let test_state = op_state_.borrow::(); - 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) { - let dispatch_count = Arc::new(AtomicUsize::new(0)); - deno_core::extension!( - test_ext, - ops = [op_test], - options = { - mode: Mode, - dispatch_count: Arc, - }, - 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::().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::().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::().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 { - 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> { - 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::::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::().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::().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 { - 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> { - 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) { - 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 { - 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("", "").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 = 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::::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>, - ) -> Result<(), Error> { - let n = { - let op_state = op_state.borrow(); - let inner_state = op_state.borrow::(); - 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); - impl ArcWake for ArcWakeImpl { - fn wake_by_ref(arc_self: &Arc) { - 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 { - 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> { - 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 { - 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> { - 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, 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 { - 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 { - Ok(42) - } - - fn ops() -> Vec { - 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 { - Ok(b.as_ref().iter().clone().map(|x| *x as u64).sum()) - } - - #[op] - fn op_boomerang( - b: DetachedBuffer, - ) -> Result { - 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 { - Ok(42) - } - - #[op(unstable)] - fn op_bar() -> Result { - 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 { - 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 { - 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 { - 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 { - 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::::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 { - Ok(String::from("Test")) - } - } - - #[op] - fn op_test() -> Result { - 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 { - Ok(String::from("Test")) - } - - #[op] - async fn op_test_async() -> Result { - 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(); - } -} diff --git a/core/runtime/mod.rs b/core/runtime/mod.rs new file mode 100644 index 0000000000..2bd3ea9feb --- /dev/null +++ b/core/runtime/mod.rs @@ -0,0 +1,35 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +mod bindings; +mod jsrealm; +mod jsruntime; +#[doc(hidden)] +pub mod ops; +mod snapshot_util; + +#[cfg(test)] +mod tests; + +pub const V8_WRAPPER_TYPE_INDEX: i32 = 0; +pub const V8_WRAPPER_OBJECT_INDEX: i32 = 1; + +pub(crate) use jsrealm::ContextState; +pub use jsrealm::JsRealm; +pub use jsruntime::CompiledWasmModuleStore; +pub use jsruntime::CrossIsolateStore; +pub(crate) use jsruntime::InitMode; +pub use jsruntime::JsRuntime; +pub use jsruntime::JsRuntimeForSnapshot; +pub use jsruntime::JsRuntimeState; +pub use jsruntime::RuntimeOptions; +pub use jsruntime::RuntimeSnapshotOptions; +pub use jsruntime::SharedArrayBufferStore; +pub use jsruntime::Snapshot; +pub use snapshot_util::create_snapshot; +pub use snapshot_util::get_js_files; +pub use snapshot_util::CreateSnapshotOptions; +pub use snapshot_util::CreateSnapshotOutput; +pub use snapshot_util::FilterFn; +pub(crate) use snapshot_util::SnapshottedData; + +pub use bindings::script_origin; +pub use bindings::throw_type_error; diff --git a/core/runtime/ops.rs b/core/runtime/ops.rs new file mode 100644 index 0000000000..c9e7fa6c7c --- /dev/null +++ b/core/runtime/ops.rs @@ -0,0 +1,156 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +use crate::ops::*; +use crate::OpResult; +use crate::PromiseId; +use anyhow::Error; +use futures::future::Future; +use futures::future::FutureExt; +use futures::future::MaybeDone; +use futures::task::noop_waker; +use std::cell::RefCell; +use std::option::Option; +use std::pin::Pin; +use std::task::Context; +use std::task::Poll; + +#[inline] +pub fn queue_fast_async_op( + ctx: &OpCtx, + promise_id: PromiseId, + op: impl Future> + 'static, +) { + let get_class = { + let state = RefCell::borrow(&ctx.state); + state.tracker.track_async(ctx.id); + state.get_error_class_fn + }; + let fut = op + .map(|result| crate::_ops::to_op_result(get_class, result)) + .boxed_local(); + // SAFETY: this this is guaranteed to be running on a current-thread executor + ctx.context_state.borrow_mut().pending_ops.spawn(unsafe { + crate::task::MaskFutureAsSend::new(OpCall::pending(ctx, promise_id, fut)) + }); +} + +#[inline] +pub fn map_async_op1( + ctx: &OpCtx, + op: impl Future> + 'static, +) -> MaybeDone>>> { + let get_class = { + let state = RefCell::borrow(&ctx.state); + state.tracker.track_async(ctx.id); + state.get_error_class_fn + }; + + let fut = op + .map(|result| crate::_ops::to_op_result(get_class, result)) + .boxed_local(); + MaybeDone::Future(fut) +} + +#[inline] +pub fn map_async_op2( + ctx: &OpCtx, + op: impl Future + 'static, +) -> MaybeDone>>> { + let state = RefCell::borrow(&ctx.state); + state.tracker.track_async(ctx.id); + + let fut = op.map(|result| OpResult::Ok(result.into())).boxed_local(); + MaybeDone::Future(fut) +} + +#[inline] +pub fn map_async_op3( + ctx: &OpCtx, + op: Result> + 'static, Error>, +) -> MaybeDone>>> { + let get_class = { + let state = RefCell::borrow(&ctx.state); + state.tracker.track_async(ctx.id); + state.get_error_class_fn + }; + + match op { + Err(err) => MaybeDone::Done(OpResult::Err(OpError::new(get_class, err))), + Ok(fut) => MaybeDone::Future( + fut + .map(|result| crate::_ops::to_op_result(get_class, result)) + .boxed_local(), + ), + } +} + +#[inline] +pub fn map_async_op4( + ctx: &OpCtx, + op: Result + 'static, Error>, +) -> MaybeDone>>> { + let get_class = { + let state = RefCell::borrow(&ctx.state); + state.tracker.track_async(ctx.id); + state.get_error_class_fn + }; + + match op { + Err(err) => MaybeDone::Done(OpResult::Err(OpError::new(get_class, err))), + Ok(fut) => MaybeDone::Future( + fut.map(|result| OpResult::Ok(result.into())).boxed_local(), + ), + } +} + +pub fn queue_async_op<'s>( + ctx: &OpCtx, + scope: &'s mut v8::HandleScope, + deferred: bool, + promise_id: PromiseId, + mut op: MaybeDone>>>, +) -> Option> { + // An op's realm (as given by `OpCtx::realm_idx`) must match the realm in + // which it is invoked. Otherwise, we might have cross-realm object exposure. + // deno_core doesn't currently support such exposure, even though embedders + // can cause them, so we panic in debug mode (since the check is expensive). + // TODO(mmastrac): Restore this + // debug_assert_eq!( + // runtime_state.borrow().context(ctx.realm_idx as usize, scope), + // Some(scope.get_current_context()) + // ); + + // All ops are polled immediately + let waker = noop_waker(); + let mut cx = Context::from_waker(&waker); + + // Note that MaybeDone returns () from the future + let op_call = match op.poll_unpin(&mut cx) { + Poll::Pending => { + let MaybeDone::Future(fut) = op else { + unreachable!() + }; + OpCall::pending(ctx, promise_id, fut) + } + Poll::Ready(_) => { + let mut op_result = Pin::new(&mut op).take_output().unwrap(); + // If the op is ready and is not marked as deferred we can immediately return + // the result. + if !deferred { + ctx.state.borrow_mut().tracker.track_async_completed(ctx.id); + return Some(op_result.to_v8(scope).unwrap()); + } + + OpCall::ready(ctx, promise_id, op_result) + } + }; + + // Otherwise we will push it to the `pending_ops` and let it be polled again + // or resolved on the next tick of the event loop. + ctx + .context_state + .borrow_mut() + .pending_ops + // SAFETY: this this is guaranteed to be running on a current-thread executor + .spawn(unsafe { crate::task::MaskFutureAsSend::new(op_call) }); + None +} diff --git a/core/serialize_deserialize_test.js b/core/runtime/serialize_deserialize_test.js similarity index 100% rename from core/serialize_deserialize_test.js rename to core/runtime/serialize_deserialize_test.js diff --git a/core/snapshot_util.rs b/core/runtime/snapshot_util.rs similarity index 100% rename from core/snapshot_util.rs rename to core/runtime/snapshot_util.rs diff --git a/core/runtime/tests.rs b/core/runtime/tests.rs new file mode 100644 index 0000000000..857290b803 --- /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, +} + +#[op] +async fn op_test( + rc_op_state: Rc>, + control: u8, + buf: Option, +) -> Result { + #![allow(clippy::await_holding_refcell_ref)] // False positive. + let op_state_ = rc_op_state.borrow(); + let test_state = op_state_.borrow::(); + 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) { + let dispatch_count = Arc::new(AtomicUsize::new(0)); + deno_core::extension!( + test_ext, + ops = [op_test], + options = { + mode: Mode, + dispatch_count: Arc, + }, + 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::().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::().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::().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 { + 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> { + 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::::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::().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::().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 { + 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> { + 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) { + 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 { + 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("", "").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 = 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::::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>, + ) -> Result<(), Error> { + let n = { + let op_state = op_state.borrow(); + let inner_state = op_state.borrow::(); + 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); + impl ArcWake for ArcWakeImpl { + fn wake_by_ref(arc_self: &Arc) { + 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 { + 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> { + 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 { + 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> { + 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, 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 { + 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 { + Ok(42) + } + + fn ops() -> Vec { + 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 { + Ok(b.as_ref().iter().clone().map(|x| *x as u64).sum()) + } + + #[op] + fn op_boomerang(b: DetachedBuffer) -> Result { + 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 { + Ok(42) + } + + #[op(unstable)] + fn op_bar() -> Result { + 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 { + 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 { + 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 { + 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 { + 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::::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 { + Ok(String::from("Test")) + } + } + + #[op] + fn op_test() -> Result { + 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 { + Ok(String::from("Test")) + } + + #[op] + async fn op_test_async() -> Result { + 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(); +}