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
9.7 KiB188 linesraw
1mod common;
2
3use std::io::{BufRead, Write};
4use std::net::TcpListener;
5use std::thread;
6use std::time::Duration;
7
8use common::run;
9
10#[test]
11fn wasm_streaming_compiles_and_instantiates_incrementally() {
12    let output = run(
13        r#"
14        const bytes = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0]);
15        function response(body = bytes, headers = {'content-type': 'application/wasm'}, status = 200) {
16            return new Response(body, {headers, status});
17        }
18        const chunks = new ReadableStream({
19            start(controller) { this.controller = controller; this.index = 0; },
20            pull(controller) {
21                if (this.index === bytes.length) controller.close();
22                else controller.enqueue(bytes.slice(this.index, ++this.index));
23            },
24        });
25        const streamed = response(chunks, {'content-type': 'application/wasm; charset=binary'});
26        const module = await WebAssembly.compileStreaming(Promise.resolve(streamed), {});
27        if (!(module instanceof WebAssembly.Module) || !streamed.bodyUsed) throw Error('Streaming compile');
28        let controller, settled = false;
29        const pendingResponse = response(new ReadableStream({start(c) { controller = c; }}));
30        const pending = WebAssembly.compileStreaming(pendingResponse).then(value => { settled = true; return value; });
31        controller.enqueue(bytes);
32        for (let i = 0; i < 10 && !pendingResponse.bodyUsed; i++) await Promise.resolve();
33        if (settled || !pendingResponse.bodyUsed) throw Error('Streaming waited for body aggregation');
34        controller.close();
35        if (!(await pending instanceof WebAssembly.Module)) throw Error('Incremental completion');
36        const instantiatedResponse = response();
37        const result = await WebAssembly.instantiateStreaming(instantiatedResponse, {}, {});
38        if (!(result.module instanceof WebAssembly.Module) || !(result.instance instanceof WebAssembly.Instance)) {
39            throw Error('Streaming instantiate result');
40        }
41        if (WebAssembly.compileStreaming.length !== 1 || WebAssembly.instantiateStreaming.length !== 2) {
42            throw Error('Streaming function lengths');
43        }
44        for (const fn of [WebAssembly.compileStreaming, WebAssembly.instantiateStreaming]) {
45            try { new fn(); throw Error('Constructible streaming method'); }
46            catch (error) { if (!(error instanceof TypeError)) throw error; }
47        }
48        if (WebAssembly.compileStreaming.name !== 'compileStreaming' || WebAssembly.instantiateStreaming.name !== 'instantiateStreaming') {
49            throw Error('Streaming function names');
50        }
51        for (const storage of [
52            new ArrayBuffer(12),
53            new ArrayBuffer(12, {maxByteLength: 24}),
54            new SharedArrayBuffer(12),
55            new SharedArrayBuffer(12, {maxByteLength: 24}),
56        ]) {
57            const view = new Uint8Array(storage, 2, bytes.length);
58            view.set(bytes);
59            const body = new ReadableStream({
60                start(c) {c.enqueue(view); c.close();}
61            });
62            if (!(await WebAssembly.compileStreaming(response(body)) instanceof WebAssembly.Module)) {
63                throw Error('Buffer storage and view bounds');
64            }
65        }
66        console.log('wasm stream success');
67        "#,
68        &[],
69    );
70    assert_eq!(output.stdout, b"wasm stream success\n");
71}
72
73#[test]
74fn wasm_streaming_consumes_chunked_http_responses_incrementally() {
75    let listener = TcpListener::bind("127.0.0.1:0").unwrap();
76    let url = format!("http://{}", listener.local_addr().unwrap());
77    let server = thread::spawn(move || {
78        let (mut socket, _) = listener.accept().unwrap();
79        let mut request = std::io::BufReader::new(socket.try_clone().unwrap());
80        let mut line = String::new();
81        request.read_line(&mut line).unwrap();
82        loop {
83            line.clear();
84            request.read_line(&mut line).unwrap();
85            if line == "\r\n" {
86                break;
87            }
88        }
89        socket.write_all(b"HTTP/1.1 200 OK\r\nContent-Type: application/wasm\r\nTransfer-Encoding: chunked\r\nConnection: close\r\n\r\n4\r\n\x00asm\r\n").unwrap();
90        socket.flush().unwrap();
91        thread::sleep(Duration::from_millis(100));
92        socket
93            .write_all(b"4\r\n\x01\x00\x00\x00\r\n0\r\n\r\n")
94            .unwrap();
95    });
96    let source = format!(
97        r#"
98        const response = await fetch('{url}');
99        let settled = false;
100        const compiling = WebAssembly.compileStreaming(response).then(module => {{ settled = true; return module; }});
101        for (let i = 0; i < 100 && !response.bodyUsed; i++) await new Promise(resolve => setTimeout(resolve, 2));
102        if (!response.bodyUsed || settled) throw Error('HTTP body was buffered before streaming');
103        if (!(await compiling instanceof WebAssembly.Module)) throw Error('Chunked HTTP module');
104        console.log('wasm http stream');
105        "#
106    );
107    assert_eq!(run(&source, &[]).stdout, b"wasm http stream\n");
108    server.join().unwrap();
109}
110
111#[test]
112fn wasm_streaming_validates_responses_and_preserves_body_errors() {
113    let output = run(
114        r#"
115        const bytes = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0]);
116        const importingModule = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 4, 1, 96, 0, 0, 2, 7, 1, 1, 109, 1, 102, 0, 0]);
117        async function rejects(promise, Type, expected) {
118            try { await promise; throw Error('Expected rejection'); }
119            catch (error) {
120                if (Type !== null && !(error instanceof Type)) throw error;
121                if (expected !== undefined && error !== expected) throw Error('Rejection identity changed');
122            }
123        }
124        async function rejectsBeforeSource(promise, expected) {
125            let settled = false, error;
126            promise.then(() => { settled = true; }, reason => { settled = true; error = reason; });
127            for (let i = 0; i < 20 && !settled; i++) await Promise.resolve();
128            if (!settled) throw Error('Native argument validation waited for the source');
129            if (expected === undefined ? !(error instanceof TypeError) : error !== expected) throw error;
130        }
131        const never = new Promise(() => {});
132        await rejectsBeforeSource(WebAssembly.instantiateStreaming(never, 42));
133        await rejectsBeforeSource(WebAssembly.instantiateStreaming(Promise.reject('source failure'), 42));
134        const optionsReason = {badOptions: true};
135        await rejectsBeforeSource(WebAssembly.compileStreaming(never, {get builtins() { throw optionsReason; }}), optionsReason);
136        await rejects(WebAssembly.compileStreaming({}), TypeError);
137        await rejects(WebAssembly.compileStreaming(Promise.reject('source failure')), null, 'source failure');
138        const wrongMime = new Response(bytes);
139        await rejects(WebAssembly.compileStreaming(wrongMime), TypeError);
140        if (wrongMime.bodyUsed) throw Error('Rejected MIME response body was consumed');
141        await rejects(WebAssembly.compileStreaming(new Response(bytes, {headers: {'content-type': 'application/wasm'}, status: 404})), TypeError);
142        const locked = new Response(bytes, {headers: {'content-type': 'application/wasm'}});
143        locked.body.getReader();
144        await rejects(WebAssembly.compileStreaming(locked), TypeError);
145        const used = new Response(bytes, {headers: {'content-type': 'application/wasm'}});
146        await used.arrayBuffer();
147        await rejects(WebAssembly.compileStreaming(used), TypeError);
148        const reason = {failure: true};
149        const errored = new Response(new ReadableStream({start(controller) { controller.error(reason); }}), {
150            headers: {'content-type': 'application/wasm'},
151        });
152        await rejects(WebAssembly.compileStreaming(errored), Object, reason);
153        const undefinedError = new Response(new ReadableStream({start(controller) { controller.error(undefined); }}), {
154            headers: {'content-type': 'application/wasm'},
155        });
156        let gotUndefined = false;
157        await WebAssembly.compileStreaming(undefinedError).then(
158            () => { throw Error('Accepted errored response'); },
159            error => { gotUndefined = error === undefined; },
160        );
161        if (!gotUndefined) throw Error('Undefined stream error was not preserved');
162        const invalidChunk = new Response(new ReadableStream({start(controller) { controller.enqueue('not bytes'); }}), {
163            headers: {'content-type': 'application/wasm'},
164        });
165        await rejects(WebAssembly.compileStreaming(invalidChunk), TypeError);
166        const truncated = new Response(bytes.subarray(0, 6), {headers: {'content-type': 'application/wasm'}});
167        await rejects(WebAssembly.compileStreaming(truncated), WebAssembly.CompileError);
168        const missingImport = new Response(importingModule, {headers: {'content-type': 'application/wasm'}});
169        await rejects(WebAssembly.instantiateStreaming(missingImport, {m: {}}), WebAssembly.LinkError);
170        const providedImport = new Response(importingModule, {headers: {'content-type': 'application/wasm'}});
171        const imported = await WebAssembly.instantiateStreaming(providedImport, {m: {f() {}}});
172        if (!(imported.instance instanceof WebAssembly.Instance)) throw Error('Streaming imports');
173        console.log('wasm stream errors');
174        "#,
175        &[],
176    );
177    assert_eq!(output.stdout, b"wasm stream errors\n");
178}
179
180#[test]
181fn ignored_streaming_compilation_failures_remain_unhandled() {
182    let (_dir, child) = common::spawn(
183        "WebAssembly.compileStreaming(new Response(new Uint8Array([0]),{headers:{'content-type':'application/wasm'}}));",
184    );
185    let out = common::wait(child);
186    assert!(!out.status.success());
187    assert!(String::from_utf8_lossy(&out.stderr).contains("CompileError"));
188}