/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
libs/core/runtime/tests/misc.rs
2 115 строк
62 KB
Haruto Tanaka
fix(lsp): fix CPU busy loop by creating tokio runtime before JsRuntime in TSC thread (#35595)
06 июл 2026, 16:58
Не верифицирован
06 июл 2026, 16:58
ac8677d
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. use std::borrow::Cow; use std::collections::HashMap; use std::collections::HashSet; use std::future::poll_fn; use std::rc::Rc; use std::sync::Arc; use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicI8; use std::sync::atomic::AtomicUsize; use std::sync::atomic::Ordering; use std::task::Context; use std::task::Poll; use std::time::Duration; use std::time::Instant; use cooked_waker::IntoWaker; use cooked_waker::Wake; use cooked_waker::WakeRef; use deno_error::JsErrorBox; use parking_lot::Mutex; use rstest::rstest; use serde_json::Value; use serde_json::json; use url::Url; use crate::error::CoreErrorKind; use crate::modules::StaticModuleLoader; use crate::runtime::tests::Mode; use crate::runtime::tests::setup; use crate::*; #[test] fn icu() { // If this test fails, update core/runtime/icudtl.dat from // rusty_v8/third_party/icu/common/icudtl.dat let mut runtime = JsRuntime::new(Default::default()); runtime .execute_script("a.js", "(new Date()).toLocaleString('ja-JP')") .unwrap(); } #[test] fn test_execute_script_return_value() { let mut runtime = JsRuntime::new(Default::default()); let value_global = runtime.execute_script("a.js", "a = 1 + 2").unwrap(); { deno_core::scope!(scope, runtime); let value = value_global.open(scope); assert_eq!(value.integer_value(scope).unwrap(), 3); } let value_global = runtime.execute_script("b.js", "b = 'foobar'").unwrap(); { deno_core::scope!(scope, runtime); let value = value_global.open(scope); assert!(value.is_string()); assert_eq!( value.to_string(scope).unwrap().to_rust_string_lossy(scope), "foobar" ); } } #[derive(Default)] struct LoggingWaker { woken: AtomicBool, } impl Wake for LoggingWaker { fn wake(self) { self.woken.store(true, Ordering::SeqCst); } } impl WakeRef for LoggingWaker { fn wake_by_ref(&self) { self.woken.store(true, Ordering::SeqCst); } } /// This is a reproduction for a very obscure bug where the Deno runtime locks up we end up polling /// an empty JoinSet and attempt to resolve ops after-the-fact. There's a small footgun in the JoinSet /// API where polling it while empty returns Ready(None), which means that it never holds on to the /// waker. This means that if we aren't testing for this particular return value and don't stash the waker /// ourselves for a future async op to eventually queue, we can end up losing the waker entirely and the /// op wakes up, notifies tokio, which notifies the JoinSet, which then has nobody to notify )`:. #[tokio::test] async fn test_wakers_for_async_ops() { static STATE: AtomicI8 = AtomicI8::new(0); #[op2] async fn op_async_sleep() -> Result<(), JsErrorBox> { STATE.store(1, Ordering::SeqCst); tokio::time::sleep(std::time::Duration::from_millis(1)).await; STATE.store(2, Ordering::SeqCst); Ok(()) } STATE.store(0, Ordering::SeqCst); let logging_waker = Arc::new(LoggingWaker::default()); let waker = logging_waker.clone().into_waker(); deno_core::extension!(test_ext, ops = [op_async_sleep]); let mut runtime = JsRuntime::new(RuntimeOptions { extensions: vec![test_ext::init()], ..Default::default() }); // Drain events until we get to Ready loop { logging_waker.woken.store(false, Ordering::SeqCst); let res = runtime .poll_event_loop(&mut Context::from_waker(&waker), Default::default()); let ready = matches!(res, Poll::Ready(Ok(()))); assert!(ready || logging_waker.woken.load(Ordering::SeqCst)); if ready { break; } } // Start the AIIFE runtime .execute_script( "", ascii_str!( "const { op_async_sleep } = Deno.core.ops; (async () => { await op_async_sleep(); })()" ), ) .unwrap(); // Wait for future to finish while STATE.load(Ordering::SeqCst) < 2 { tokio::time::sleep(Duration::from_millis(1)).await; } // This shouldn't take one minute, but if it does, things are definitely locked up for _ in 0..Duration::from_secs(60).as_millis() { if logging_waker.woken.load(Ordering::SeqCst) { // Success return; } tokio::time::sleep(Duration::from_millis(1)).await; } panic!("The waker was never woken after the future completed"); } #[rstest] #[case("Promise.resolve(1 + 2)", Ok(3))] #[case("Promise.resolve(new Promise(resolve => resolve(2 + 2)))", Ok(4))] #[case( "Promise.reject(new Error('fail'))", Err("Error: fail\n at a.js:1:16") )] #[case( "new Promise(resolve => {})", Err( "Promise resolution is still pending but the event loop has already resolved" ) )] #[tokio::test] async fn test_resolve_promise( #[case] script: &'static str, #[case] result: Result<i32, &'static str>, ) { let mut runtime = JsRuntime::new(Default::default()); let value_global = runtime.execute_script("a.js", script).unwrap(); let resolve = runtime.resolve(value_global); let out = runtime .with_event_loop_promise(resolve, PollEventLoopOptions::default()) .await; deno_core::scope!(scope, runtime); match result { Ok(value) => { let out = v8::Local::new(scope, out.expect("expected success")); assert_eq!(out.int32_value(scope).unwrap(), value); } Err(err) => assert_eq!( out.expect_err("expected error").to_string(), err.to_string() ), } } #[rstest] #[case("script", "Promise.resolve(1 + 2)", Ok(Some(3)))] #[case( "script", "Promise.resolve(new Promise(resolve => resolve(2 + 2)))", Ok(Some(4)) )] #[case( "script", "Promise.reject(new Error('fail'))", Err("Uncaught (in promise) Error: fail") )] #[case("script", "new Promise(resolve => {})", Ok(None))] #[case("call", "async () => 1 + 2", Ok(Some(3)))] #[case( "call", "async () => { throw new Error('fail'); }", Err("Uncaught (in promise) Error: fail") )] #[case("call", "async () => new Promise(resolve => {})", Ok(None))] #[case("call", "() => Promise.resolve(1 + 2)", Ok(Some(3)))] #[case( "call", "() => Promise.resolve(new Promise(resolve => resolve(2 + 2)))", Ok(Some(4)) )] #[case( "call", "() => Promise.reject(new Error('fail'))", Err("Uncaught (in promise) Error: fail") )] #[case("call", "() => new Promise(resolve => {})", Ok(None))] #[case( "call", "() => { throw new Error('fail'); }", Err("Uncaught Error: fail") )] #[case( "call", "() => { Promise.reject(new Error('fail')); return 1; }", Ok(Some(1)) )] // V8 will not terminate the runtime properly before this call returns. This test may fail // in the future, but is being left as a form of change detection so we can see when this // happens. #[case( "call", "() => { Deno.core.reportUnhandledException(new Error('fail')); return 1; }", Ok(Some(1)) )] #[case( "call", "() => { Deno.core.reportUnhandledException(new Error('fail')); willNotCall(); }", Err("Uncaught Error: fail") )] #[tokio::test] async fn test_resolve_value( #[case] runner: &'static str, #[case] code: &'static str, #[case] output: Result<Option<u32>, &'static str>, ) { test_resolve_value_generic(runner, code, output).await } async fn test_resolve_value_generic( runner: &'static str, code: &'static str, output: Result<Option<u32>, &'static str>, ) { let mut runtime = JsRuntime::new(Default::default()); let result_global = if runner == "script" { let value_global: v8::Global<v8::Value> = runtime.execute_script("a.js", code).unwrap(); #[allow(deprecated, reason = "test code")] runtime.resolve_value(value_global).await } else if runner == "call" { let value_global = runtime.execute_script("a.js", code).unwrap(); let function: v8::Global<v8::Function> = unsafe { std::mem::transmute(value_global) }; #[allow(deprecated, reason = "test code")] runtime.call_and_await(&function).await } else { unreachable!() }; deno_core::scope!(scope, runtime); match output { Ok(None) => { let error_string = result_global.unwrap_err().to_string(); assert_eq!( "Promise resolution is still pending but the event loop has already resolved", error_string, ); } Ok(Some(v)) => { let value = result_global.unwrap(); let value = value.open(scope); assert_eq!(value.integer_value(scope).unwrap(), v as i64); } Err(e) => { let Err(err) = result_global else { let value = result_global.unwrap(); let value = value.open(scope); panic!( "Expected an error, got {}", value.to_rust_string_lossy(scope) ); }; let CoreErrorKind::Js(js_err) = err.into_kind() else { unreachable!() }; assert_eq!(e, js_err.exception_message); } } } // Uses `tokio::test` because, without `--wasm-test-streaming`, async // `WebAssembly.compile` finishes on a V8 background thread and posts a // foreground task to resolve the promise. The custom V8 platform only delivers // those foreground tasks when the isolate was created inside a tokio runtime // (see `register_isolate`), so `block_on` without tokio would hang. #[tokio::test] async 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 WebAssembly code, which should be terminated. let promise = runtime.execute_script("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(); #[allow(deprecated, reason = "test code")] runtime.resolve_value(promise).await.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( "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("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("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("simple.js", "1 + 1") .expect("execution should be possible again"); terminator_thread.join().unwrap(); } #[tokio::test] async fn wasm_streaming_op_invocation_in_import() { let (mut runtime, _dispatch_count) = setup(Mode::Async); // Run an infinite loop in WebAssembly code, which should be terminated. runtime.execute_script("setup.js", r#" Deno.core.setWasmStreamingCallback((source, rid) => { Deno.core.ops.op_wasm_streaming_set_url(rid, "file:///foo.wasm"); Deno.core.ops.op_wasm_streaming_feed(rid, source); Deno.core.close(rid); }); "#).unwrap(); let promise = runtime.execute_script("main.js", r#" // (module (import "env" "data" (global i64))) const bytes = new Uint8Array([0,97,115,109,1,0,0,0,2,13,1,3,101,110,118,4,100,97,116,97,3,126,0,0,8,4,110,97,109,101,2,1,0]); WebAssembly.instantiateStreaming(bytes, { env: { get data() { return new WebAssembly.Global({ value: "i64", mutable: false }, 42n); } } }); "#).unwrap(); #[allow(deprecated, reason = "test code")] let value = runtime.resolve_value(promise).await.unwrap(); deno_core::scope!(scope, runtime); let val = value.open(scope); assert!(val.is_object()); } #[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(); } /// 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("check.js", "null").unwrap(); } #[rstest] // https://github.com/denoland/deno/issues/29059 #[case(0.9999999999999999)] #[case(31.245270191439438)] #[case(117.63331139400017)] #[tokio::test] async fn test_preserve_float_precision_from_local_inspector_evaluate( #[case] input: f64, ) { let mut runtime = JsRuntime::new(RuntimeOptions { inspector: true, ..Default::default() }); let result = local_inspector_evaluate(&mut runtime, &format!("{}", input)); assert_eq!( result["result"]["value"], Value::Number(serde_json::Number::from_f64(input).unwrap()), ); } fn local_inspector_evaluate( runtime: &mut JsRuntime, expression: &str, ) -> Value { let kind = inspector::InspectorSessionKind::NonBlocking { wait_for_disconnect: false, }; let inspector = runtime.inspector(); let (tx, rx) = std::sync::mpsc::channel(); let callback = Box::new(move |msg: InspectorMsg| { if matches!(msg.kind, InspectorMsgKind::Message(1)) { let value: serde_json::Value = serde_json::from_str(&msg.content).unwrap(); let _ = tx.send(value["result"].clone()); } }); let mut local_inspector_session = JsRuntimeInspector::create_local_session(inspector, callback, kind); local_inspector_session.post_message( 1, "Runtime.evaluate", Some(json!({ "expression": expression, })), ); rx.try_recv().unwrap() } #[test] fn test_get_module_namespace() { let mut runtime = JsRuntime::new(RuntimeOptions { module_loader: Some(Rc::new(NoopModuleLoader)), ..Default::default() }); let specifier = crate::resolve_url("file:///main.js").unwrap(); let source_code = r#" export const a = "b"; export default 1 + 2; "#; let module_id = futures::executor::block_on( runtime.load_main_es_module_from_code(&specifier, source_code), ) .unwrap(); #[allow(clippy::let_underscore_future, reason = "test code")] let _ = runtime.mod_evaluate(module_id); let module_namespace = runtime.get_module_namespace(module_id).unwrap(); deno_core::scope!(scope, runtime); let module_namespace = v8::Local::<v8::Object>::new(scope, module_namespace); assert!(module_namespace.is_module_namespace_object()); let unknown_export_name = v8::String::new(scope, "none").unwrap(); let binding = module_namespace.get(scope, unknown_export_name.into()); assert!(binding.is_some()); assert!(binding.unwrap().is_undefined()); let empty_export_name = v8::String::new(scope, "").unwrap(); let binding = module_namespace.get(scope, empty_export_name.into()); assert!(binding.is_some()); assert!(binding.unwrap().is_undefined()); let a_export_name = v8::String::new(scope, "a").unwrap(); let binding = module_namespace.get(scope, a_export_name.into()); assert!(binding.unwrap().is_string()); assert_eq!(binding.unwrap(), v8::String::new(scope, "b").unwrap()); let default_export_name = v8::String::new(scope, "default").unwrap(); let binding = module_namespace.get(scope, default_export_name.into()); assert!(binding.unwrap().is_number()); assert_eq!(binding.unwrap(), v8::Number::new(scope, 3_f64)); } #[test] fn test_heap_limits() { // The GC pressure in this test makes V8 post delayed foreground tasks // (memory reducer), which require a tokio runtime context. let tokio = tokio::runtime::Builder::new_current_thread() .enable_time() .build() .unwrap(); let _guard = tokio.enter(); 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 js_err = runtime .execute_script( "script name", r#"let s = ""; while(true) { s += "Hello"; }"#, ) .expect_err("script should fail"); assert_eq!( "Uncaught Error: execution terminated", js_err.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() { // The GC pressure in this test makes V8 post delayed foreground tasks // (memory reducer), which require a tokio runtime context. let tokio = tokio::runtime::Builder::new_current_thread() .enable_time() .build() .unwrap(); let _guard = tokio.enter(); 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 js_err = runtime .execute_script( "script name", r#"let s = ""; while(true) { s += "Hello"; }"#, ) .expect_err("script should fail"); assert_eq!( "Uncaught Error: execution terminated", js_err.exception_message ); assert_eq!(0, callback_invoke_count_first.load(Ordering::SeqCst)); assert!(callback_invoke_count_second.load(Ordering::SeqCst) > 0); } #[tokio::test] async fn test_pump_message_loop() { let mut runtime = JsRuntime::new(RuntimeOptions::default()); poll_fn(move |cx| { runtime .execute_script( "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, Default::default()) { Poll::Ready(Ok(())) => {} _ => panic!(), }; // noop script, will resolve promise from first script runtime .execute_script("pump_message_loop2.js", r#"assertEquals(1, 1);"#) .unwrap(); // check that promise from `Atomics.waitAsync` has been resolved runtime .execute_script( "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("<none>", "").unwrap(); } #[ignore] // TODO(@littledivy): Fast API ops when snapshot is not loaded. #[test] fn test_is_proxy() { let mut runtime = JsRuntime::new(RuntimeOptions::default()); let all_true: v8::Global<v8::Value> = runtime .execute_script( "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(); deno_core::scope!(scope, runtime); let all_true = v8::Local::<v8::Value>::new(scope, &all_true); assert!(all_true.is_true()); } #[tokio::test] async fn test_set_macrotask_callback_set_next_tick_callback() { #[op2] async fn op_async_sleep() -> Result<(), JsErrorBox> { // 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()], ..Default::default() }); runtime .execute_script( "macrotasks_and_nextticks.js", r#" const { op_async_sleep } = Deno.core.ops; (async function () { const results = []; Deno.core.queueNextTick({ callback: () => results.push("nextTick"), args: undefined, snapshot: undefined, }); const imm = { _idleNext: null, _idlePrev: null, _onImmediate: () => results.push("immediate"), _argv: null, _destroyed: false, _refed: false, ref() { this._refed = true; return this; }, unref() { this._refed = false; return this; }, }; Deno.core.queueImmediate(imm); await op_async_sleep(); if (results[0] != "nextTick") { throw new Error(`expected nextTick, got: ${results[0]}`); } // Manually trigger immediate callbacks to test they were registered Deno.core.runImmediateCallbacks(); if (results[1] != "immediate") { throw new Error(`expected immediate, got: ${results[1]}`); } })(); "#, ) .unwrap(); runtime.run_event_loop(Default::default()).await.unwrap(); } #[tokio::test] async fn test_next_tick() { static NEXT_TICK: AtomicUsize = AtomicUsize::new(0); #[allow(clippy::unnecessary_wraps, reason = "test code")] #[op2(fast)] fn op_next_tick() -> Result<(), JsErrorBox> { NEXT_TICK.fetch_add(1, Ordering::Relaxed); Ok(()) } #[op2] async fn op_async_sleep() -> Result<(), JsErrorBox> { tokio::time::sleep(std::time::Duration::from_millis(1)).await; Ok(()) } deno_core::extension!(test_ext, ops = [op_next_tick, op_async_sleep]); let mut runtime = JsRuntime::new(RuntimeOptions { extensions: vec![test_ext::init()], ..Default::default() }); runtime .execute_script( "has_tick_scheduled.js", r#" (async function() { // Queue multiple ticks and verify they all drain Deno.core.queueNextTick({ callback: () => Deno.core.ops.op_next_tick(), args: undefined, snapshot: undefined, }); Deno.core.queueNextTick({ callback: () => Deno.core.ops.op_next_tick(), args: undefined, snapshot: undefined, }); Deno.core.queueNextTick({ callback: () => Deno.core.ops.op_next_tick(), args: undefined, snapshot: undefined, }); // Wait for the event loop to drain the ticks await Deno.core.ops.op_async_sleep(); if (Deno.core.ops.op_next_tick.length !== 0) { // Just a no-op to ensure op is used } })(); "#, ) .unwrap(); runtime.run_event_loop(Default::default()).await.unwrap(); assert_eq!(3, NEXT_TICK.load(Ordering::Relaxed)); } /// Test that promise rejection processing is interleaved with nextTick /// draining inside processTicksAndRejections, matching Node.js behavior. /// A rejection handler that queues a nextTick should have that tick /// drained in the same processTicksAndRejections cycle. #[tokio::test] async fn test_promise_rejection_nexttick_interleave() { #[op2] async fn op_async_sleep() -> Result<(), JsErrorBox> { 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()], ..Default::default() }); runtime .execute_script( "promise_rejection_nexttick_interleave.js", r#" const { op_async_sleep } = Deno.core.ops; (async function () { const order = []; // Register a rejection handler that queues a nextTick Deno.core.setUnhandledPromiseRejectionHandler((promise, reason) => { order.push("rejection-handler"); Deno.core.queueNextTick({ callback: () => order.push("tick-from-rejection-handler"), args: undefined, snapshot: undefined, }); return true; // handled }); // Queue a tick that creates an unhandled rejection Deno.core.queueNextTick({ callback: () => { order.push("first-tick"); Promise.reject(new Error("test rejection")); }, args: undefined, snapshot: undefined, }); // Wait for the event loop to process everything await op_async_sleep(); // Give one more turn for the rejection + tick to drain await op_async_sleep(); const result = order.join(","); const expected = "first-tick,rejection-handler,tick-from-rejection-handler"; if (result !== expected) { throw new Error("expected '" + expected + "' but got '" + result + "'"); } })(); "#, ) .unwrap(); runtime.run_event_loop(Default::default()).await.unwrap(); } /// Test that when a nextTick callback throws, subsequent ticks still drain. /// Matches Node.js behavior where TriggerUncaughtException dispatches the /// error and then re-enters processTicksAndRejections. #[tokio::test] async fn test_next_tick_error_continues_drain() { static TICKS_RUN: AtomicUsize = AtomicUsize::new(0); #[allow(clippy::unnecessary_wraps, reason = "test code")] #[op2(fast)] fn op_tick_count() -> Result<(), JsErrorBox> { TICKS_RUN.fetch_add(1, Ordering::Relaxed); Ok(()) } #[op2] async fn op_async_sleep() -> Result<(), JsErrorBox> { tokio::time::sleep(std::time::Duration::from_millis(1)).await; Ok(()) } deno_core::extension!(test_ext, ops = [op_tick_count, op_async_sleep]); let mut runtime = JsRuntime::new(RuntimeOptions { extensions: vec![test_ext::init()], ..Default::default() }); runtime .execute_script( "nexttick_error_continues.js", r#" Deno.core.setReportExceptionCallback((e) => { // Swallow the error so the event loop doesn't abort }); Deno.core.queueNextTick({ callback: () => Deno.core.ops.op_tick_count(), args: undefined, snapshot: undefined, }); Deno.core.queueNextTick({ callback: () => { throw new Error("boom"); }, args: undefined, snapshot: undefined, }); Deno.core.queueNextTick({ callback: () => Deno.core.ops.op_tick_count(), args: undefined, snapshot: undefined, }); (async () => { await Deno.core.ops.op_async_sleep(); })(); "#, ) .unwrap(); runtime.run_event_loop(Default::default()).await.unwrap(); // All 3 ticks should have been attempted; the 2 non-throwing ones // increment the counter. assert_eq!(2, TICKS_RUN.load(Ordering::Relaxed)); } #[test] fn terminate_during_module_eval() { let mut runtime = JsRuntime::new(RuntimeOptions { module_loader: Some(Rc::new(NoopModuleLoader)), ..Default::default() }); let specifier = crate::resolve_url("file:///main.js").unwrap(); let module_id = futures::executor::block_on( runtime .load_main_es_module_from_code(&specifier, "Deno.core.print('hello\\n')"), ) .unwrap(); runtime.v8_isolate().terminate_execution(); let mod_result = futures::executor::block_on(runtime.mod_evaluate(module_id)).unwrap_err(); assert!(mod_result.to_string().contains("terminated")); } async fn test_promise_rejection_handler_generic( module: bool, case: &'static str, error: Option<&'static str>, ) { #[op2(fast)] fn op_breakpoint() {} deno_core::extension!(test_ext, ops = [op_breakpoint]); // We don't test throw_ cases in non-module mode since those don't reject if !module && case.starts_with("throw_") { return; } let mut runtime = JsRuntime::new(RuntimeOptions { extensions: vec![test_ext::init()], ..Default::default() }); let script = r#" let test = "__CASE__"; function throwError() { throw new Error("boom"); } const { op_void_async, op_void_async_deferred } = Deno.core.ops; if (test != "no_handler") { Deno.core.setUnhandledPromiseRejectionHandler((promise, rejection) => { if (test.startsWith("exception_")) { try { throwError(); } catch (e) { Deno.core.reportUnhandledException(e); } } return test.endsWith("_true"); }); } if (test != "no_reject") { if (test.startsWith("async_op_eager_")) { op_void_async().then(() => { Deno.core.ops.op_breakpoint(); throw new Error("fail") }); } else if (test.startsWith("async_op_deferred_")) { op_void_async_deferred().then(() => { Deno.core.ops.op_breakpoint(); throw new Error("fail") }); } else if (test.startsWith("throw_")) { Deno.core.ops.op_breakpoint(); throw new Error("fail"); } else { Deno.core.ops.op_breakpoint(); Promise.reject(new Error("fail")); } } "# .replace("__CASE__", case); let future = if module { let id = runtime .load_main_es_module_from_code( &Url::parse("file:///test.js").unwrap(), script, ) .await .unwrap(); Some(runtime.mod_evaluate(id)) } else { runtime.execute_script("", script).unwrap(); None }; let res = runtime.run_event_loop(Default::default()).await; if let Some(error) = error { let err = res.expect_err("Expected a failure"); let CoreErrorKind::Js(js_error) = err.into_kind() else { panic!("Expected a JsError"); }; assert_eq!(js_error.exception_message, error); } else { assert!(res.is_ok()); } // Module evaluation will be successful in all cases except the one that throws at // the top level. if let Some(f) = future { f.await.expect("expected module resolution to succeed"); } } #[rstest] // Don't throw anything -- success #[case::no_reject("no_reject", None)] // Reject with no handler #[case::no_handler("no_handler", Some("Uncaught (in promise) Error: fail"))] // Exception thrown in unhandled rejection handler #[case::exception_true("exception_true", Some("Uncaught Error: boom"))] #[case::exception_false("exception_false", Some("Uncaught Error: boom"))] // Standard promise rejection #[case::return_true("return_true", None)] #[case::return_false("return_false", Some("Uncaught (in promise) Error: fail"))] // Top-level await throw #[case::throw_true("throw_true", None)] #[case::throw_false("throw_false", Some("Uncaught (in promise) Error: fail"))] // Eager async op, throw from "then" #[case::async_op_eager_true("async_op_eager_true", None)] #[case::async_op_eager_false( "async_op_eager_false", Some("Uncaught (in promise) Error: fail") )] // Deferred async op, throw from "then" #[case::async_op_deferred_true("async_op_deferred_true", None)] #[case::async_op_deferred_false( "async_op_deferred_false", Some("Uncaught (in promise) Error: fail") )] #[tokio::test] async fn test_promise_rejection_handler( #[case] case: &'static str, #[case] error: Option<&'static str>, #[values(true, false)] module: bool, ) { test_promise_rejection_handler_generic(module, case, error).await } // Verify that the async context (continuation-preserved embedder data) that // was active at the time of a promise rejection is restored when the // unhandled promise rejection handler is called. This is required for // AsyncLocalStorage to work correctly inside unhandledRejection handlers // (matching Node.js behavior). See https://github.com/denoland/deno/issues/30135 #[tokio::test] async fn test_promise_rejection_handler_preserves_async_context() { let mut runtime = JsRuntime::new(Default::default()); let script = r#" const v = new Deno.core.AsyncVariable(); let capturedValue = undefined; Deno.core.setUnhandledPromiseRejectionHandler((promise, rejection) => { capturedValue = v.get(); return true; }); // Enter an async context with a known value, then reject a promise const prev = v.enter("my_context_data"); Promise.reject(new Error("fail")); Deno.core.setAsyncContext(prev); // capturedValue will be checked after the event loop tick "#; runtime.execute_script("", script).unwrap(); runtime .run_event_loop(Default::default()) .await .expect("Event loop should complete without error"); // Verify the handler saw the correct async context let result = runtime .execute_script( "", "if (capturedValue !== 'my_context_data') { throw new Error('expected my_context_data but got ' + capturedValue); }", ) .unwrap(); drop(result); } // Make sure that stalled top-level awaits (that is, top-level awaits that // aren't tied to the progress of some op) are correctly reported, even in a // realm other than the main one. #[tokio::test] async fn test_stalled_tla() { let loader = StaticModuleLoader::with( Url::parse("file:///test.js").unwrap(), "await new Promise(() => {});", ); let mut runtime = JsRuntime::new(RuntimeOptions { module_loader: Some(Rc::new(loader)), ..Default::default() }); let module_id = runtime .load_main_es_module(&crate::resolve_url("file:///test.js").unwrap()) .await .unwrap(); #[allow(clippy::let_underscore_future, reason = "test code")] let _ = runtime.mod_evaluate(module_id); let error = runtime .run_event_loop(Default::default()) .await .unwrap_err(); let CoreErrorKind::Js(js_error) = error.into_kind() else { unreachable!() }; assert_eq!( &js_error.exception_message, "Top-level await promise never resolved" ); assert_eq!(js_error.frames.len(), 1); assert_eq!( js_error.frames[0].file_name.as_deref(), Some("file:///test.js") ); assert_eq!(js_error.frames[0].line_number, Some(1)); assert_eq!(js_error.frames[0].column_number, Some(1)); } // Regression test for https://github.com/denoland/deno/issues/20034. #[tokio::test] async fn test_dynamic_import_module_error_stack() { #[op2] async fn op_async_error() -> Result<(), JsErrorBox> { tokio::time::sleep(std::time::Duration::from_millis(1)).await; Err(deno_error::JsErrorBox::type_error("foo")) } deno_core::extension!(test_ext, ops = [op_async_error]); let loader = StaticModuleLoader::new([ ( Url::parse("file:///main.js").unwrap(), "await import(\"file:///import.js\");", ), ( Url::parse("file:///import.js").unwrap(), "const { op_async_error } = Deno.core.ops; await op_async_error();", ), ]); let mut runtime = JsRuntime::new(RuntimeOptions { extensions: vec![test_ext::init()], module_loader: Some(Rc::new(loader)), ..Default::default() }); let module_id = runtime .load_main_es_module(&crate::resolve_url("file:///main.js").unwrap()) .await .unwrap(); #[allow(clippy::let_underscore_future, reason = "test code")] let _ = runtime.mod_evaluate(module_id); let error = runtime .run_event_loop(Default::default()) .await .unwrap_err(); let CoreErrorKind::Js(js_error) = error.into_kind() else { unreachable!() }; let error_str = js_error.to_string(); assert!( error_str.contains("TypeError: foo"), "Expected error to contain 'TypeError: foo', got: {error_str}" ); assert!( error_str.contains("at async file:///import.js:1:43"), "Expected error to contain import.js stack frame, got: {error_str}" ); } #[tokio::test] #[should_panic( expected = "Failed to initialize a JsRuntime: Top-level await is not allowed in synchronous evaluation" )] async fn tla_in_esm_extensions_panics() { #[op2] async fn op_wait(#[number] ms: usize) { tokio::time::sleep(Duration::from_millis(ms as u64)).await } deno_core::extension!( test_ext, ops = [op_wait], esm_entry_point = "mod:test", esm = [ "mod:test" = { source = "import 'mod:tla';" }, "mod:tla" = { source = r#" const { op_wait } = Deno.core.ops; await op_wait(0); export const TEST = "foo"; "# } ], ); // Panics let _runtime = JsRuntime::new(RuntimeOptions { module_loader: Some(Rc::new(StaticModuleLoader::default())), extensions: vec![test_ext::init()], ..Default::default() }); } #[tokio::test] async fn generic_in_extension_middleware() { trait WelcomeWorld { fn hello(&self) -> String; } struct English; impl WelcomeWorld for English { fn hello(&self) -> String { "Hello World".to_string() } } #[op2] #[string] fn say_greeting<W: WelcomeWorld + 'static>(state: &mut OpState) -> String { let welcomer = state.borrow::<W>(); welcomer.hello() } #[op2] #[string] pub fn say_goodbye() -> String { "Goodbye!".to_string() } deno_core::extension!(welcome_ext, parameters = [W: WelcomeWorld], ops = [say_greeting<W>, say_goodbye], middleware = |op| { match op.name { "say_goodbye" => say_greeting::<W>(), _ => op, } }, ); let mut runtime = JsRuntime::new(RuntimeOptions { extensions: vec![welcome_ext::init::<English>()], ..Default::default() }); { let op_state = runtime.op_state(); let mut state = op_state.borrow_mut(); state.put(English); } let value_global = runtime .execute_script( "greet.js", r#" const greet = Deno.core.ops.say_greeting(); const bye = Deno.core.ops.say_goodbye(); greet + " and " + bye; "#, ) .unwrap(); // Check the result deno_core::scope!(scope, &mut runtime); let value = value_global.open(scope); let result = value.to_rust_string_lossy(scope); assert_eq!(result, "Hello World and Hello World"); } // TODO(mmastrac): This is only fired in debug mode #[cfg(debug_assertions)] #[tokio::test] #[should_panic( expected = r#"Failed to initialize a JsRuntime: Error: This fails at a (mod:error:2:30) at mod:error:3:9"# )] async fn esm_extensions_throws() { #[op2] async fn op_wait(#[number] ms: usize) { tokio::time::sleep(Duration::from_millis(ms as u64)).await } deno_core::extension!( test_ext, ops = [op_wait], esm_entry_point = "mod:test", esm = [ "mod:test" = { source = "import 'mod:error';" }, "mod:error" = { source = r#" function a() { throw new Error("This fails") }; a(); "# } ], ); // Panics let _runtime = JsRuntime::new(RuntimeOptions { module_loader: Some(Rc::new(StaticModuleLoader::default())), extensions: vec![test_ext::init()], ..Default::default() }); } fn create_spawner_runtime() -> JsRuntime { let mut runtime = JsRuntime::new(RuntimeOptions { ..Default::default() }); runtime .execute_script("main", ascii_str!("function f() { return 42; }")) .unwrap(); runtime } fn call_i32_function(scope: &mut v8::PinScope) -> i32 { let ctx = scope.get_current_context(); let global = ctx.global(scope); let key = v8::String::new_external_onebyte_static(scope, b"f") .unwrap() .into(); let f: v8::Local<'_, v8::Function> = global.get(scope, key).unwrap().try_into().unwrap(); let recv = v8::undefined(scope).into(); let res: v8::Local<v8::Integer> = f.call(scope, recv, &[]).unwrap().try_into().unwrap(); res.int32_value(scope).unwrap() } #[tokio::test] async fn task_spawner() { let mut runtime = create_spawner_runtime(); let value = Arc::new(AtomicUsize::new(0)); let value_clone = value.clone(); runtime .op_state() .borrow() .borrow::<V8TaskSpawner>() .spawn(move |scope| { let res = call_i32_function(scope); value_clone.store(res as _, Ordering::SeqCst); }); poll_fn(|cx| runtime.poll_event_loop(cx, Default::default())) .await .unwrap(); assert_eq!(value.load(Ordering::SeqCst), 42); } #[tokio::test] async fn task_spawner_cross_thread() { let mut runtime = create_spawner_runtime(); let value = Arc::new(AtomicUsize::new(0)); let value_clone = value.clone(); let spawner = runtime .op_state() .borrow() .borrow::<V8CrossThreadTaskSpawner>() .clone(); let barrier = Arc::new(std::sync::Barrier::new(2)); let barrier2 = barrier.clone(); std::thread::spawn(move || { barrier2.wait(); spawner.spawn(move |scope| { let res = call_i32_function(scope); value_clone.store(res as _, Ordering::SeqCst); }); }); barrier.wait(); // Async spin while we wait for this to complete let start = Instant::now(); while value.load(Ordering::SeqCst) != 42 { poll_fn(|cx| runtime.poll_event_loop(cx, Default::default())) .await .unwrap(); tokio::time::sleep(Duration::from_millis(10)).await; assert!(start.elapsed().as_secs() < 180); } } #[tokio::test] async fn task_spawner_cross_thread_blocking() { let mut runtime = create_spawner_runtime(); let value = Arc::new(AtomicUsize::new(0)); let value_clone = value.clone(); let spawner = runtime .op_state() .borrow() .borrow::<V8CrossThreadTaskSpawner>() .clone(); let barrier = Arc::new(std::sync::Barrier::new(2)); let barrier2 = barrier.clone(); std::thread::spawn(move || { barrier2.wait(); let res = spawner.spawn_blocking(call_i32_function); value_clone.store(res as _, Ordering::SeqCst); }); barrier.wait(); // Async spin while we wait for this to complete let start = Instant::now(); while value.load(Ordering::SeqCst) != 42 { poll_fn(|cx| runtime.poll_event_loop(cx, Default::default())) .await .unwrap(); tokio::time::sleep(Duration::from_millis(10)).await; assert!(start.elapsed().as_secs() < 1800); } } #[tokio::test] async fn terminate_execution_run_event_loop_js() { #[op2] async fn op_async_sleep() -> Result<(), JsErrorBox> { tokio::time::sleep(std::time::Duration::from_millis(100)).await; Ok(()) } deno_core::extension!(test_ext, ops = [op_async_sleep]); let mut runtime = JsRuntime::new(RuntimeOptions { extensions: vec![test_ext::init()], ..Default::default() }); // Start async task runtime.execute_script("sleep.js", "(async () => { while (true) { await Deno.core.ops.op_async_sleep(); } })()").unwrap(); // Terminate execution after 1 second. let v8_isolate_handle = runtime.v8_isolate().thread_safe_handle(); let barrier = Arc::new(std::sync::Barrier::new(2)); let barrier2 = barrier.clone(); let terminator_thread = std::thread::spawn(move || { barrier2.wait(); std::thread::sleep(std::time::Duration::from_millis(1000)); let ok = v8_isolate_handle.terminate_execution(); assert!(ok); }); barrier.wait(); let err = runtime .run_event_loop(Default::default()) .await .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("simple.js", "1 + 1") .expect("execution should be possible again"); terminator_thread.join().unwrap(); } #[tokio::test] async fn global_template_middleware() { use parking_lot::Mutex; use v8::MapFnTo; static CALLS: Mutex<Vec<String>> = Mutex::new(Vec::new()); pub fn descriptor<'s>( _scope: &mut v8::PinScope<'s, '_>, _key: v8::Local<'s, v8::Name>, _args: v8::PropertyCallbackArguments<'s>, _rv: v8::ReturnValue, ) -> v8::Intercepted { CALLS.lock().push("descriptor".to_string()); v8::Intercepted::kNo } pub fn setter<'s>( _scope: &mut v8::PinScope<'s, '_>, _key: v8::Local<'s, v8::Name>, _value: v8::Local<'s, v8::Value>, _args: v8::PropertyCallbackArguments<'s>, _rv: v8::ReturnValue<()>, ) -> v8::Intercepted { CALLS.lock().push("setter".to_string()); v8::Intercepted::kNo } fn definer<'s>( _scope: &mut v8::PinScope<'s, '_>, _key: v8::Local<'s, v8::Name>, _descriptor: &v8::PropertyDescriptor, _args: v8::PropertyCallbackArguments<'s>, _rv: v8::ReturnValue<()>, ) -> v8::Intercepted { CALLS.lock().push("definer".to_string()); v8::Intercepted::kNo } pub fn gt_middleware<'s>( _scope: &mut v8::PinScope<'s, '_, ()>, template: v8::Local<'s, v8::ObjectTemplate>, ) -> v8::Local<'s, v8::ObjectTemplate> { let mut config = v8::NamedPropertyHandlerConfiguration::new().flags( v8::PropertyHandlerFlags::NON_MASKING | v8::PropertyHandlerFlags::HAS_NO_SIDE_EFFECT, ); config = config.descriptor_raw(descriptor.map_fn_to()); config = config.setter_raw(setter.map_fn_to()); config = config.definer_raw(definer.map_fn_to()); template.set_named_property_handler(config); template } deno_core::extension!(test_ext, global_template_middleware = gt_middleware); let mut runtime = JsRuntime::new(RuntimeOptions { extensions: vec![test_ext::init()], ..Default::default() }); // Create sleep function that waits for 2 seconds. runtime .execute_script( "check_global_template_middleware.js", r#"Object.defineProperty(globalThis, 'key', { value: 9, enumerable: true, configurable: true, writable: true })"#, ) .unwrap(); let calls_set = CALLS .lock() .clone() .into_iter() .collect::<HashSet<String>>(); assert!(calls_set.contains("definer")); assert!(calls_set.contains("setter")); assert!(calls_set.contains("descriptor")); } #[test] fn eval_context_with_code_cache() { let code_cache = { let updated_code_cache = Arc::new(Mutex::new(HashMap::new())); let get_code_cache_cb = Box::new(|_: &Url, source: &v8::String| { Ok(SourceCodeCacheInfo { data: None, hash: hash_source(source), }) }); let updated_code_cache_clone = updated_code_cache.clone(); let set_code_cache_cb = Box::new(move |specifier: Url, _hash: u64, code_cache: &[u8]| { let mut c = updated_code_cache_clone.lock(); c.insert(specifier, code_cache.to_vec()); }); let mut runtime = JsRuntime::new(RuntimeOptions { eval_context_code_cache_cbs: Some((get_code_cache_cb, set_code_cache_cb)), ..Default::default() }); runtime .execute_script( "", ascii_str!("Deno.core.evalContext('const i = 10;', 'file:///foo.js');"), ) .unwrap(); let c = updated_code_cache.lock(); let mut keys = c.keys().map(|s| s.as_str()).collect::<Vec<_>>(); keys.sort(); assert_eq!(keys, vec!["file:///foo.js",]); c.clone() }; { // Create another runtime and try to use the code cache. let updated_code_cache = Arc::new(Mutex::new(HashMap::new())); let code_cache_clone = code_cache.clone(); let get_code_cache_cb = Box::new(move |specifier: &Url, source: &v8::String| { Ok(SourceCodeCacheInfo { data: code_cache_clone .get(specifier) .map(|code_cache| Cow::Owned(code_cache.clone())), hash: hash_source(source), }) }); let updated_code_cache_clone = updated_code_cache.clone(); let set_code_cache_cb = Box::new(move |specifier: Url, _hash: u64, code_cache: &[u8]| { let mut c = updated_code_cache_clone.lock(); c.insert(specifier, code_cache.to_vec()); }); let mut runtime = JsRuntime::new(RuntimeOptions { eval_context_code_cache_cbs: Some((get_code_cache_cb, set_code_cache_cb)), ..Default::default() }); runtime .execute_script( "", ascii_str!("Deno.core.evalContext('const i = 10;', 'file:///foo.js');"), ) .unwrap(); // Verify that code cache was not updated, which means that provided code cache was used. let c = updated_code_cache.lock(); assert!(c.is_empty()); } } fn hash_source(source: &v8::String) -> u64 { use std::hash::Hash; use std::hash::Hasher; let mut hasher = twox_hash::XxHash64::default(); source.hash(&mut hasher); hasher.finish() } /// Test that process.nextTick callbacks run before Promise.then callbacks, /// matching Node.js behavior. This requires V8's Explicit microtask policy /// so that microtasks are not auto-drained before the tick queue. #[tokio::test] async fn test_nexttick_before_promise_then() { #[op2] async fn op_async_sleep() -> Result<(), JsErrorBox> { 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()], ..Default::default() }); runtime .execute_script( "nexttick_before_then.js", r#" (async function() { const order = []; Deno.core.queueNextTick({ callback: () => order.push("tick"), args: undefined, snapshot: undefined, }); Promise.resolve().then(() => order.push("then")); await Deno.core.ops.op_async_sleep(); const result = order.join(","); if (result !== "tick,then") { throw new Error("expected 'tick,then' but got '" + result + "'"); } })(); "#, ) .unwrap(); runtime.run_event_loop(Default::default()).await.unwrap(); } #[tokio::test] async fn test_timer_return_protocol_keeps_timers_armed_from_promise_hook() { static FIRED: AtomicBool = AtomicBool::new(false); #[op2(fast)] fn op_mark_timer_race_fired() { FIRED.store(true, Ordering::SeqCst); } #[op2] async fn op_async_sleep(#[number] delay_ms: u64) -> Result<(), JsErrorBox> { tokio::time::sleep(Duration::from_millis(delay_ms)).await; Ok(()) } deno_core::extension!( timer_return_protocol_ext, ops = [op_mark_timer_race_fired, op_async_sleep] ); let cases = [ ( "stale_zero", r#" Deno.core.createTimer(() => {}, 20, undefined, false, true, false); "#, ), ( "stale_negative_unrefed_later", r#" Deno.core.createTimer(() => {}, 20, undefined, false, true, false); laterTimer = Deno.core.createTimer( () => {}, 1000, undefined, false, false, false, ); "#, ), ( "stale_positive_refed_later", r#" Deno.core.createTimer(() => {}, 20, undefined, false, true, false); laterTimer = Deno.core.createTimer( () => {}, 1000, undefined, false, true, false, ); "#, ), ]; for (name, timer_setup) in cases { FIRED.store(false, Ordering::SeqCst); let mut runtime = JsRuntime::new(RuntimeOptions { extensions: vec![timer_return_protocol_ext::init()], ..Default::default() }); runtime .execute_script( name, format!( r#" const {{ op_mark_timer_race_fired, op_async_sleep }} = Deno.core.ops; let armed = false; let laterTimer; Deno.core.setPromiseHooks(null, null, null, () => {{ if (armed) return; armed = true; Deno.core.createTimer(() => {{ if (laterTimer !== undefined) {{ Deno.core.cancelTimer(laterTimer); }} op_mark_timer_race_fired(); }}, 5, undefined, false, true, false); }}); {timer_setup} op_async_sleep(20); "#, ), ) .unwrap(); let run_result = tokio::time::timeout( Duration::from_millis(250), runtime.run_event_loop(Default::default()), ) .await; // The event loop must actually complete. Accepting a timeout (even with // FIRED set) would let a regression that arms the timer but leaves the // loop hung still pass. let result = run_result.unwrap_or_else(|_| { panic!("{name} timed out; event loop never completed") }); result.unwrap(); assert!( FIRED.load(Ordering::SeqCst), "{name} did not fire the timer armed by the promise hook" ); } } #[tokio::test] async fn test_timer_return_protocol_keeps_timers_armed_from_run_next_ticks() { static FIRED: AtomicBool = AtomicBool::new(false); #[op2(fast)] fn op_mark_run_next_ticks_timer_fired() { FIRED.store(true, Ordering::SeqCst); } deno_core::extension!( timer_run_next_ticks_ext, ops = [op_mark_run_next_ticks_timer_fired] ); FIRED.store(false, Ordering::SeqCst); let mut runtime = JsRuntime::new(RuntimeOptions { extensions: vec![timer_run_next_ticks_ext::init()], ..Default::default() }); runtime .execute_script( "timer_return_protocol_run_next_ticks.js", r#" const { op_mark_run_next_ticks_timer_fired } = Deno.core.ops; let armed = false; Deno.core.setPromiseHooks(null, () => { if (armed) return; armed = true; Deno.core.createTimer(() => { op_mark_run_next_ticks_timer_fired(); }, 5, undefined, false, true, false); }, null, null); // These timers share a timeout list. processTimers() drains ticks and // microtasks between callbacks, so the Promise callback below runs from // processTimers()'s runNextTicks() path before the second timer fires. Deno.core.createTimer(() => { Promise.resolve().then(() => {}); }, 1, undefined, false, true, false); Deno.core.createTimer(() => {}, 1, undefined, false, true, false); "#, ) .unwrap(); let run_result = tokio::time::timeout( Duration::from_millis(250), runtime.run_event_loop(Default::default()), ) .await; // The event loop must actually complete. Accepting a timeout (even with // FIRED set) would let a regression that arms the timer but leaves the // loop hung still pass. let result = run_result .unwrap_or_else(|_| panic!("timed out; event loop never completed")); result.unwrap(); assert!( FIRED.load(Ordering::SeqCst), "timer armed during runNextTicks did not fire" ); } /// Test that multiple nextTick callbacks all run before any Promise.then /// callbacks, and that promises queued during nextTick run after all ticks. #[tokio::test] async fn test_nexttick_queue_drains_before_microtasks() { #[op2] async fn op_async_sleep() -> Result<(), JsErrorBox> { 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()], ..Default::default() }); runtime .execute_script( "nexttick_drains_before_microtasks.js", r#" (async function() { const order = []; Deno.core.queueNextTick({ callback: () => { order.push("tick1"); // Promise queued during tick should run after all ticks Promise.resolve().then(() => order.push("then-from-tick1")); // Nested tick should run before that promise Deno.core.queueNextTick({ callback: () => order.push("tick2"), args: undefined, snapshot: undefined, }); }, args: undefined, snapshot: undefined, }); Promise.resolve().then(() => order.push("then1")); Promise.resolve().then(() => order.push("then2")); await Deno.core.ops.op_async_sleep(); const result = order.join(","); const expected = "tick1,tick2,then1,then2,then-from-tick1"; if (result !== expected) { throw new Error("expected '" + expected + "' but got '" + result + "'"); } })(); "#, ) .unwrap(); runtime.run_event_loop(Default::default()).await.unwrap(); } /// Test that nextTick queued inside an await continuation runs AFTER /// Promise.then from the same continuation. This matches Node.js: the /// await continuation is itself a microtask, so Promise.resolve().then() /// queued inside it runs in the same microtask checkpoint. The nextTick /// runs after the checkpoint completes. #[tokio::test] async fn test_nexttick_ordering_after_await() { #[op2] async fn op_async_sleep() -> Result<(), JsErrorBox> { 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()], ..Default::default() }); runtime .execute_script( "nexttick_after_await.js", r#" (async function() { const order = []; // First await to get into a "later turn" await Deno.core.ops.op_async_sleep(); // Inside an await continuation (which is a microtask), a .then // runs in the same microtask checkpoint, before nextTick. Deno.core.queueNextTick({ callback: () => order.push("tick-after-await"), args: undefined, snapshot: undefined, }); Promise.resolve().then(() => order.push("then-after-await")); // Wait for drain await Deno.core.ops.op_async_sleep(); const result = order.join(","); if (result !== "then-after-await,tick-after-await") { throw new Error("expected 'then-after-await,tick-after-await' but got '" + result + "'"); } })(); "#, ) .unwrap(); runtime.run_event_loop(Default::default()).await.unwrap(); } /// Test that queueMicrotask is ordered after nextTick but alongside /// Promise.then, matching Node.js behavior. #[tokio::test] async fn test_nexttick_before_queue_microtask() { #[op2] async fn op_async_sleep() -> Result<(), JsErrorBox> { 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()], ..Default::default() }); runtime .execute_script( "nexttick_before_queuemicrotask.js", r#" (async function() { const order = []; Deno.core.queueNextTick({ callback: () => order.push("tick"), args: undefined, snapshot: undefined, }); queueMicrotask(() => order.push("microtask")); Promise.resolve().then(() => order.push("then")); await Deno.core.ops.op_async_sleep(); const result = order.join(","); if (result !== "tick,microtask,then") { throw new Error("expected 'tick,microtask,then' but got '" + result + "'"); } })(); "#, ) .unwrap(); runtime.run_event_loop(Default::default()).await.unwrap(); } // Test that foreground tasks are delivered even when the isolate is // registered without a tokio handle. #[test] fn foreground_tasks_delivered_without_tokio_handle() { let tokio_runtime = tokio::runtime::Builder::new_current_thread() .enable_all() .build() .unwrap(); // Created outside `block_on` so there is no tokio runtime context and // the isolate is registered with `handle: None`. let mut runtime = JsRuntime::new(Default::default()); tokio_runtime.block_on(async { let promise = runtime .execute_script( "foreground_task_test.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, ]); await WebAssembly.compile(wasmCode); })() "#, ) .unwrap(); #[allow(deprecated, reason = "test code")] runtime.resolve_value(promise).await.unwrap(); }); }