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
6.4 KiB136 linesraw
1mod common;
2
3#[test]
4fn broadcast_fanout_clones_once_and_preserves_fifo_tasks() {
5    let output = common::run(
6        r#"
7        const sender = new BroadcastChannel('test'), a = new BroadcastChannel('test'), b = new BroadcastChannel('test');
8        const other = new BroadcastChannel('other');
9        sender.onmessage = other.onmessage = () => {throw Error('Wrong recipient');};
10        let reads = 0;
11        const values = {get value() {reads++; return 1;}};
12        sender.postMessage(values);
13        sender.postMessage({value: 2});
14        const seen = [];
15        a.onmessage = event => {
16            if (event.target !== a || !(event instanceof MessageEvent) || !event.isTrusted ||
17                event.source !== null || event.ports.length !== 0 || event.origin !== '') throw Error('MessageEvent');
18            seen.push(event.data.value); event.data.value = 9;
19            queueMicrotask(() => seen.push('microtask'));
20        };
21        const received = new Promise(resolve => b.onmessage = event => {
22            if (event.data.value !== 1 && event.data.value !== 2) throw Error('Shared clone');
23            if (event.data.value === 2) resolve();
24        });
25        queueMicrotask(() => seen.push('before'));
26        await received;
27        await new Promise(resolve => setTimeout(resolve, 10));
28        if (reads !== 1 || seen.join() !== 'before,1,microtask,2,microtask') throw Error('Clone/task order ' + seen);
29        for (const channel of [sender,a,b,other]) channel.close();
30        console.log('broadcast');
31    "#,
32        &[],
33    );
34    assert_eq!(output.stdout, b"broadcast\n");
35}
36
37#[test]
38fn broadcast_brands_clone_errors_and_close() {
39    let output = common::run(
40        r#"
41        class Sub extends BroadcastChannel {}
42        const a = new Sub({toString() {return 'name';}}), b = new BroadcastChannel('name');
43        if (a.name !== 'name' || !(a instanceof EventTarget) || !(a instanceof Sub) ||
44            Object.prototype.toString.call(a) !== '[object BroadcastChannel]' || a.onmessage !== null || a.onmessageerror !== null) throw Error('Surface');
45        for (const operation of [() => new BroadcastChannel(), () => new BroadcastChannel(Symbol()),
46            () => BroadcastChannel.prototype.close.call({}), () => a.postMessage()]) {
47            try {operation(); throw Error('Accepted invalid input');} catch (error) {if (!(error instanceof TypeError)) throw error;}
48        }
49        const buffer = new Uint8Array([1]).buffer;
50        for (const value of [() => {}, a, new MessageChannel().port1]) {
51            try {a.postMessage(value); throw Error('Clone accepted');} catch (error) {if (error.name !== 'DataCloneError') throw error;}
52        }
53        a.postMessage(buffer, [buffer]);
54        if (buffer.byteLength !== 1) throw Error('Unexpected transfer');
55        b.onmessage = () => {throw Error('Closed destination received a message');};
56        b.close(); b.close();
57        a.close();
58        try {a.postMessage(() => {}); throw Error('Closed channel posted');} catch (error) {if (error.name !== 'InvalidStateError') throw error;}
59        const lonely = new BroadcastChannel('lonely');
60        try {lonely.postMessage(() => {}); throw Error('No-recipient clone skipped');} catch (error) {if (error.name !== 'DataCloneError') throw error;}
61        lonely.close();
62        await new Promise(resolve => setTimeout(resolve, 10));
63        console.log('closed');
64    "#,
65        &[],
66    );
67    assert_eq!(output.stdout, b"closed\n");
68}
69
70#[test]
71fn broadcasts_cross_worker_isolates_and_clone_host_objects() {
72    let dir = tempfile::tempdir().unwrap();
73    std::fs::write(dir.path().join("worker.js"), r#"
74        const channel = new BroadcastChannel('workers');
75        channel.onmessage = async event => {
76            if (!(event.data.blob instanceof Blob) || await event.data.blob.text() !== 'blob' || event.data.map.get('key') !== 4) throw Error('Host clone');
77            channel.postMessage('reply');
78            channel.close(); close();
79        };
80        postMessage('ready');
81    "#).unwrap();
82    std::fs::write(dir.path().join("main.js"), r#"
83        const channel = new BroadcastChannel('workers');
84        const worker = new Worker(new URL('./worker.js', import.meta.url), {type:'module'});
85        await new Promise((resolve, reject) => {worker.onmessage = resolve; worker.onerror = reject;});
86        const reply = new Promise(resolve => channel.onmessage = event => resolve(event.data));
87        const post = BroadcastChannel.prototype.postMessage, close = BroadcastChannel.prototype.close;
88        MessagePort.prototype.postMessage = () => {throw Error('Guest port');};
89        globalThis.structuredClone = () => {throw Error('Guest clone');};
90        post.call(channel, {blob:new Blob(['blob']), map:new Map([['key',4]])});
91        if (await reply !== 'reply') throw Error('Worker reply');
92        close.call(channel); worker.terminate();
93        console.log('workers');
94    "#).unwrap();
95    let output = common::wait(
96        std::process::Command::new(env!("CARGO_BIN_EXE_avril"))
97            .arg("run")
98            .arg(dir.path().join("main.js"))
99            .stdout(std::process::Stdio::piped())
100            .stderr(std::process::Stdio::piped())
101            .spawn()
102            .unwrap(),
103    );
104    assert!(
105        output.status.success(),
106        "{}",
107        String::from_utf8_lossy(&output.stderr)
108    );
109    assert_eq!(output.stdout, b"workers\n");
110}
111
112#[test]
113fn broadcast_names_preserve_exact_domstring_code_units() {
114    let output = common::run(
115        r#"
116        const names = ['\uD800','\uDC00','\uFFFD','valid😀','nul\0name'];
117        const senders = names.map(name => new BroadcastChannel(name));
118        const receivers = names.map(name => new BroadcastChannel(name));
119        const counts = names.map(() => 0);
120        const completed = new Promise(resolve => receivers.forEach((channel,i) => {
121            if (channel.name !== names[i]) throw Error('Name conversion');
122            channel.onmessage = event => {
123                if (event.data !== i || ++counts[i] !== 1) throw Error('Name crosstalk');
124                if (counts.every(count => count === 1)) resolve();
125            };
126        }));
127        senders.forEach((channel,i) => channel.postMessage(i));
128        await completed;
129        await new Promise(resolve => setTimeout(resolve,10));
130        [...senders,...receivers].forEach(channel => channel.close());
131        console.log('names');
132    "#,
133        &[],
134    );
135    assert_eq!(output.stdout, b"names\n");
136}