use std::cell::Cell; use std::io; use std::rc::Rc; use avril_core::{Extension, Op, Runtime, RuntimeOptions, v8}; #[test] fn raw_ops_receive_positional_values_and_preserve_javascript_exceptions() { let mut runtime = Runtime::new(RuntimeOptions { extensions: vec![Extension { name: "ext:test", dependencies: &[], ops: vec![ Op::raw("invoke", |scope, args| { let callback = v8::Local::::try_from(args.get(0)).ok()?; let values: Vec<_> = (2..args.length()).map(|index| args.get(index)).collect(); callback.call(scope, args.get(1), &values) }), Op::raw("count", |scope, args| { Some(v8::Integer::new(scope, args.length()).into()) }), Op::raw("empty", |_, _| None), ], modules: &[( "ext:test/main.js", "import {invoke,count,empty} from 'ext:core/ops'; Object.assign(globalThis,{invoke,count,empty});", )], entry_point: Some("ext:test/main.js"), }], ..Default::default() }) .unwrap(); runtime .execute_script::<()>( "arguments.js", r#" const marker = {toJSON() {throw Error('Serialized arguments');}}; const symbol = Symbol('opaque'); const array = [1, 2]; if (count() !== 0 || count(undefined) !== 1 || count(marker, array, symbol) !== 3) { throw Error('Argument count'); } const result = invoke(function(first, second, third, fourth) { if (this !== marker || first !== marker || second !== array || third !== symbol || fourth !== undefined || arguments.length !== 4) throw Error('Argument values'); return marker; }, marker, marker, array, symbol, undefined); if (result !== marker || invoke(() => 42) !== 42 || empty() !== undefined) throw Error('Return value'); if (invoke((first, second) => first === undefined && second === undefined, null) !== true) { throw Error('Missing arguments'); } try {invoke(() => {throw marker;}, null); throw Error('Lost exception');} catch (error) {if (error !== marker) throw error;} if (invoke(() => 7, null) !== 7) throw Error('Poisoned bridge'); "#, ) .unwrap(); } #[test] fn captured_op_state_survives_gc_reentrancy_and_unrelated_isolate_slots() { struct Probe(Rc>); impl Drop for Probe { fn drop(&mut self) { self.0.set(self.0.get() + 1); } } let drops = Rc::new(Cell::new(0)); let mut ops: Vec<_> = [("one", 10), ("two", 20)] .into_iter() .map(|(name, offset)| { let probe = Probe(drops.clone()); Op::raw(name, move |scope, args| { let value = args.get(0).uint32_value(scope)?; assert_eq!(probe.0.get(), 0); Some(v8::Integer::new_from_unsigned(scope, value + offset).into()) }) }) .collect(); ops.push(Op::raw("collect", |scope, _| { scope.set_slot(Rc::<[Op]>::from([])); scope.low_memory_notification(); None })); let mut runtime = Runtime::new(RuntimeOptions { extensions: vec![Extension { name: "ext:test", dependencies: &[], ops, modules: &[( "ext:test/main.js", "import {one,two,collect} from 'ext:core/ops'; Object.assign(globalThis,{one,two,collect});", )], entry_point: Some("ext:test/main.js"), }], ..Default::default() }) .unwrap(); runtime .execute_script::<()>( "captures.js", r#" if (one({valueOf() {collect(); return two(1);}}) !== 31) throw Error('Reentrant state'); for (let i = 0; i < 1000; i++) { if (one(i) !== i + 10 || two(i) !== i + 20) throw Error('Shared callback state'); } const marker = {}; try {one({valueOf() {collect(); throw marker;}}); throw Error('Lost exception');} catch (error) {if (error !== marker) throw error;} if (two(2) !== 22) throw Error('Poisoned state'); "#, ) .unwrap(); assert_eq!(drops.get(), 0); drop(runtime); assert_eq!(drops.get(), 2); } #[test] fn zero_sized_captures_are_dropped_only_with_the_runtime() { thread_local! { static DROPS: Cell = const { Cell::new(0) }; } #[repr(align(64))] struct Probe; impl Drop for Probe { fn drop(&mut self) { DROPS.with(|drops| drops.set(drops.get() + 1)); } } DROPS.with(|drops| drops.set(0)); let probe = Probe; let op = Op::raw("identity", move |_, args| { let address = std::hint::black_box(&probe) as *const Probe as usize; assert_eq!(address % std::mem::align_of::(), 0); Some(args.get(0)) }); let mut runtime = Runtime::new(RuntimeOptions { extensions: vec![Extension { name: "ext:test", dependencies: &[], ops: vec![op], modules: &[( "ext:test/main.js", "import {identity} from 'ext:core/ops'; globalThis.identity = identity;", )], entry_point: Some("ext:test/main.js"), }], ..Default::default() }) .unwrap(); assert_eq!( runtime .execute_script::("zero.js", "identity(42)") .unwrap(), 42 ); DROPS.with(|drops| assert_eq!(drops.get(), 0)); drop(runtime); DROPS.with(|drops| assert_eq!(drops.get(), 1)); } #[test] fn fallible_raw_ops_return_opaque_values_and_throw_host_errors() { let mut runtime = Runtime::new(RuntimeOptions { extensions: vec![Extension { name: "ext:test", dependencies: &[], ops: vec![Op::try_raw("select", |_, args| { if args.get(0).is_true() { Ok(args.get(1)) } else { Err(io::Error::other("Selection failed")) } })], modules: &[( "ext:test/main.js", "import {select} from 'ext:core/ops'; globalThis.select = select;", )], entry_point: Some("ext:test/main.js"), }], ..Default::default() }) .unwrap(); runtime .execute_script::<()>( "fallible.js", r#" const value = {toJSON() {throw Error('Serialized result');}}; if (select(true, value) !== value) throw Error('Lost identity'); try {select(false, value); throw Error('Expected failure');} catch (error) { if (!(error instanceof Error) || error.message !== 'Selection failed') throw error; } if (select(true, value) !== value) throw Error('Poisoned op'); "#, ) .unwrap(); } #[test] fn typed_op_adapters_still_decode_only_the_first_argument() { let mut runtime = Runtime::new(RuntimeOptions { extensions: vec![Extension { name: "ext:test", dependencies: &[], ops: vec![ Op::new("sum", |(left, right): (u32, u32)| { Ok::<_, io::Error>(left + right) }), Op::with_scope("scoped", |_, (left, right): (u32, u32)| { Ok::<_, io::Error>(left + right) }), Op::asynchronous("asyncSum", |(left, right): (u32, u32)| async move { Ok::<_, io::Error>(left + right) }), Op::asynchronous_unreferenced("unreferenced", |(left, right): (u32, u32)| async move { Ok::<_, io::Error>(left + right) }), ], modules: &[( "ext:test/main.js", "import {sum,scoped,asyncSum,unreferenced} from 'ext:core/ops'; Object.assign(globalThis,{sum,scoped,asyncSum,unreferenced});", )], entry_point: Some("ext:test/main.js"), }], ..Default::default() }) .unwrap(); runtime .execute_script::<()>( "typed.js", r#" const extra = Symbol('ignored'); globalThis.results = [sum([20, 22], extra), scoped([20, 22], extra)]; Promise.all([asyncSum([20, 22], extra), unreferenced([20, 22], extra)]) .then(values => results.push(...values)); "#, ) .unwrap(); runtime.run_event_loop().unwrap(); assert_eq!( runtime .execute_script::>("results.js", "results") .unwrap(), [42, 42, 42, 42] ); }