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
8.4 KiB161 linesraw
1mod common;
2
3#[test]
4fn exposed_constructors_ignore_reparenting_and_preserve_subclassing() {
5    let output = common::run(
6        r#"
7        import { errors } from 'avril:std';
8        let calls = 0;
9        function Other() { calls++; }
10        for (const parent of [Other, null]) {
11            for (const ctor of [DOMException, CustomEvent, ErrorEvent, PromiseRejectionEvent, MessageEvent, AbortSignal, MessagePort, ...Object.values(errors)]) {
12                Object.setPrototypeOf(ctor, parent);
13            }
14            const exception = new DOMException('message', 'AbortError');
15            if (!(exception instanceof Error) || exception.message !== 'message' || exception.code !== 20) throw Error('Exception initialization');
16            const custom = new CustomEvent('custom', { detail: 42, cancelable: true });
17            const error = new ErrorEvent('error', { message: 'message', lineno: 7 });
18            const promise = Promise.resolve();
19            const rejection = new PromiseRejectionEvent('rejection', { promise, reason: 9 });
20            const message = new MessageEvent('message', { data: 3, origin: 'origin' });
21            if (custom.detail !== 42 || !custom.cancelable || error.message !== 'message' || error.lineno !== 7 || rejection.promise !== promise || rejection.reason !== 9 || message.data !== 3 || message.origin !== 'origin') throw Error('Event initialization');
22            const target = new EventTarget();
23            let events = 0;
24            target.addEventListener('custom', () => events++);
25            target.dispatchEvent(custom);
26            if (events !== 1 || custom.target !== target) throw Error('Event base state');
27            const controller = new AbortController();
28            let aborted = false;
29            controller.signal.addEventListener('abort', () => aborted = true);
30            controller.abort(11);
31            if (!aborted || controller.signal.reason !== 11 || !AbortSignal.abort().aborted) throw Error('Signal base state');
32            const channel = new MessageChannel();
33            let delivered = false;
34            channel.port2.onmessage = event => {
35                if (event.data !== 12 || event.type !== 'message' || event.target !== channel.port2) throw Error('Port delivery');
36                delivered = true;
37            };
38            channel.port1.postMessage(12);
39            await new Promise(resolve => setTimeout(resolve, 10));
40            if (!delivered) throw Error('MessagePort base state');
41            channel.port1.close(); channel.port2.close();
42            for (const [name, ctor] of Object.entries(errors)) {
43                const error = new ctor('failure');
44                if (!(error instanceof Error) || error.name !== name || error.message !== 'failure') throw Error('File error initialization');
45            }
46            class Child extends CustomEvent { #value = 13; get value() { return this.#value; } }
47            const child = new Child('child', { detail: 14 });
48            if (!(child instanceof Child) || !(child instanceof CustomEvent) || !(child instanceof Event) || child.type !== 'child' || child.detail !== 14 || child.value !== 13) throw Error('Guest subclass');
49        }
50        if (calls) throw Error('Guest superclass called');
51        console.log('constructors safe');
52        "#,
53        &[],
54    );
55    assert_eq!(output.stdout, b"constructors safe\n");
56}
57
58#[test]
59fn message_event_initialization_uses_the_captured_base_method() {
60    let output = common::run(
61        r#"
62        Event.prototype.initEvent = () => { throw Error('Guest base method'); };
63        const event = new MessageEvent('before');
64        event.initMessageEvent('after', true, true, 42);
65        if (event.type !== 'after' || !event.bubbles || !event.cancelable || event.data !== 42) throw Error('Event initialization');
66        console.log('base method safe');
67        "#,
68        &[],
69    );
70    assert_eq!(output.stdout, b"base method safe\n");
71}
72
73#[test]
74fn file_write_options_do_not_iterate_internal_arrays() {
75    let output = common::run(
76        r#"
77        import { writeTextFile, readTextFile } from 'avril:fs';
78        Array.prototype[Symbol.iterator] = () => { throw Error('Guest array iterator'); };
79        const path = new URL('./data', import.meta.url);
80        await writeTextFile(path, 'safe');
81        await writeTextFile(path, ' append', { append: true });
82        if (await readTextFile(path) !== 'safe append') throw Error('Write flags');
83        console.log('file options safe');
84        "#,
85        &[],
86    );
87    assert_eq!(output.stdout, b"file options safe\n");
88}
89
90const PROMISE_TEST: &str = r#"
91    import { stdin } from 'avril:std';
92    import * as fs from 'avril:fs';
93    const originalPromise = Promise, originalThen = Promise.prototype.then;
94    function observe(promise) {
95        Object.defineProperty(promise, 'constructor', { value: undefined, configurable: true });
96        const result = Reflect.apply(originalThen, promise, [value => value, error => { throw error; }]);
97        Object.defineProperty(result, 'constructor', { value: originalPromise });
98        return result;
99    }
100    let hooks = 0;
101    const poison = () => { hooks++; throw Error('Guest promise hook'); };
102    POISON
103    const response = new Response(new Blob(['hello']).stream());
104    if (await observe(response.text()) !== 'hello') throw Error('Body consumption');
105    const source = new ReadableStream({
106        start(controller) { controller.enqueue('chunk'); controller.close(); },
107    });
108    const clone = structuredClone(source, { transfer: [source] });
109    const reader = clone.getReader();
110    if ((await observe(reader.read())).value !== 'chunk' || !(await observe(reader.read())).done) throw Error('Stream transfer');
111    reader.releaseLock();
112    const iterator = new ReadableStream({ start(controller) { controller.close(); } }).values();
113    if (!(await observe(iterator.next())).done || !(await observe(iterator.next())).done || !(await observe(iterator.return())).done) throw Error('Closed stream iterator');
114    let writes = '';
115    const stream = new ReadableStream({ start(controller) { controller.enqueue('pipe'); controller.close(); } });
116    await observe(stream.pipeTo(new WritableStream({ write(chunk) { writes += chunk; return new originalPromise(resolve => resolve()); } })));
117    if (writes !== 'pipe') throw Error('Promise callback assimilation');
118    const abortController = new AbortController();
119    const piping = new ReadableStream().pipeTo(new WritableStream(), { signal: abortController.signal });
120    abortController.abort(42);
121    try { await observe(piping); throw Error('Abort accepted'); } catch (error) { if (error !== 42) throw error; }
122    const bytes = await observe(new Response(new Blob(['hello']).stream().pipeThrough(new CompressionStream('gzip'))).bytes());
123    if (await observe(new Response(new Blob([bytes]).stream().pipeThrough(new DecompressionStream('gzip'))).text()) !== 'hello') throw Error('Codec promises');
124    const path = new URL('./data', import.meta.url);
125    await observe(fs.writeTextFile(path, 'file'));
126    if (await observe(fs.readTextFile(path)) !== 'file') throw Error('File promises');
127    const input = stdin.getReader();
128    if (new TextDecoder().decode((await observe(input.read())).value) !== 'stdin') throw Error('Stdio promises');
129    input.releaseLock();
130    const wasm = new Response(new Uint8Array([0,97,115,109,1,0,0,0]), { headers: { 'content-type': 'application/wasm' } });
131    if (!(await observe(WebAssembly.compileStreaming(new originalPromise(resolve => resolve(wasm)))) instanceof WebAssembly.Module)) throw Error('Wasm promises');
132    let thenableCalls = 0;
133    await observe(new WritableStream({ start() { return { then(resolve) { thenableCalls++; resolve(); } }; } }).close());
134    if (thenableCalls !== 1 || hooks !== 0) throw Error('Promise protocol');
135    console.log('promises safe');
136"#;
137
138#[test]
139fn internal_reactions_ignore_promise_species() {
140    promises("Object.defineProperty(Promise, Symbol.species, { get: poison });");
141}
142
143#[test]
144fn internal_awaits_ignore_promise_constructor_getters() {
145    promises(
146        "Object.defineProperty(Promise.prototype, 'constructor', { get: poison, configurable: true });",
147    );
148}
149
150#[test]
151fn internal_assimilation_ignores_replaced_promise_then() {
152    promises(
153        "Promise.prototype.then = poison; Promise.resolve = poison; Promise.reject = poison; Promise.all = poison;",
154    );
155}
156
157fn promises(poison: &str) {
158    let source = PROMISE_TEST.replace("POISON", poison);
159    let output = common::run(&source, b"stdin");
160    assert_eq!(output.stdout, b"promises safe\n");
161}