use std::cell::Cell; use std::convert::Infallible; use std::rc::Rc; use avril_core::{Extension, Op, Runtime, RuntimeOptions, v8}; fn runtime() -> Runtime { Runtime::new(RuntimeOptions { extensions: vec![Extension { name: "ext:test", dependencies: &[], ops: vec![], modules: &[( "ext:test/main.js", "import {markPromiseHandled} from 'ext:core/promises'; globalThis.markPromiseHandled = markPromiseHandled;", )], entry_point: Some("ext:test/main.js"), }], ..Default::default() }) .unwrap() } #[test] fn marking_rejected_promises_clears_tracking_without_changing_their_results() { let mut runtime = runtime(); runtime .execute_script::<()>( "rejected", r#" globalThis.reason = {failure: true}; globalThis.promise = Promise.reject(reason); if (markPromiseHandled(promise) !== undefined) throw Error('Unexpected result'); markPromiseHandled(promise); "#, ) .unwrap(); runtime .execute_script::<()>( "observe", "globalThis.observed = false; promise.catch(error => observed = error === reason);", ) .unwrap(); assert!( runtime .execute_script::("result", "observed") .unwrap() ); runtime.run_event_loop().unwrap(); } #[test] fn marking_pending_promises_suppresses_later_rejections_without_property_access() { let mut runtime = runtime(); runtime .execute_script::<()>( "pending", r#" let reject; const promise = new Promise((resolve, failure) => reject = failure); const poison = () => {throw Error('Inspected promise property');}; Object.defineProperties(promise, { constructor: {get: poison}, then: {get: poison} }); markPromiseHandled(promise); Promise.resolve().then(() => reject('later failure')); "#, ) .unwrap(); runtime.run_event_loop().unwrap(); } #[test] fn marking_one_promise_does_not_suppress_unrelated_rejections() { let mut runtime = runtime(); let error = runtime .execute_script::<()>( "unrelated", "Promise.reject('unrelated failure'); markPromiseHandled(Promise.reject('suppressed failure'));", ) .unwrap_err() .to_string(); assert!(error.contains("unrelated failure"), "{error}"); runtime.run_event_loop().unwrap(); } #[test] fn marking_promises_does_not_allocate_derived_promises() { extern "C" fn created( event: v8::PromiseHookType, promise: v8::Local, _parent: v8::Local, ) { if event == v8::PromiseHookType::Init { // SAFETY: V8 supplies a valid promise belonging to the active isolate. v8::callback_scope!(unsafe scope, promise); let count = scope.get_slot::>>().unwrap(); count.set(count.get() + 1); } } let count = Rc::new(Cell::new(0_u32)); let counter = count.clone(); let mut runtime = Runtime::new(RuntimeOptions { extensions: vec![Extension { name: "ext:test", dependencies: &[], ops: vec![Op::with_scope("count_promises", move |scope, (): ()| { scope.set_slot(counter.clone()); scope.set_promise_hook(created); Ok::<_, Infallible>(()) })], modules: &[( "ext:test/main.js", "import {markPromiseHandled} from 'ext:core/promises'; import {count_promises} from 'ext:core/ops'; globalThis.markPromiseHandled = markPromiseHandled; globalThis.countPromises = count_promises;", )], entry_point: Some("ext:test/main.js"), }], ..Default::default() }) .unwrap(); runtime .execute_script::<()>( "allocations", r#" countPromises(null); const promises = [new Promise(() => {}), Promise.resolve(), Promise.reject('handled')]; for (const promise of promises) { markPromiseHandled(promise); markPromiseHandled(promise); } "#, ) .unwrap(); assert_eq!(count.get(), 3); } #[test] fn native_promise_handling_accepts_subclasses_but_rejects_impostors() { let mut runtime = runtime(); runtime .execute_script::<()>( "brands", r#" class Derived extends Promise {} markPromiseHandled(Derived.resolve()); markPromiseHandled(Derived.reject('subclass failure')); for (const value of [ undefined, null, 42, {}, {then() {}}, Object.create(Promise.prototype), new Proxy(Promise.resolve(), {}) ]) { try {markPromiseHandled(value); throw Error('Accepted impostor');} catch (error) {if (!(error instanceof TypeError)) throw error;} } try {new markPromiseHandled(Promise.resolve()); throw Error('Constructible helper');} catch (error) {if (!(error instanceof TypeError)) throw error;} "#, ) .unwrap(); }