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