char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somsnapshot: embed shared main and worker startup in the CLI688e96e

main
15.3 KiB428 linesraw
1mod common;
2
3use common::run;
4
5#[test]
6fn timers_order_tasks_microtasks_arguments_and_receivers() {
7    let output = run(
8        r#"
9        const order = [];
10        queueMicrotask(() => order.push('microtask'));
11        Promise.resolve().then(() => order.push('promise'));
12        const first = setTimeout(function(a, b) {
13            'use strict';
14            if (this !== globalThis || a !== 7 || b.value !== 8) throw Error('Timer arguments or receiver');
15            order.push('first');
16            queueMicrotask(() => order.push('first microtask'));
17            Promise.resolve().then(() => order.push('first promise'));
18            setTimeout(() => order.push('nested'));
19            return {get then() {throw Error('Adopted timer return value');}};
20        }, 0, 7, {value:8});
21        const second = setTimeout(() => order.push('second'));
22        if (!Number.isInteger(first) || first <= 0 || second === first) throw Error('Timer IDs');
23        await new Promise(resolve => setTimeout(resolve, 30));
24        console.log(order.join('|'));
25        let adopted = false;
26        queueMicrotask(function() {
27            'use strict';
28            if (this !== undefined) throw Error('Microtask receiver');
29            return {then() {adopted = true;}};
30        });
31        await Promise.resolve();
32        if (adopted) throw Error('Adopted microtask return value');
33    "#,
34        &[],
35    );
36    assert_eq!(
37        output.stdout,
38        b"microtask|promise|first|first microtask|first promise|second|nested\n"
39    );
40}
41
42#[test]
43fn microtasks_share_fifo_order_with_promises_and_ignore_callback_results() {
44    let output = run(
45        r#"
46        const order = [];
47        const result = queueMicrotask(function() {
48            'use strict';
49            if (this !== undefined || arguments.length !== 0) throw Error('Microtask invocation');
50            order.push('first');
51            queueMicrotask(() => order.push('nested microtask'));
52            Promise.resolve().then(() => order.push('nested promise'));
53            return {get then() {throw Error('Inspected microtask result');}};
54        });
55        if (result !== undefined || order.length) throw Error('Synchronous microtask');
56        Promise.resolve().then(() => {
57            order.push('promise');
58            queueMicrotask(() => order.push('promise microtask'));
59        });
60        queueMicrotask(() => order.push('last'));
61        await new Promise(resolve => setTimeout(resolve));
62        console.log(order.join('|'));
63        "#,
64        &[],
65    );
66    assert_eq!(
67        output.stdout,
68        b"first|promise|last|nested microtask|nested promise|promise microtask\n"
69    );
70}
71
72#[test]
73fn microtasks_accept_bound_and_proxy_callbacks_without_consulting_call_properties() {
74    let output = run(
75        r#"
76        const receiver = {};
77        const callback = function(value) {
78            if (this !== receiver || value !== 42) throw Error('Bound callback');
79            console.log('bound');
80        }.bind(receiver, 42);
81        callback.call = callback.apply = () => {throw Error('Callback property');};
82        queueMicrotask(callback);
83        queueMicrotask(new Proxy(() => {}, {
84            apply(target, receiver, args) {
85                if (receiver !== undefined || args.length !== 0) throw Error('Proxy callback');
86                console.log('proxy');
87            }
88        }));
89        "#,
90        &[],
91    );
92    assert_eq!(output.stdout, b"bound\nproxy\n");
93}
94
95#[test]
96fn cancellation_cross_clearing_and_intervals_release_host_work() {
97    let output = run(
98        r#"
99        const fail = () => {throw Error('Canceled timer fired');};
100        const a = setTimeout(fail, 1_000_000);
101        clearInterval(a); clearTimeout(a);
102        const b = setInterval(fail, 1_000_000);
103        clearTimeout(b); clearInterval(b);
104        let calls = 0;
105        await new Promise(resolve => {
106            const id = setInterval(() => {
107                if (++calls === 3) {clearTimeout(id);resolve();}
108            }, 1);
109        });
110        await new Promise(resolve => {
111            const id = setInterval(() => {
112                queueMicrotask(() => {clearInterval(id);resolve();});
113            }, 0);
114        });
115        const long = setTimeout(fail, 1_000_000);
116        await new Promise(resolve => setTimeout(() => {clearTimeout(long);resolve();}, 0));
117        console.log(calls);
118    "#,
119        &[],
120    );
121    assert_eq!(output.stdout, b"3\n");
122}
123
124#[test]
125fn timer_delay_and_handle_conversions_follow_webidl() {
126    let output = run(
127        r#"
128        const conversions = [];
129        const handler = {toString() {conversions.push('handler');return 'code';}};
130        const delay = {valueOf() {conversions.push('delay');return 1;}};
131        try {setTimeout(handler, delay);throw Error('String callback accepted');}
132        catch(e) {if (!e.message.includes('not yet implemented')) throw e;}
133        if (conversions.join() !== 'handler,delay') throw Error('Conversion order');
134        for (const fn of [() => setTimeout(), () => setInterval(), () => queueMicrotask(), () => queueMicrotask('code'), () => setTimeout(()=>{},1n), () => clearTimeout(1n), () => setTimeout('code',Symbol())]) {
135            try {fn();throw Error('Invalid input accepted');} catch(e) {if (!(e instanceof TypeError)) throw e;}
136        }
137        clearTimeout(); clearInterval(); clearTimeout(-1); clearTimeout(NaN);
138        for (const fn of [setTimeout,setInterval,clearTimeout,clearInterval,queueMicrotask]) {
139            try {new fn(()=>{});throw Error('Constructible timer API');} catch(e) {if (!(e instanceof TypeError)) throw e;}
140        }
141        const order = [];
142        const late = setTimeout(() => order.push('late'), 20);
143        for (const value of [-1,NaN,Infinity,2147483648,4294967296]) setTimeout(() => order.push('zero'), value);
144        await new Promise(resolve => setTimeout(resolve,40));
145        if (order.join() !== 'zero,zero,zero,zero,zero,late') throw Error(order.join());
146        const canceled = setTimeout(() => {throw Error('Converted handle');}, 1_000_000);
147        clearTimeout({valueOf() {return canceled + 4294967296;}});
148        console.log('converted');
149    "#,
150        &[],
151    );
152    assert_eq!(output.stdout, b"converted\n");
153}
154
155#[test]
156fn nested_timers_clamp_but_microtasks_do_not_inherit_nesting() {
157    let output = run(
158        r#"
159        async function probe(microtask) {
160            return await new Promise(resolve => {
161                let count = 0;
162                function tick() {
163                    if (++count < 8) {setTimeout(tick);return;}
164                    const create = () => {
165                        const order = [];
166                        const done = label => {order.push(label);if (order.length === 2) resolve(order.join());};
167                        setTimeout(() => done('four'),4);
168                        setTimeout(() => done('zero'),0);
169                    };
170                    if (microtask) queueMicrotask(() => Promise.resolve().then(create));
171                    else create();
172                }
173                setTimeout(tick);
174            });
175        }
176        console.log(await probe(false),await probe(true));
177        const order = [];
178        setTimeout(() => order.push('four'),4);
179        await new Promise(resolve => setTimeout(() => {order.push('zero');resolve();}));
180        if (order.join() !== 'zero') throw Error('Nesting escaped its task');
181    "#,
182        &[],
183    );
184    assert_eq!(output.stdout, b"four,zero zero,four\n");
185}
186
187#[test]
188fn timer_internals_ignore_poisoned_globals_and_promise_species() {
189    let output = run(
190        r#"
191        const resolved = Promise.resolve();
192        let species = 0;
193        const poison = () => {throw Error('Poisoned intrinsic');};
194        Object.defineProperty(Promise, Symbol.species, {get() {species++;throw Error('Species');}, configurable:true});
195        Promise.prototype.then = poison;
196        globalThis.Promise = poison;
197        globalThis.Map = poison;
198        Array.prototype.splice = poison;
199        Array.prototype.sort = poison;
200        Function.prototype.apply = poison;
201        queueMicrotask(() => console.log('microtask'));
202        const id = setTimeout(() => {throw Error('Canceled');}, 1_000_000);
203        clearTimeout(id);
204        const original = globalThis;
205        setTimeout(function() {
206            'use strict';
207            if (species || this !== original) throw Error('Poisoned timer');
208            console.log('timer');
209        });
210        globalThis.globalThis = null;
211        await resolved;
212    "#,
213        &[],
214    );
215    assert_eq!(output.stdout, b"microtask\ntimer\n");
216}
217
218#[test]
219fn abort_signal_timeout_uses_native_timer_and_enforce_range_conversion() {
220    let output = run(
221        r#"
222        const fail = () => {throw Error('Expected TypeError');};
223        for (const call of [
224            () => AbortSignal.timeout(),
225            () => AbortSignal.timeout(-1),
226            () => AbortSignal.timeout(Infinity),
227            () => AbortSignal.timeout(NaN),
228            () => AbortSignal.timeout(2 ** 64),
229            () => AbortSignal.timeout(1n),
230            () => AbortSignal.timeout(Symbol()),
231        ]) {
232            try {call();fail();} catch (error) {if (!(error instanceof TypeError)) throw error;}
233        }
234        const previous = globalThis.setTimeout;
235        globalThis.setTimeout = fail;
236        Math.trunc = fail;
237        const signal = AbortSignal.timeout({
238            [Symbol.toPrimitive](hint) {
239                if (hint !== 'number') throw Error('Wrong conversion hint');
240                return 0.9;
241            },
242        });
243        if (signal.aborted) throw Error('Timeout was synchronous');
244        for(let id=1;id<=10;id++)clearTimeout(id);
245        await new Promise(resolve => signal.addEventListener('abort', resolve, {once:true}));
246        if (!signal.aborted || signal.reason.name !== 'TimeoutError' ||
247            signal.reason.message !== 'The operation timed out' || signal.reason.code !== 23) {
248            throw Error('Invalid timeout reason');
249        }
250        try {signal.throwIfAborted();throw Error('Expected timeout reason');}
251        catch (error) {if (error !== signal.reason) throw error;}
252        globalThis.setTimeout = previous;
253        console.log('timeout');
254    "#,
255        &[],
256    );
257    assert_eq!(output.stdout, b"timeout\n");
258}
259
260#[test]
261fn long_abort_timeout_does_not_keep_the_cli_alive() {
262    let output = run("AbortSignal.timeout(2 ** 31); console.log('exit');", &[]);
263    assert_eq!(output.stdout, b"exit\n");
264}
265
266#[test]
267fn private_timer_liveness_tracks_public_queue_changes() {
268    let promoted = run(
269        r#"
270        const signal = AbortSignal.timeout(1);
271        await new Promise(resolve => setTimeout(resolve, 30));
272        if (!signal.aborted || signal.reason.name !== 'TimeoutError') throw Error('Private timer did not fire while referenced');
273        console.log('promoted');
274    "#,
275        &[],
276    );
277    assert_eq!(promoted.stdout, b"promoted\n");
278
279    let demoted = run(
280        r#"
281        AbortSignal.timeout(2 ** 31);
282        const id = setTimeout(() => {throw Error('Cleared public timer fired');}, 2 ** 31);
283        clearTimeout(id);
284        console.log('demoted');
285    "#,
286        &[],
287    );
288    assert_eq!(demoted.stdout, b"demoted\n");
289
290    let removed = run(
291        r#"
292        AbortSignal.timeout(2 ** 31);
293        await new Promise(resolve => setTimeout(resolve, 0));
294        console.log('removed');
295    "#,
296        &[],
297    );
298    assert_eq!(removed.stdout, b"removed\n");
299}
300
301#[test]
302fn canceled_private_waits_do_not_disarm_rescheduled_public_timers() {
303    let output = run(
304        r#"
305        const order = [];
306        AbortSignal.timeout(500);
307        await new Promise(resolve => setTimeout(() => {
308            order.push('first');
309            setTimeout(() => {order.push('second');resolve();}, 0);
310        }, 0));
311        if (order.join() !== 'first,second') throw Error(order.join());
312        console.log(order.join());
313    "#,
314        &[],
315    );
316    assert_eq!(output.stdout, b"first,second\n");
317}
318
319#[test]
320fn private_timer_tasks_do_not_increase_public_timer_nesting() {
321    let output = run(
322        r#"
323        const order = await new Promise(resolve => {
324            const result = [];
325            function finish(label) {
326                result.push(label);
327                if (result.length === 2) resolve(result.join(','));
328            }
329            function deepen(level) {
330                if (level < 7) {setTimeout(() => deepen(level + 1), 0);return;}
331                setTimeout(() => finish('earlier'), 0);
332                const signal = AbortSignal.timeout(0);
333                signal.addEventListener('abort', () => setTimeout(() => finish('timeout'), 0), {once:true});
334            }
335            setTimeout(() => deepen(1), 0);
336        });
337        if (order !== 'timeout,earlier') throw Error(order);
338        console.log(order);
339    "#,
340        &[],
341    );
342    assert_eq!(output.stdout, b"timeout,earlier\n");
343}
344
345#[test]
346fn callbacks_report_the_original_exception() {
347    for source in [
348        "setTimeout(() => {throw Error('timer failure');});",
349        "queueMicrotask(() => {throw Error('microtask failure');});",
350    ] {
351        let (_dir, child) = common::spawn(source);
352        let output = common::wait(child);
353        assert!(!output.status.success());
354        assert!(String::from_utf8_lossy(&output.stderr).contains(
355            if source.starts_with("setTimeout") {
356                "Error: timer failure\n"
357            } else {
358                "Error: microtask failure\n"
359            }
360        ));
361    }
362}
363
364#[test]
365fn clearing_one_timeout_does_not_disarm_other_timers() {
366    let output = run(
367        r#"
368        let canceled = false;
369        const canceledTimer = setTimeout(() => canceled = true, 5);
370        clearTimeout(canceledTimer);
371        await new Promise(resolve => setTimeout(() => {console.log('awake');resolve();},20));
372        if (canceled) throw Error('Cleared timeout fired');
373    "#,
374        &[],
375    );
376    assert_eq!(output.stdout, b"awake\n");
377}
378
379#[test]
380fn timers_are_permissionless_and_private_modules_remain_inaccessible() {
381    let dir = tempfile::tempdir().unwrap();
382    let path = dir.path().join("main.js");
383    std::fs::write(
384        &path,
385        r#"
386        import {pledge} from 'avril:std';
387        pledge({net:false,read:false,jsExec:false});
388        await new Promise(resolve => setTimeout(resolve,0));
389        for (const name of ['ext:timers/main.js','ext:timers/timers.js','ext:core/tasks','ext:core/microtasks']) {
390            try {await import(name);throw Error('Private module escaped');}
391            catch(e) {if (!e.message.includes('private')) throw e;}
392        }
393        console.log('permissionless');
394    "#,
395    )
396    .unwrap();
397    let output = std::process::Command::new(env!("CARGO_BIN_EXE_avril"))
398        .arg("run")
399        .args(["-0", path.to_str().unwrap()])
400        .output()
401        .unwrap();
402    assert!(
403        output.status.success(),
404        "{}",
405        String::from_utf8_lossy(&output.stderr)
406    );
407    assert_eq!(output.stdout, b"permissionless\n");
408}
409
410#[test]
411fn intervals_restart_after_callback_microtasks() {
412    let output = run(
413        r#"
414        const order = [];
415        let count = 0;
416        await new Promise(resolve => {
417            const id = setInterval(() => {
418                order.push(`I${count}`);
419                if (count++ === 0) queueMicrotask(() => setTimeout(() => order.push('T')));
420                else {clearInterval(id);resolve();}
421            });
422        });
423        console.log(order.join());
424    "#,
425        &[],
426    );
427    assert_eq!(output.stdout, b"I0,T,I1\n");
428}