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.5 KiB417 linesraw
1mod common;
2
3use std::fs;
4use std::io::Write;
5#[cfg(unix)]
6use std::io::{BufRead, Read};
7use std::process::{Command, Stdio};
8#[cfg(unix)]
9use std::time::Duration;
10
11#[test]
12fn blocking_binary_io_preserves_views_and_does_not_run_callbacks() {
13    let input: Vec<u8> = (0..160_003).map(|index| (index % 251) as u8).collect();
14    let output = common::run(
15        r#"
16        import { stdin, stdout } from 'avril:std/sync';
17        const storage = new Uint8Array(1033).fill(255);
18        const buffer = new Uint8Array(storage.buffer, 7, 1024);
19        let ran = false;
20        queueMicrotask(() => ran = true);
21        if (stdin.read(new Uint8Array()) !== 0) throw Error('Empty read');
22        for (;;) {
23            const count = stdin.read(buffer);
24            if (count === null) break;
25            if (count <= 0 || count > buffer.length) throw Error('Read count');
26            if (stdout.write(buffer.subarray(0, count)) !== count) throw Error('Write count');
27            if (storage[0] !== 255 || storage[6] !== 255 || storage[1032] !== 255) throw Error('View boundary');
28            if (ran) throw Error('Sync IO ran JS');
29        }
30        if (stdin.read(new Uint8Array(1)) !== null || stdin.read(new Uint8Array()) !== 0) throw Error('EOF');
31        if (stdout.write(new Uint8Array()) !== 0) throw Error('Empty write');
32    "#,
33        &input,
34    );
35    assert_eq!(output.stdout, input);
36    assert!(output.stderr.is_empty());
37}
38
39#[test]
40fn blocking_output_shares_ordering_with_console_and_submitted_stream_writes() {
41    let output = common::run(
42        r#"
43        import { stdout as stream, stderr as errorStream } from 'avril:std';
44        import { stdout, stderr } from 'avril:std/sync';
45        const encode = text => new TextEncoder().encode(text);
46        const writer = stream.getWriter(), errors = errorStream.getWriter();
47        await writer.ready; await errors.ready;
48        const first = writer.write(encode('A'));
49        let settled = false;
50        first.then(() => settled = true);
51        if (stdout.write(encode('B')) !== 1 || settled) throw Error('Output order or reentrancy');
52        console.log('C');
53        await first;
54        await writer.write(encode('D'));
55        stdout.write(encode('E'));
56        await writer.close();
57        stdout.write(encode('F'));
58        const pending = errors.write(encode('G'));
59        stderr.write(encode('H')); console.error('I');
60        await pending; await errors.close();
61        stderr.write(encode('J'));
62    "#,
63        &[],
64    );
65    assert_eq!(output.stdout, b"ABC\nDEF");
66    assert_eq!(output.stderr, b"GHI\nJ");
67}
68
69#[test]
70fn sync_input_refuses_to_steal_an_outstanding_async_read() {
71    let (_dir, mut child) = common::spawn(
72        r#"
73        import { stdin as stream } from 'avril:std';
74        import { stdin, errors } from 'avril:std/sync';
75        const reader = stream.getReader();
76        await Promise.resolve();
77        const pending = reader.read();
78        let busy = false;
79        try { stdin.read(new Uint8Array(1)); }
80        catch (error) { if (!(error instanceof errors.Busy)) throw error; busy = true; }
81        if (!busy) throw Error('Overlapping read accepted');
82        if (stdin.read(new Uint8Array()) !== 0) throw Error('Empty read should not reserve stdin');
83        await reader.cancel();
84        if (!(await pending).done || stdin.read(new Uint8Array(1)) !== null) throw Error('Cancellation');
85        console.log('canceled');
86    "#,
87    );
88    let stdin = child.stdin.take().unwrap();
89    let output = common::wait(child);
90    drop(stdin);
91    assert!(
92        output.status.success(),
93        "{}",
94        String::from_utf8_lossy(&output.stderr)
95    );
96    assert_eq!(output.stdout, b"canceled\n");
97}
98
99#[test]
100fn completed_sync_and_byob_reads_share_the_input_without_losing_bytes() {
101    let output = common::run(
102        r#"
103        import { stdin as stream } from 'avril:std';
104        import { stdin, stdout } from 'avril:std/sync';
105        const first = new Uint8Array(2);
106        if (stdin.read(first) !== 2) throw Error('Initial read');
107        stdout.write(first);
108        const reader = stream.getReader({mode: 'byob'});
109        const {value, done} = await reader.read(new Uint8Array(3));
110        if (done || value.length !== 3) throw Error('BYOB read');
111        stdout.write(value); reader.releaseLock();
112        const last = new Uint8Array(16), count = stdin.read(last);
113        if (count !== 1) throw Error('Trailing read');
114        stdout.write(last.subarray(0, count));
115        if (stdin.read(last) !== null) throw Error('EOF');
116    "#,
117        b"abcdef",
118    );
119    assert_eq!(output.stdout, b"abcdef");
120}
121
122#[test]
123fn blocking_stdio_validates_buffers_and_exposes_only_frozen_interfaces() {
124    let output = common::run(
125        r#"
126        import { stdin, stdout, stderr } from 'avril:std/sync';
127        import { stdin as stream } from 'avril:std';
128        for (const value of [stdin, stdout, stderr]) {
129            if (!Object.isFrozen(value) || 'close' in value || Symbol.dispose in value) throw Error('Ownership');
130        }
131        if (stdin === stream || 'readable' in stdin || 'writable' in stdout) throw Error('Stream API replaced');
132        const bytes = new Uint8Array(2);
133        structuredClone(bytes.buffer, {transfer: [bytes.buffer]});
134        const resizable = new ArrayBuffer(4, {maxByteLength: 8});
135        const outOfBounds = new Uint8Array(resizable, 1, 2); resizable.resize(0);
136        for (const invalid of [undefined, null, [], 'text', new DataView(new ArrayBuffer(2)), new Uint16Array(2), bytes, outOfBounds]) {
137            for (const method of [stdin.read, stdout.write, stderr.write]) {
138                let threw = false;
139                try { method(invalid); }
140                catch (error) { if (!(error instanceof TypeError)) throw error; threw = true; }
141                if (!threw) throw Error('Accepted invalid buffer');
142            }
143        }
144        console.log('validated');
145    "#,
146        &[],
147    );
148    assert_eq!(output.stdout, b"validated\n");
149}
150
151#[test]
152fn blocking_stdio_supports_shared_resizable_views_and_captured_intrinsics() {
153    let output = common::run(
154        r#"
155        import { stdin, stdout } from 'avril:std/sync';
156        const shared = new Uint8Array(new SharedArrayBuffer(4));
157        const a = new Uint8Array(shared.buffer, 1, 2);
158        const resizable = new ArrayBuffer(4, {maxByteLength: 8});
159        const b = new Uint8Array(resizable, 1, 2);
160        const c = new Uint8Array(resizable, 3); resizable.resize(7);
161        const growable = new SharedArrayBuffer(1, {maxByteLength: 4});
162        const d = new Uint8Array(growable); growable.grow(2);
163        const fail = () => { throw Error('Mutable intrinsic'); };
164        globalThis.Uint8Array = fail;
165        Object.getPrototypeOf(a).set = fail;
166        Object.prototype.toJSON = fail;
167        Array.prototype.toJSON = fail;
168        for (const view of [a, b, c, d]) {
169            if (stdin.read(view) !== view.length || stdout.write(view) !== view.length) throw Error('Transfer count');
170        }
171    "#,
172        b"ABCDEFGHIJ",
173    );
174    assert_eq!(output.stdout, b"ABCDEFGHIJ");
175}
176
177#[test]
178fn stdio_remains_available_without_filesystem_authority() {
179    let output = common::run(
180        r#"
181        import { pledge } from 'avril:std';
182        import { stdin, stdout, stderr } from 'avril:std/sync';
183        pledge({read: false, write: false, env: false, sys: false});
184        const bytes = new Uint8Array(32), count = stdin.read(bytes);
185        stdout.write(bytes.subarray(0, count)); stderr.write(new TextEncoder().encode('diagnostic'));
186    "#,
187        b"stdio",
188    );
189    assert_eq!(output.stdout, b"stdio");
190    assert_eq!(output.stderr, b"diagnostic");
191}
192
193#[cfg(unix)]
194#[test]
195fn broken_pipes_throw_synchronously_and_remain_output_failures() {
196    let (_dir, mut child) = common::spawn(
197        r#"
198        import { stdout, stderr, errors } from 'avril:std/sync';
199        let failed = false;
200        try { stdout.write(new Uint8Array([1])); }
201        catch (error) { if (!(error instanceof errors.BrokenPipe)) throw error; failed = true; }
202        if (!failed) throw Error('Broken pipe accepted');
203        stderr.write(new TextEncoder().encode('caught BrokenPipe\n'));
204    "#,
205    );
206    drop(child.stdout.take());
207    let output = common::wait(child);
208    assert!(!output.status.success());
209    assert!(String::from_utf8_lossy(&output.stderr).contains("caught BrokenPipe"));
210}
211
212#[test]
213fn the_repl_reserves_input_but_allows_blocking_output_in_callbacks() {
214    let dir = tempfile::tempdir().unwrap();
215    let mut child = Command::new(env!("CARGO_BIN_EXE_avril"))
216        .args(["repl", "-0"])
217        .current_dir(dir.path())
218        .stdin(Stdio::piped())
219        .stdout(Stdio::piped())
220        .stderr(Stdio::piped())
221        .spawn()
222        .unwrap();
223    child.stdin.take().unwrap().write_all(br#"import { stdin, stderr, errors } from 'avril:std/sync'
224try { stdin.read(new Uint8Array(1)) } catch (error) { error instanceof errors.NotSupported }
225await new Promise(resolve => setTimeout(() => { stderr.write(new TextEncoder().encode('callback')); resolve(); }, 0))
226.exit
227"#).unwrap();
228    let output = common::wait(child);
229    assert!(
230        output.status.success(),
231        "{}",
232        String::from_utf8_lossy(&output.stderr)
233    );
234    assert!(String::from_utf8_lossy(&output.stdout).contains("true\n"));
235    assert_eq!(output.stderr, b"callback");
236}
237
238fn worker(main: &str, source: &str) -> (tempfile::TempDir, std::process::Child) {
239    let dir = tempfile::tempdir().unwrap();
240    fs::write(dir.path().join("main.js"), main).unwrap();
241    fs::write(dir.path().join("worker.js"), source).unwrap();
242    fs::write(dir.path().join("lock"), "").unwrap();
243    let child = Command::new(env!("CARGO_BIN_EXE_avril"))
244        .args(["run", "main.js"])
245        .current_dir(dir.path())
246        .stdin(Stdio::null())
247        .stdout(Stdio::piped())
248        .stderr(Stdio::piped())
249        .spawn()
250        .unwrap();
251    (dir, child)
252}
253
254#[test]
255fn workers_reserve_input_but_allow_blocking_output_from_callbacks() {
256    let (_dir, child) = worker(
257        r#"
258        const worker = new Worker('./worker.js', {type: 'module'});
259        const done = new Promise((resolve, reject) => { worker.onmessage = resolve; worker.onerror = reject; });
260        worker.postMessage('go'); await done; worker.terminate();
261    "#,
262        r#"
263        import { stdin, stdout, errors } from 'avril:std/sync';
264        onmessage = () => {
265            let refused = false;
266            try { stdin.read(new Uint8Array(1)); }
267            catch (error) { if (!(error instanceof errors.NotSupported)) throw error; refused = true; }
268            if (!refused) throw Error('Worker consumed stdin');
269            stdout.write(new TextEncoder().encode('worker output'));
270            postMessage('done');
271        };
272    "#,
273    );
274    let output = common::wait(child);
275    assert!(
276        output.status.success(),
277        "{}",
278        String::from_utf8_lossy(&output.stderr)
279    );
280    assert_eq!(output.stdout, b"worker output");
281}
282
283#[cfg(unix)]
284fn stderr_lines(
285    stderr: std::process::ChildStderr,
286) -> (
287    std::sync::mpsc::Receiver<String>,
288    std::thread::JoinHandle<Vec<u8>>,
289) {
290    let (send, receive) = std::sync::mpsc::channel();
291    let reader = std::thread::spawn(move || {
292        let mut stderr = std::io::BufReader::new(stderr);
293        let mut output = Vec::new();
294        loop {
295            let mut line = String::new();
296            if stderr.read_line(&mut line).unwrap() == 0 {
297                break;
298            }
299            output.extend_from_slice(line.as_bytes());
300            let _ = send.send(line);
301        }
302        output
303    });
304    (receive, reader)
305}
306
307#[cfg(unix)]
308#[test]
309fn terminating_a_worker_interrupts_blocked_output_and_its_shutdown_flush() {
310    let (_dir, mut child) = worker(
311        r#"
312        import { open, stderr } from 'avril:std/sync';
313        const worker = new Worker('./worker.js', {type: 'module'});
314        await new Promise((resolve, reject) => { worker.onmessage = resolve; worker.onerror = reject; });
315        await new Promise(resolve => setTimeout(resolve, 30));
316        worker.terminate();
317        const file = open('lock', {read: true, write: true});
318        file.flockSync(); file.funlockSync(); file.close();
319        stderr.write(new TextEncoder().encode('terminated\n'));
320    "#,
321        r#"
322        import { open, stdout } from 'avril:std/sync';
323        const file = open('lock', {read: true, write: true});
324        file.flockSync();
325        postMessage('writing');
326        stdout.write(new Uint8Array(4 * 1024 * 1024).fill(65));
327        throw Error('Blocked write returned before termination');
328    "#,
329    );
330    let (receive, reader) = stderr_lines(child.stderr.take().unwrap());
331    let marker = receive.recv_timeout(Duration::from_secs(3));
332    if marker.as_deref() != Ok("terminated\n") {
333        let _ = child.kill();
334        let output = common::wait(child);
335        let stderr = reader.join().unwrap();
336        panic!(
337            "Worker retained its file lock during output: {marker:?}; {:?}; {stderr:?}",
338            output.status
339        );
340    }
341    let output = common::wait(child);
342    let stderr = reader.join().unwrap();
343    assert!(
344        output.status.success(),
345        "{}",
346        String::from_utf8_lossy(&stderr)
347    );
348    assert_eq!(output.stdout, vec![65; 4 * 1024 * 1024]);
349}
350
351#[cfg(unix)]
352#[test]
353fn repl_interrupts_a_blocked_sync_write_without_waiting_for_stdout_to_drain() {
354    use std::os::unix::process::CommandExt;
355
356    let dir = tempfile::tempdir().unwrap();
357    let mut command = Command::new(env!("CARGO_BIN_EXE_avril"));
358    // SAFETY: signal is async-signal-safe; no inherited SIGINT ignore may mask the test.
359    unsafe {
360        command.pre_exec(|| {
361            libc::signal(libc::SIGINT, libc::SIG_DFL);
362            Ok(())
363        });
364    }
365    let mut child = command
366        .args(["repl", "-0"])
367        .current_dir(dir.path())
368        .stdin(Stdio::piped())
369        .stdout(Stdio::piped())
370        .stderr(Stdio::piped())
371        .spawn()
372        .unwrap();
373    let mut stdin = child.stdin.take().unwrap();
374    let (messages, stderr) = stderr_lines(child.stderr.take().unwrap());
375    let mut stdout = child.stdout.take().unwrap();
376    let (send, receive) = std::sync::mpsc::channel();
377    let first = std::thread::spawn(move || {
378        let mut byte = [0];
379        let result = stdout.read_exact(&mut byte).map(|()| byte[0]);
380        let _ = send.send(result);
381        stdout
382    });
383    stdin.write_all(b"(await import('avril:std/sync')).stdout.write(new Uint8Array(4 * 1024 * 1024).fill(65))\n").unwrap();
384    let started = receive.recv_timeout(Duration::from_secs(3));
385    if !matches!(started, Ok(Ok(65))) {
386        let _ = child.kill();
387        child.stdout = Some(first.join().unwrap());
388        let output = common::wait(child);
389        panic!(
390            "Sync write did not start: {started:?}; {:?}; {:?}",
391            output.status,
392            stderr.join().unwrap()
393        );
394    }
395    child.stdout = Some(first.join().unwrap());
396    assert_eq!(unsafe { libc::kill(child.id() as i32, libc::SIGINT) }, 0);
397    let interrupted = messages.recv_timeout(Duration::from_secs(3));
398    if interrupted.as_deref() != Ok("Evaluation interrupted\n") {
399        let _ = child.kill();
400        let output = common::wait(child);
401        panic!(
402            "Sync write was not interrupted: {interrupted:?}; {:?}; {:?}",
403            output.status,
404            stderr.join().unwrap()
405        );
406    }
407    stdin.write_all(b".exit\n").unwrap();
408    drop(stdin);
409    let output = common::wait(child);
410    let stderr = stderr.join().unwrap();
411    assert!(
412        output.status.success(),
413        "{}",
414        String::from_utf8_lossy(&stderr)
415    );
416    assert_eq!(output.stdout, vec![65; 4 * 1024 * 1024 - 1]);
417}