mod common; use common::{run, spawn, wait}; #[test] fn binary_piping_and_byob_preserve_bytes() { let input: Vec = (0..160_003).map(|index| (index % 256) as u8).collect(); for source in [ r#" import { stdin, stdout } from "avril:std"; await stdin.pipeThrough(new TransformStream()).pipeTo(stdout); "#, r#" import { stdin, stdout } from "avril:std"; const reader = stdin.getReader({ mode: "byob" }); const writer = stdout.getWriter(); while (true) { const view = new Uint8Array(new ArrayBuffer(1033), 7, 1024); const { value, done } = await reader.read(view); if (done) break; await writer.write(value); } await writer.close(); "#, ] { assert_eq!(run(source, &input).stdout, input); } } #[test] fn stdio_is_not_exposed_on_the_global_and_output_is_ordered() { let output = run( r#" const opsSymbol = Symbol.for("__avril.ops"); if (opsSymbol in globalThis || globalThis[opsSymbol] !== undefined || Reflect.ownKeys(globalThis).includes(opsSymbol)) { throw new Error("Native op registry exposed"); } if (Symbol.for("__avril.stdio") in globalThis) { throw new Error("Stdio registry exposed"); } import { stdout, stderr } from "avril:std"; const Bytes = Uint8Array; const writer = stdout.getWriter(); const errors = stderr.getWriter(); globalThis.Uint8Array = () => { throw new Error("Poisoned constructor"); }; Promise.resolve = () => { throw new Error("Poisoned resolve"); }; Array.prototype.map = () => { throw new Error("Poisoned map"); }; writer.write(new Bytes([1, 2])); writer.write(new Bytes([3, 4])); await writer.close(); errors.write(new Bytes([5, 6])); "#, &[], ); assert_eq!(output.stdout, [1, 2, 3, 4]); assert_eq!(output.stderr, [5, 6]); } #[test] fn canceling_a_pending_stdin_read_does_not_hold_the_process_open() { let (_dir, mut child) = spawn( r#" import { stdin } from "avril:std"; const reader = stdin.getReader(); const pending = reader.read(); await Promise.resolve(); await reader.cancel(); await pending; "#, ); let stdin = child.stdin.take().unwrap(); let output = wait(child); drop(stdin); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); } #[test] fn output_failures_reject_writes_and_fail_the_cli() { let (_dir, mut child) = spawn( r#"import { stdout } from "avril:std"; await stdout .getWriter().write(new Uint8Array([1]));"#, ); drop(child.stdout.take()); let output = wait(child); assert!(!output.status.success()); assert!(!output.stderr.is_empty()); } #[test] fn default_stdin_reads_do_not_retain_oversized_buffers() { let output = run( r#" import { stdin, stdout } from "avril:std"; const reader = stdin.getReader(); const writer = stdout.getWriter(); while (true) { const { value, done } = await reader.read(); if (done) break; if (value.buffer.byteLength !== value.byteLength) { throw new Error("Input chunk retained an oversized buffer"); } await writer.write(value); } await writer.close(); "#, b"hi\n", ); assert_eq!(output.stdout, b"hi\n"); } #[test] fn concurrent_stdin_reads_cancel_cleanly() { let (_dir, mut child) = spawn( r#" import { stdin } from "avril:std"; const reader = stdin.getReader(); const reads = [reader.read(), reader.read()]; await reader.cancel(); const results = await Promise.all(reads); if (results.some(({done}) => !done)) throw new Error("Cancellation did not close input"); "#, ); let stdin = child.stdin.take().unwrap(); let output = wait(child); drop(stdin); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); } #[test] fn queued_stream_output_does_not_block_console_or_the_event_loop() { let output = run( r#" import { stdout } from "avril:std"; const writer = stdout.getWriter(); const writes = []; for (let value = 65; value < 73; value++) { writes.push(writer.write(new Uint8Array(128 * 1024).fill(value))); } writes[0].then(() => console.log("done")); await Promise.all(writes); await writer.close(); "#, &[], ); let done = output .stdout .windows(b"done\n".len()) .position(|window| window == b"done\n") .expect("console output should progress while stream writes are queued"); let mut stream_output = output.stdout; stream_output.drain(done..done + b"done\n".len()); let expected: Vec<_> = (65..73) .flat_map(|byte| std::iter::repeat_n(byte, 128 * 1024)) .collect(); assert_eq!(stream_output, expected); } #[test] fn output_accepts_shared_and_resizable_byte_views() { let output = run( r#" import { stdout } from "avril:std"; const writer = stdout.getWriter(); const shared = new Uint8Array(new SharedArrayBuffer(4)); shared.set([0, 65, 66, 0]); const sharedView = new Uint8Array(shared.buffer, 1, 2); const resizable = new ArrayBuffer(4, { maxByteLength: 8 }); const fixedView = new Uint8Array(resizable, 1, 2); fixedView.set([67, 68]); const tracking = new Uint8Array(resizable, 3); resizable.resize(7); tracking.set([69, 70, 71, 72]); const growable = new SharedArrayBuffer(1, { maxByteLength: 4 }); const growingView = new Uint8Array(growable); growable.grow(2); growingView.set([73, 74]); globalThis.Uint8Array = () => { throw new Error("Poisoned constructor"); }; Object.defineProperty(ArrayBuffer.prototype, "resizable", { get() { throw new Error("Poisoned getter"); }, }); await writer.write(sharedView); await writer.write(fixedView); await writer.write(tracking); await writer.write(growingView); await writer.close(); "#, &[], ); assert_eq!(output.stdout, b"ABCDEFGHIJ"); } #[test] fn accepted_stream_writes_precede_console_output() { let output = run( r#" import { stdout } from "avril:std"; const writer = stdout.getWriter(); await writer.ready; writer.write(new Uint8Array([65, 10])); console.log("B"); await writer.close(); "#, &[], ); assert_eq!(output.stdout, b"A\nB\n"); } #[test] fn accepted_output_is_flushed_even_when_the_module_fails() { let (_dir, child) = spawn( r#" import { stdout } from "avril:std"; const writer = stdout.getWriter(); const bytes = new Uint8Array(8 * 128 * 1024); for (let value = 65; value < 73; value++) { bytes.fill(value, (value - 65) * 128 * 1024, (value - 64) * 128 * 1024); } writer.write(bytes); throw new Error("after accepting output"); "#, ); let output = wait(child); let expected: Vec<_> = (65..73) .flat_map(|byte| std::iter::repeat_n(byte, 128 * 1024)) .collect(); assert!(!output.status.success()); assert_eq!(output.stdout, expected); assert!(String::from_utf8_lossy(&output.stderr).contains("after accepting output")); }