mod common; use common::run; #[test] fn timers_order_tasks_microtasks_arguments_and_receivers() { let output = run( r#" const order = []; queueMicrotask(() => order.push('microtask')); Promise.resolve().then(() => order.push('promise')); const first = setTimeout(function(a, b) { 'use strict'; if (this !== globalThis || a !== 7 || b.value !== 8) throw Error('Timer arguments or receiver'); order.push('first'); queueMicrotask(() => order.push('first microtask')); Promise.resolve().then(() => order.push('first promise')); setTimeout(() => order.push('nested')); return {get then() {throw Error('Adopted timer return value');}}; }, 0, 7, {value:8}); const second = setTimeout(() => order.push('second')); if (!Number.isInteger(first) || first <= 0 || second === first) throw Error('Timer IDs'); await new Promise(resolve => setTimeout(resolve, 30)); console.log(order.join('|')); let adopted = false; queueMicrotask(function() { 'use strict'; if (this !== undefined) throw Error('Microtask receiver'); return {then() {adopted = true;}}; }); await Promise.resolve(); if (adopted) throw Error('Adopted microtask return value'); "#, &[], ); assert_eq!( output.stdout, b"microtask|promise|first|first microtask|first promise|second|nested\n" ); } #[test] fn microtasks_share_fifo_order_with_promises_and_ignore_callback_results() { let output = run( r#" const order = []; const result = queueMicrotask(function() { 'use strict'; if (this !== undefined || arguments.length !== 0) throw Error('Microtask invocation'); order.push('first'); queueMicrotask(() => order.push('nested microtask')); Promise.resolve().then(() => order.push('nested promise')); return {get then() {throw Error('Inspected microtask result');}}; }); if (result !== undefined || order.length) throw Error('Synchronous microtask'); Promise.resolve().then(() => { order.push('promise'); queueMicrotask(() => order.push('promise microtask')); }); queueMicrotask(() => order.push('last')); await new Promise(resolve => setTimeout(resolve)); console.log(order.join('|')); "#, &[], ); assert_eq!( output.stdout, b"first|promise|last|nested microtask|nested promise|promise microtask\n" ); } #[test] fn microtasks_accept_bound_and_proxy_callbacks_without_consulting_call_properties() { let output = run( r#" const receiver = {}; const callback = function(value) { if (this !== receiver || value !== 42) throw Error('Bound callback'); console.log('bound'); }.bind(receiver, 42); callback.call = callback.apply = () => {throw Error('Callback property');}; queueMicrotask(callback); queueMicrotask(new Proxy(() => {}, { apply(target, receiver, args) { if (receiver !== undefined || args.length !== 0) throw Error('Proxy callback'); console.log('proxy'); } })); "#, &[], ); assert_eq!(output.stdout, b"bound\nproxy\n"); } #[test] fn cancellation_cross_clearing_and_intervals_release_host_work() { let output = run( r#" const fail = () => {throw Error('Canceled timer fired');}; const a = setTimeout(fail, 1_000_000); clearInterval(a); clearTimeout(a); const b = setInterval(fail, 1_000_000); clearTimeout(b); clearInterval(b); let calls = 0; await new Promise(resolve => { const id = setInterval(() => { if (++calls === 3) {clearTimeout(id);resolve();} }, 1); }); await new Promise(resolve => { const id = setInterval(() => { queueMicrotask(() => {clearInterval(id);resolve();}); }, 0); }); const long = setTimeout(fail, 1_000_000); await new Promise(resolve => setTimeout(() => {clearTimeout(long);resolve();}, 0)); console.log(calls); "#, &[], ); assert_eq!(output.stdout, b"3\n"); } #[test] fn timer_delay_and_handle_conversions_follow_webidl() { let output = run( r#" const conversions = []; const handler = {toString() {conversions.push('handler');return 'code';}}; const delay = {valueOf() {conversions.push('delay');return 1;}}; try {setTimeout(handler, delay);throw Error('String callback accepted');} catch(e) {if (!e.message.includes('not yet implemented')) throw e;} if (conversions.join() !== 'handler,delay') throw Error('Conversion order'); for (const fn of [() => setTimeout(), () => setInterval(), () => queueMicrotask(), () => queueMicrotask('code'), () => setTimeout(()=>{},1n), () => clearTimeout(1n), () => setTimeout('code',Symbol())]) { try {fn();throw Error('Invalid input accepted');} catch(e) {if (!(e instanceof TypeError)) throw e;} } clearTimeout(); clearInterval(); clearTimeout(-1); clearTimeout(NaN); for (const fn of [setTimeout,setInterval,clearTimeout,clearInterval,queueMicrotask]) { try {new fn(()=>{});throw Error('Constructible timer API');} catch(e) {if (!(e instanceof TypeError)) throw e;} } const order = []; const late = setTimeout(() => order.push('late'), 20); for (const value of [-1,NaN,Infinity,2147483648,4294967296]) setTimeout(() => order.push('zero'), value); await new Promise(resolve => setTimeout(resolve,40)); if (order.join() !== 'zero,zero,zero,zero,zero,late') throw Error(order.join()); const canceled = setTimeout(() => {throw Error('Converted handle');}, 1_000_000); clearTimeout({valueOf() {return canceled + 4294967296;}}); console.log('converted'); "#, &[], ); assert_eq!(output.stdout, b"converted\n"); } #[test] fn nested_timers_clamp_but_microtasks_do_not_inherit_nesting() { let output = run( r#" async function probe(microtask) { return await new Promise(resolve => { let count = 0; function tick() { if (++count < 8) {setTimeout(tick);return;} const create = () => { const order = []; const done = label => {order.push(label);if (order.length === 2) resolve(order.join());}; setTimeout(() => done('four'),4); setTimeout(() => done('zero'),0); }; if (microtask) queueMicrotask(() => Promise.resolve().then(create)); else create(); } setTimeout(tick); }); } console.log(await probe(false),await probe(true)); const order = []; setTimeout(() => order.push('four'),4); await new Promise(resolve => setTimeout(() => {order.push('zero');resolve();})); if (order.join() !== 'zero') throw Error('Nesting escaped its task'); "#, &[], ); assert_eq!(output.stdout, b"four,zero zero,four\n"); } #[test] fn timer_internals_ignore_poisoned_globals_and_promise_species() { let output = run( r#" const resolved = Promise.resolve(); let species = 0; const poison = () => {throw Error('Poisoned intrinsic');}; Object.defineProperty(Promise, Symbol.species, {get() {species++;throw Error('Species');}, configurable:true}); Promise.prototype.then = poison; globalThis.Promise = poison; globalThis.Map = poison; Array.prototype.splice = poison; Array.prototype.sort = poison; Function.prototype.apply = poison; queueMicrotask(() => console.log('microtask')); const id = setTimeout(() => {throw Error('Canceled');}, 1_000_000); clearTimeout(id); const original = globalThis; setTimeout(function() { 'use strict'; if (species || this !== original) throw Error('Poisoned timer'); console.log('timer'); }); globalThis.globalThis = null; await resolved; "#, &[], ); assert_eq!(output.stdout, b"microtask\ntimer\n"); } #[test] fn abort_signal_timeout_uses_native_timer_and_enforce_range_conversion() { let output = run( r#" const fail = () => {throw Error('Expected TypeError');}; for (const call of [ () => AbortSignal.timeout(), () => AbortSignal.timeout(-1), () => AbortSignal.timeout(Infinity), () => AbortSignal.timeout(NaN), () => AbortSignal.timeout(2 ** 64), () => AbortSignal.timeout(1n), () => AbortSignal.timeout(Symbol()), ]) { try {call();fail();} catch (error) {if (!(error instanceof TypeError)) throw error;} } const previous = globalThis.setTimeout; globalThis.setTimeout = fail; Math.trunc = fail; const signal = AbortSignal.timeout({ [Symbol.toPrimitive](hint) { if (hint !== 'number') throw Error('Wrong conversion hint'); return 0.9; }, }); if (signal.aborted) throw Error('Timeout was synchronous'); for(let id=1;id<=10;id++)clearTimeout(id); await new Promise(resolve => signal.addEventListener('abort', resolve, {once:true})); if (!signal.aborted || signal.reason.name !== 'TimeoutError' || signal.reason.message !== 'The operation timed out' || signal.reason.code !== 23) { throw Error('Invalid timeout reason'); } try {signal.throwIfAborted();throw Error('Expected timeout reason');} catch (error) {if (error !== signal.reason) throw error;} globalThis.setTimeout = previous; console.log('timeout'); "#, &[], ); assert_eq!(output.stdout, b"timeout\n"); } #[test] fn long_abort_timeout_does_not_keep_the_cli_alive() { let output = run("AbortSignal.timeout(2 ** 31); console.log('exit');", &[]); assert_eq!(output.stdout, b"exit\n"); } #[test] fn private_timer_liveness_tracks_public_queue_changes() { let promoted = run( r#" const signal = AbortSignal.timeout(1); await new Promise(resolve => setTimeout(resolve, 30)); if (!signal.aborted || signal.reason.name !== 'TimeoutError') throw Error('Private timer did not fire while referenced'); console.log('promoted'); "#, &[], ); assert_eq!(promoted.stdout, b"promoted\n"); let demoted = run( r#" AbortSignal.timeout(2 ** 31); const id = setTimeout(() => {throw Error('Cleared public timer fired');}, 2 ** 31); clearTimeout(id); console.log('demoted'); "#, &[], ); assert_eq!(demoted.stdout, b"demoted\n"); let removed = run( r#" AbortSignal.timeout(2 ** 31); await new Promise(resolve => setTimeout(resolve, 0)); console.log('removed'); "#, &[], ); assert_eq!(removed.stdout, b"removed\n"); } #[test] fn canceled_private_waits_do_not_disarm_rescheduled_public_timers() { let output = run( r#" const order = []; AbortSignal.timeout(500); await new Promise(resolve => setTimeout(() => { order.push('first'); setTimeout(() => {order.push('second');resolve();}, 0); }, 0)); if (order.join() !== 'first,second') throw Error(order.join()); console.log(order.join()); "#, &[], ); assert_eq!(output.stdout, b"first,second\n"); } #[test] fn private_timer_tasks_do_not_increase_public_timer_nesting() { let output = run( r#" const order = await new Promise(resolve => { const result = []; function finish(label) { result.push(label); if (result.length === 2) resolve(result.join(',')); } function deepen(level) { if (level < 7) {setTimeout(() => deepen(level + 1), 0);return;} setTimeout(() => finish('earlier'), 0); const signal = AbortSignal.timeout(0); signal.addEventListener('abort', () => setTimeout(() => finish('timeout'), 0), {once:true}); } setTimeout(() => deepen(1), 0); }); if (order !== 'timeout,earlier') throw Error(order); console.log(order); "#, &[], ); assert_eq!(output.stdout, b"timeout,earlier\n"); } #[test] fn callbacks_report_the_original_exception() { for source in [ "setTimeout(() => {throw Error('timer failure');});", "queueMicrotask(() => {throw Error('microtask failure');});", ] { let (_dir, child) = common::spawn(source); let output = common::wait(child); assert!(!output.status.success()); assert!(String::from_utf8_lossy(&output.stderr).contains( if source.starts_with("setTimeout") { "Error: timer failure\n" } else { "Error: microtask failure\n" } )); } } #[test] fn clearing_one_timeout_does_not_disarm_other_timers() { let output = run( r#" let canceled = false; const canceledTimer = setTimeout(() => canceled = true, 5); clearTimeout(canceledTimer); await new Promise(resolve => setTimeout(() => {console.log('awake');resolve();},20)); if (canceled) throw Error('Cleared timeout fired'); "#, &[], ); assert_eq!(output.stdout, b"awake\n"); } #[test] fn timers_are_permissionless_and_private_modules_remain_inaccessible() { let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("main.js"); std::fs::write( &path, r#" import {pledge} from 'avril:std'; pledge({net:false,read:false,jsExec:false}); await new Promise(resolve => setTimeout(resolve,0)); for (const name of ['ext:timers/main.js','ext:timers/timers.js','ext:core/tasks','ext:core/microtasks']) { try {await import(name);throw Error('Private module escaped');} catch(e) {if (!e.message.includes('private')) throw e;} } console.log('permissionless'); "#, ) .unwrap(); let output = std::process::Command::new(env!("CARGO_BIN_EXE_avril")) .arg("run") .args(["-0", path.to_str().unwrap()]) .output() .unwrap(); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); assert_eq!(output.stdout, b"permissionless\n"); } #[test] fn intervals_restart_after_callback_microtasks() { let output = run( r#" const order = []; let count = 0; await new Promise(resolve => { const id = setInterval(() => { order.push(`I${count}`); if (count++ === 0) queueMicrotask(() => setTimeout(() => order.push('T'))); else {clearInterval(id);resolve();} }); }); console.log(order.join()); "#, &[], ); assert_eq!(output.stdout, b"I0,T,I1\n"); }