char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
b7bd857
main
1mod common; 2 3use std:: fs; 4use std:: process::{ Command , Stdio }; 5 6fn run ( main : & str , worker : & str ) -> std:: process:: Output { 7let dir = tempfile:: tempdir (). unwrap (); 8 fs:: write ( dir. path (). join ( "main.js" ), main). unwrap (); 9 fs:: write ( dir. path (). join ( "worker.ts" ), worker). unwrap (); 10let child =Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 11. arg ( "run" ) 12. arg ( dir. path (). join ( "main.js" )) 13. stdout ( Stdio :: piped ()) 14. stderr ( Stdio :: piped ()) 15. spawn () 16. unwrap (); 17let output = common:: wait ( child); 18assert! ( 19 output. status . success (), 20"{}" , 21String :: from_utf8_lossy ( & output. stderr ) 22); 23 output 24} 25 26# [ cfg ( unix )] 27# [ test ] 28fn worker_callbacks_can_run_synchronous_processes_without_pumping_js () { 29let output =run ( 30r#" 31const worker = new Worker('./worker.ts', {type: 'module'}); 32const result = new Promise((resolve, reject) => { 33worker.onmessage = event => resolve(event.data); 34worker.onerror = reject; 35}); 36worker.postMessage('run'); 37console.log(await result); 38"# , 39r#" 40import {runSync} from 'avril:std'; 41onmessage = async () => { 42await new Promise(resolve => setTimeout(resolve, 1)); 43let pumped = false; 44queueMicrotask(() => pumped = true); 45const result = runSync('/bin/sh', ['-c', 'cat; printf stderr >&2'], { 46input: 'worker sync', encoding: 'utf-8', 47}); 48if (pumped) throw Error('runSync pumped JS'); 49if (result.stderr !== 'stderr') throw Error('Missing stderr'); 50postMessage(result.stdout); 51close(); 52}; 53"# , 54); 55assert_eq! ( output. stdout , b"worker sync\n" ); 56} 57 58# [ test ] 59fn isolates_share_memory_and_clone_graphs_and_transfer_buffers () { 60let output =run ( 61r#" 62globalThis.parentOnly = true; 63const worker = new Worker(new URL('./worker.ts', import.meta.url), {type: 'module', name: 'test'}); 64const shared = new Int32Array(new SharedArrayBuffer(4)); 65const buffer = new Uint8Array([3, 4]); 66const graph = {shared, buffer}; graph.self = graph; 67const answer = new Promise((resolve, reject) => {worker.onmessage = e => resolve(e.data); worker.onerror = reject;}); 68worker.postMessage(graph, [buffer.buffer]); 69if (!buffer.buffer.detached) throw Error('Not transferred'); 70if (Atomics.wait(shared, 0, 0, 2000) === 'timed-out') throw Error('No parallel worker'); 71const result = await answer; 72if (result.self !== result || result.buffer[1] !== 4 || result.isolated !== true || result.name !== 'test') throw Error('Bad graph'); 73console.log('isolate and memory'); 74"# , 75r#" 76const typed: number = 42; 77onmessage = ({data}) => { 78if (data.self !== data || typed !== 42) throw Error('Graph or TS'); 79data.isolated = globalThis.parentOnly === undefined; 80data.name = name; 81Atomics.store(data.shared, 0, 1); Atomics.notify(data.shared, 0); 82postMessage(data, [data.buffer.buffer]); 83close(); 84}; 85"# , 86); 87assert_eq! ( output. stdout , b"isolate and memory\n" ); 88} 89 90# [ test ] 91fn ports_move_queued_messages_across_isolates_and_back () { 92let output =run ( 93r#" 94const worker = new Worker('./worker.ts', {type: 'module'}); 95const channel = new MessageChannel(); 96channel.port1.postMessage('queued'); 97const reply = new Promise(resolve => channel.port1.onmessage = e => resolve(e.data)); 98const returned = new Promise(resolve => worker.onmessage = e => resolve(e)); 99worker.postMessage({port: channel.port2}, [channel.port2]); 100if (await reply !== 'reply:queued') throw Error('Port reply'); 101const event = await returned; 102if (event.data.port !== event.ports[0]) throw Error('Port graph identity'); 103event.data.port.onmessage = e => { 104if (e.data !== 'back') throw Error('Retransferred endpoint'); 105console.log('ports'); worker.terminate(); 106}; 107channel.port1.postMessage('back'); 108"# , 109r#" 110onmessage = event => { 111if (event.data.port !== event.ports[0]) throw Error('Port identity'); 112const port = event.data.port; 113port.onmessage = ({data}) => { 114port.postMessage('reply:' + data); 115postMessage({port}, [port]); 116}; 117}; 118"# , 119); 120assert_eq! ( output. stdout , b"ports\n" ); 121} 122 123# [ test ] 124fn streams_cross_isolates_with_backpressure_errors_and_cancellation () { 125let output =run ( 126r#" 127const worker = new Worker('./worker.ts', {type: 'module'}); 128let pulls = 0, canceled, written = []; 129const readable = new ReadableStream({pull(c) {pulls++; c.enqueue({value: pulls});}, cancel(reason) {canceled = reason;}}, {highWaterMark: 0}); 130const writable = new WritableStream({write(chunk) {written.push(chunk);}}); 131const transform = new TransformStream({transform(value, c) {c.enqueue(value * 2);}}); 132const failed = new ReadableStream({pull() {throw {failure: true};}}); 133const answer = new Promise((resolve, reject) => {worker.onmessage = e => resolve(e.data); worker.onerror = reject;}); 134worker.postMessage({readable, writable, transform, failed}, [readable, writable, transform, failed]); 135if (!readable.locked || !writable.locked || !transform.readable.locked || !transform.writable.locked) throw Error('Source locks'); 136const result = await answer; 137if (result !== 12 || pulls !== 1 || !canceled.stop || written[0].value !== 1) throw Error('Stream protocol'); 138console.log('streams'); worker.terminate(); 139"# , 140r#" 141onmessage = async ({data: {readable, writable, transform, failed}}) => { 142const reader = readable.getReader(), writer = writable.getWriter(); 143await writer.write((await reader.read()).value); 144await writer.close(); await reader.cancel({stop: true}); 145try { await failed.getReader().read(); throw Error('Missing failure'); } 146catch (error) { if (!error.failure) throw error; } 147const tr = transform.readable.getReader(), tw = transform.writable.getWriter(); 148const write = tw.write(6); 149const value = (await tr.read()).value; await write; 150await tw.close(); if (!(await tr.read()).done) throw Error('Transform EOF'); 151postMessage(value); 152}; 153"# , 154); 155assert_eq! ( output. stdout , b"streams\n" ); 156} 157 158# [ test ] 159fn errors_and_cpu_bound_termination_do_not_stall_the_parent () { 160let output =run ( 161r#" 162const worker = new Worker('./worker.ts', {type: 'module'}); 163await new Promise(resolve => worker.onerror = event => { 164if (!event.message.includes('worker failure')) throw Error('Error detail'); 165event.preventDefault(); resolve(); 166}); 167const spinning = new Worker('./worker.ts', {type: 'module', name: 'spin'}); 168await new Promise(resolve => spinning.onmessage = resolve); 169spinning.terminate(); spinning.terminate(); 170console.log('lifecycle'); 171"# , 172r#" 173if (name === 'spin') {postMessage('started'); while (true) {}} 174throw Error('worker failure'); 175"# , 176); 177assert_eq! ( output. stdout , b"lifecycle\n" ); 178} 179 180# [ test ] 181fn worker_capabilities_are_independent_snapshots () { 182let output =run ( 183r#" 184import {avril, pledge} from 'avril:std'; 185const worker = new Worker('./worker.ts', {type: 'module', [avril]: {caps: {env: false}}}); 186await new Promise(resolve => worker.onmessage = resolve); 187const result = new Promise(resolve => worker.onmessage = e => resolve(e.data)); 188worker.postMessage('test'); 189if (await result !== 'PermissionDenied') throw Error('Worker restriction'); 190worker.terminate(); 191const live = new Worker('./worker.ts', {type: 'module'}); 192await new Promise(resolve => live.onmessage = resolve); 193// Existing workers keep their snapshot; later ones inherit the pledge. 194pledge({env: false}); 195const stillAllowed = new Promise(resolve => live.onmessage = e => resolve(e.data)); 196live.postMessage('test'); 197if (await stillAllowed !== 'allowed') throw Error('Existing worker revoked'); 198const narrowed = new Worker('./worker.ts', {type: 'module'}); 199await new Promise(resolve => narrowed.onmessage = resolve); 200const denied = new Promise(resolve => narrowed.onmessage = e => resolve(e.data)); 201narrowed.postMessage('test'); 202if (await denied !== 'PermissionDenied') throw Error('Later worker escaped pledge'); 203live.terminate(); narrowed.terminate(); console.log('caps'); 204"# , 205r#" 206import {avril, env} from 'avril:std'; 207if (avril !== Symbol.for('runtime:avril')) throw Error('Worker avril symbol'); 208postMessage('ready'); 209onmessage = () => { 210try {env.get('PATH'); postMessage('allowed');} 211catch (error) {postMessage(error.name);} 212}; 213"# , 214); 215assert_eq! ( output. stdout , b"caps\n" ); 216} 217 218# [ test ] 219fn termination_cancels_top_level_await_and_nested_workers () { 220let output =run ( 221r#" 222for (const name of ['await', 'nested', 'atomics']) { 223const worker = new Worker('./worker.ts', {type: 'module', name}); 224await new Promise(resolve => worker.onmessage = resolve); 225worker.terminate(); 226} 227console.log('canceled'); 228"# , 229r#" 230if (location.href !== import.meta.url || location.toString() !== import.meta.url) throw Error('Worker location'); 231if (name === 'atomics') { 232postMessage('ready'); 233Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0); 234} else if (name === 'nested') { 235const worker = new Worker('./worker.ts', {type: 'module', name: 'await'}); 236worker.onmessage = () => postMessage('ready'); 237} else { 238postMessage('ready'); 239await new Promise(resolve => setTimeout(resolve, 1000000)); 240} 241"# , 242); 243assert_eq! ( output. stdout , b"canceled\n" ); 244} 245 246# [ test ] 247fn host_objects_and_wasm_modules_clone_between_isolates () { 248let output =run ( 249r#" 250const worker = new Worker('./worker.ts', {type: 'module'}); 251const key = await crypto.subtle.generateKey({name: 'HMAC', hash: 'SHA-256'}, false, ['sign']); 252const module = new WebAssembly.Module(new Uint8Array([0,97,115,109,1,0,0,0])); 253const file = new File(['contents'], 'test.txt', {type: 'text/plain', lastModified: 42}); 254const error = new DOMException('message', 'AbortError'); 255const answer = new Promise(resolve => worker.onmessage = e => resolve(e.data)); 256try {worker.postMessage({key, fail(){}}); throw Error('Accepted function');} 257catch (error) {if (error.name !== 'DataCloneError') throw error;} 258worker.postMessage({key, again: key, module, file, error}); 259const result = await answer; 260if (!(result.key instanceof CryptoKey) || result.key.extractable || result.key !== result.again || 261!(result.module instanceof WebAssembly.Module) || !(result.file instanceof File) || 262result.file.name !== 'test.txt' || result.file.lastModified !== 42 || await result.file.text() !== 'contents' || 263!(result.error instanceof DOMException) || result.error.name !== 'AbortError') throw Error('Host clone'); 264const local = new Uint8Array(await crypto.subtle.sign('HMAC', key, new Uint8Array([1]))); 265if (local.join() !== result.signature.join()) throw Error('Key material'); 266try {await crypto.subtle.exportKey('raw', result.key); throw Error('Nonextractable export');} 267catch (error) {if (error.name !== 'InvalidAccessError') throw error;} 268console.log('hosts'); worker.terminate(); 269"# , 270r#" 271onmessage = async ({data}) => { 272if (!(data.key instanceof CryptoKey) || data.key.extractable || data.key !== data.again || 273!(new WebAssembly.Instance(data.module) instanceof WebAssembly.Instance) || 274await data.file.text() !== 'contents' || data.error.message !== 'message') throw Error('Worker host clone'); 275try {await crypto.subtle.exportKey('raw', data.key); throw Error('Nonextractable worker export');} 276catch (error) {if (error.name !== 'InvalidAccessError') throw error;} 277data.signature = new Uint8Array(await crypto.subtle.sign('HMAC', data.key, new Uint8Array([1]))); 278postMessage(data); 279}; 280"# , 281); 282assert_eq! ( output. stdout , b"hosts\n" ); 283} 284 285# [ test ] 286fn failed_stream_transfers_are_recoverable_and_pending_reads_cancel () { 287let output =run ( 288r#" 289const worker = new Worker('./worker.ts', {type: 'module'}); 290let canceled; 291const source = new ReadableStream({cancel(reason) {canceled = reason;}}, {highWaterMark: 0}); 292const buffer = new ArrayBuffer(4); 293for (const transfer of [[source, buffer], [source, source]]) { 294try {worker.postMessage({source, fail() {}}, transfer); throw Error('Clone succeeded');} 295catch (error) {if (error.name !== 'DataCloneError') throw error;} 296} 297if (source.locked || buffer.detached) throw Error('Failed transfer committed'); 298const answer = new Promise(resolve => worker.onmessage = e => resolve(e.data)); 299worker.postMessage(source, [source]); 300if (await answer !== 'canceled' || canceled.stop !== true) throw Error('Pending read cancellation'); 301console.log('stream rollback'); worker.terminate(); 302"# , 303r#" 304onmessage = async ({data: stream}) => { 305const reader = stream.getReader(); 306const reading = reader.read(); 307await reader.cancel({stop: true}); 308if (!(await reading).done) throw Error('Pending read'); 309postMessage('canceled'); 310}; 311"# , 312); 313assert_eq! ( output. stdout , b"stream rollback\n" ); 314} 315 316# [ test ] 317fn terminating_a_stream_owner_rejects_pending_operations () { 318let output =run ( 319r#" 320const worker = new Worker('./worker.ts', {type: 'module'}); 321const streams = await new Promise(resolve => worker.onmessage = e => resolve(e.data)); 322const reader = streams.readable.getReader(), writer = streams.writable.getWriter(); 323const reading = reader.read().catch(error => error.name); 324const writing = writer.write(1).catch(error => error.name); 325const closed = writer.closed.catch(error => error.name); 326worker.terminate(); 327if (await reading !== 'DataCloneError' || await writing !== 'DataCloneError' || await closed !== 'DataCloneError') throw Error('Unsettled stream operations'); 328console.log('disconnected'); 329"# , 330r#" 331const readable = new ReadableStream({}, {highWaterMark: 0}); 332const writable = new WritableStream({write() {return new Promise(() => {});}}); 333postMessage({readable, writable}, [readable, writable]); 334"# , 335); 336assert_eq! ( output. stdout , b"disconnected\n" ); 337} 338 339# [ test ] 340fn environment_snapshots_and_child_pledges_do_not_mutate_the_parent () { 341let output =run ( 342r#" 343import {env} from 'avril:std'; 344env.set('AVRIL_WORKER_TEST', 'parent'); 345const worker = new Worker('./worker.ts', {type: 'module'}); 346const result = await new Promise(resolve => worker.onmessage = e => resolve(e.data)); 347if (result !== 'parent' || env.get('AVRIL_WORKER_TEST') !== 'parent') throw Error('Environment isolation'); 348console.log('environment'); 349"# , 350r#" 351import {env, pledge} from 'avril:std'; 352const value = env.get('AVRIL_WORKER_TEST'); 353env.set('AVRIL_WORKER_TEST', 'child'); 354pledge({env: false}); 355postMessage(value); close(); 356"# , 357); 358assert_eq! ( output. stdout , b"environment\n" ); 359} 360 361# [ test ] 362fn closed_writable_streams_preserve_their_terminal_state () { 363let output =run ( 364r#" 365const worker = new Worker('./worker.ts', {type: 'module'}); 366const stream = new WritableStream(); 367await stream.close(); 368const result = new Promise(resolve => worker.onmessage = e => resolve(e.data)); 369worker.postMessage(stream, [stream]); 370if (await result !== 'closed') throw Error('Closed stream transfer'); 371console.log('closed stream'); worker.terminate(); 372"# , 373r#" 374onmessage = async ({data: stream}) => { 375const writer = stream.getWriter(); 376await writer.closed; 377try {await writer.write(1); throw Error('Write to closed stream');} 378catch (error) {if (!(error instanceof TypeError)) throw error;} 379postMessage('closed'); 380}; 381"# , 382); 383assert_eq! ( output. stdout , b"closed stream\n" ); 384} 385 386# [ test ] 387fn background_wasm_compilation_wakes_an_otherwise_idle_worker () { 388let output =run ( 389r#" 390const worker = new Worker('./worker.ts', {type: 'module'}); 391const value = await new Promise(resolve => worker.onmessage = e => resolve(e.data)); 392if (value !== 42) throw Error('Background compilation'); 393console.log('compiled'); 394"# , 395r#" 396function unsigned(value) { 397const result = []; 398do { 399const byte = value & 127; value >>>= 7; 400result.push(byte | (value ? 128 : 0)); 401} while (value); 402return result; 403} 404const body = [0, ...new Uint8Array(250000).fill(1), 0x41, 42, 0x0b]; 405const code = [1, ...unsigned(body.length), ...body]; 406const bytes = new Uint8Array([ 4070,97,115,109,1,0,0,0, 4081,5,1,0x60,0,1,0x7f, 4093,2,1,0, 4107,5,1,1,102,0,0, 41110,...unsigned(code.length),...code, 412]); 413const module = await WebAssembly.compile(bytes); 414postMessage(new WebAssembly.Instance(module).exports.f()); close(); 415"# , 416); 417assert_eq! ( output. stdout , b"compiled\n" ); 418} 419 420# [ test ] 421fn worker_and_stream_glue_uses_captured_intrinsics () { 422let output =run ( 423r#" 424const worker = new Worker('./worker.ts', {type: 'module'}); 425const getData = Object.getOwnPropertyDescriptor(MessageEvent.prototype, 'data').get; 426const source = new ReadableStream({start(c) {c.enqueue(42); c.close();}}); 427const answer = new Promise(resolve => worker.onmessage = event => resolve(getData.call(event))); 428worker.postMessage(source, [source]); 429Array.prototype[Symbol.iterator] = () => {throw Error('Guest iterator');}; 430Object.defineProperty(MessageEvent.prototype, 'data', {get() {throw Error('Guest message getter');}}); 431MessagePort.prototype.postMessage = () => {throw Error('Guest port method');}; 432Promise.prototype.then = () => {throw Error('Guest then');}; 433if (await answer !== 42) throw Error('Hardened delivery'); 434console.log('intrinsics'); 435"# , 436r#" 437const getData = Object.getOwnPropertyDescriptor(MessageEvent.prototype, 'data').get; 438onmessage = async event => { 439const reader = getData.call(event).getReader(); 440postMessage((await reader.read()).value); close(); 441}; 442Array.prototype[Symbol.iterator] = () => {throw Error('Guest iterator');}; 443Object.defineProperty(MessageEvent.prototype, 'data', {get() {throw Error('Guest message getter');}}); 444MessagePort.prototype.postMessage = () => {throw Error('Guest port method');}; 445Promise.prototype.then = () => {throw Error('Guest then');}; 446"# , 447); 448assert_eq! ( output. stdout , b"intrinsics\n" ); 449} 450 451# [ test ] 452fn construction_preserves_option_reads_subclasses_and_location_shape () { 453let output =run ( 454r#" 455import {avril} from 'avril:std'; 456const reads = []; 457const caps = {get env() {reads.push('env'); return false;}}; 458const extension = {get caps() {reads.push('caps'); return caps;}}; 459const options = { 460get type() {reads.push('type'); return 'module';}, 461get name() {reads.push('name'); return 'named';}, 462get [avril]() {reads.push('extension'); return extension;}, 463}; 464class Child extends Worker {} 465const worker = new Child({ 466toString() {reads.push('script'); return './worker.ts';}, 467}, options); 468if (!(worker instanceof Child) || !(worker instanceof EventTarget)) throw Error('Subclass identity'); 469if (Object.getOwnPropertyNames(worker).length !== 0) throw Error('Leaked worker state'); 470if (reads.join(',') !== 'script,type,type,name,name,extension,caps,caps,env') throw Error(reads); 471if (Worker.length !== 1 || Worker.prototype.postMessage.length !== 1 || 472Worker.prototype.terminate.length !== 0) throw Error('Worker arity'); 473const result = await new Promise((resolve, reject) => { 474worker.onmessage = event => resolve(event.data); 475worker.onerror = reject; 476}); 477if (result.name !== 'named' || result.href !== new URL('./worker.ts', import.meta.url).href) 478throw Error('Worker metadata'); 479worker.terminate(); 480console.log('construction'); 481"# , 482r#" 483if (Object.getOwnPropertyNames(location).length !== 0) throw Error('Leaked location state'); 484if (location.toString() !== location.href || location.toString.length !== 0) 485throw Error('Location conversion'); 486const href = Object.getOwnPropertyDescriptor(WorkerLocation.prototype, 'href'); 487if (href.set !== undefined || href.get.length !== 0) throw Error('Location descriptor'); 488try {new WorkerLocation(); throw Error('Constructed location');} 489catch (error) {if (!(error instanceof TypeError)) throw error;} 490postMessage({name, href: location.href}); 491close(); 492"# , 493); 494assert_eq! ( output. stdout , b"construction\n" ); 495} 496 497# [ test ] 498fn classic_workers_are_rejected_and_execution_restrictions_apply_before_loading () { 499let output =run ( 500r#" 501import {avril} from 'avril:std'; 502for (const options of [undefined, {type: 'classic'}]) { 503try {new Worker('./worker.ts', options); throw Error('Classic worker accepted');} 504catch (error) {if (!(error instanceof TypeError)) throw error;} 505} 506const worker = new Worker('./worker.ts', { 507type: 'module', 508[avril]: {caps: {jsExec: false}}, 509}); 510await new Promise(resolve => worker.onerror = event => { 511if (!event.message.includes('PermissionDenied')) throw Error('Execution authority'); 512event.preventDefault(); resolve(); 513}); 514console.log('entry caps'); 515"# , 516"throw Error('Unauthorized code executed');" , 517); 518assert_eq! ( output. stdout , b"entry caps\n" ); 519}