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# [ path = "common/http.rs" ] 3# [ allow ( dead_code )] 4mod http_fixture; 5 6use std:: fs; 7use std:: io:: Write ; 8use std:: path:: Path ; 9use std:: process::{ Command , Output , Stdio }; 10 11const PRINT_ARGS : & str =r#" 12import {process} from 'avril:std'; 13console.log(JSON.stringify(process.args)); 14"# ; 15 16fn run ( dir : & Path , args : & [ & str ]) ->Output { 17let child =Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 18. current_dir ( dir) 19. args ( args) 20. stdin ( Stdio :: null ()) 21. stdout ( Stdio :: piped ()) 22. stderr ( Stdio :: piped ()) 23. spawn () 24. unwrap (); 25 common:: wait ( child) 26} 27 28fn repl ( dir : & Path , args : & [ & str ], source : & str ) ->Output { 29let mut child =Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 30. current_dir ( dir) 31. args ( args) 32. stdin ( Stdio :: piped ()) 33. stdout ( Stdio :: piped ()) 34. stderr ( Stdio :: piped ()) 35. spawn () 36. unwrap (); 37 child 38. stdin 39. take () 40. unwrap () 41. write_all ( source. as_bytes ()) 42. unwrap (); 43 common:: wait ( child) 44} 45 46fn success ( output : & Output ) { 47assert! ( 48 output. status . success (), 49"{}" , 50String :: from_utf8_lossy ( & output. stderr ) 51); 52} 53 54fn assert_args ( output : & Output , expected : & [ & str ]) { 55assert_eq! ( 56 serde_json:: from_slice ::< Vec < String >>( & output. stdout ). unwrap (), 57 expected 58); 59} 60 61# [ test ] 62fn script_arguments_are_verbatim_frozen_and_available_without_capabilities () { 63let dir = tempfile:: tempdir (). unwrap (); 64 fs:: write ( dir. path (). join ( "main.js" ), r#" 65import {process} from 'avril:std'; 66if (!Array.isArray(process.args) || !Object.isFrozen(process.args)) throw Error('Not a frozen array'); 67if (process.args !== (await import('avril:std')).process.args) throw Error('Unstable identity'); 68const before = JSON.stringify(process.args); 69for (const mutate of [ 70() => process.args.push('extra'), 71() => process.args[0] = 'changed', 72() => process.args.length = 0, 73() => process.args = [], 74]) { 75let denied = false; 76try { mutate(); } catch (error) { if (!(error instanceof TypeError)) throw error; denied = true; } 77if (!denied) throw Error('Mutation accepted'); 78} 79if (JSON.stringify(process.args) !== before) throw Error('Arguments changed'); 80console.log(before); 81"# ). unwrap (); 82let args =[ 83"" , 84"two words" , 85"héllo 🫐" , 86"a\nb" , 87"'quotes'\"" , 88"*" , 89"$HOME" , 90"--help" , 91"-h" , 92"-0" , 93"--allow-read=/" , 94"--deny-read=/" , 95"--offline" , 96"--env-file" , 97"missing.env" , 98"--env-file=missing.env" , 99"--unknown" , 100"--" , 101"--" , 102"other.js" , 103]; 104let mut cli =vec! [ "run" , "-0" , "main.js" ]; 105 cli. extend ( args); 106let output =run ( dir. path (), & cli); 107success ( & output); 108assert_args ( & output, & args); 109} 110 111# [ test ] 112fn no_arguments_means_an_empty_array_and_a_trailing_separator_is_preserved () { 113let dir = tempfile:: tempdir (). unwrap (); 114 fs:: write ( dir. path (). join ( "main.js" ), PRINT_ARGS ). unwrap (); 115for cliin [ & [ "run" , "main.js" ][ ..], & [ "run" , "--" , "main.js" ]] { 116let output =run ( dir. path (), cli); 117success ( & output); 118assert_args ( & output, & []); 119} 120let output =run ( dir. path (), & [ "run" , "main.js" , "--" ]); 121success ( & output); 122assert_args ( & output, & [ "--" ]); 123} 124 125# [ test ] 126fn argument_flags_cannot_grant_or_revoke_capabilities () { 127let dir = tempfile:: tempdir (). unwrap (); 128 fs:: write ( dir. path (). join ( "secret" ), "secret" ). unwrap (); 129 fs:: write ( dir. path (). join ( "main.js" ), r#" 130import {readTextFile} from 'avril:fs'; 131import {errors} from 'avril:std'; 132try { console.log(await readTextFile('secret')); } 133catch (error) { if (!(error instanceof errors.PermissionDenied)) throw error; console.log('denied'); } 134"# ). unwrap (); 135let output =run ( dir. path (), & [ "run" , "-0" , "main.js" , "--allow-read=." ]); 136success ( & output); 137assert_eq! ( output. stdout , b"denied\n" ); 138let output =run ( dir. path (), & [ "run" , "main.js" , "-0" , "--deny-read=." ]); 139success ( & output); 140assert_eq! ( output. stdout , b"secret\n" ); 141} 142 143# [ test ] 144fn environment_file_values_are_not_mistaken_for_the_entrypoint () { 145let dir = tempfile:: tempdir (). unwrap (); 146 fs:: write ( 147 dir. path (). join ( "settings.env" ), 148"AVRIL_ARGS_FIXTURE=loaded\n" , 149) 150. unwrap (); 151 fs:: write ( 152 dir. path (). join ( "main.js" ), 153r#" 154import {env, process} from 'avril:std'; 155if (env.get('AVRIL_ARGS_FIXTURE') !== 'loaded') throw Error('Environment file not loaded'); 156console.log(JSON.stringify(process.args)); 157"# , 158) 159. unwrap (); 160for optionin [ 161& [ "--env-file" , "settings.env" ][ ..], 162& [ "--env-file=settings.env" ], 163] { 164let mut cli =vec! [ "run" , "-0" , "--allow-env=AVRIL_ARGS_FIXTURE" ]; 165 cli. extend ( option); 166 cli. extend ([ "main.js" , "--help" , "-0" , "--env-file=missing.env" ]); 167let output =run ( dir. path (), & cli); 168success ( & output); 169assert_args ( & output, & [ "--help" , "-0" , "--env-file=missing.env" ]); 170 cli. insert ( cli. len () -4 , "--help" ); 171let help =run ( dir. path (), & cli); 172success ( & help); 173assert! ( String :: from_utf8_lossy ( & help. stdout ). contains ( "Usage: avril run" )); 174} 175} 176 177# [ test ] 178fn a_separator_before_the_entry_allows_flag_shaped_filenames () { 179let dir = tempfile:: tempdir (). unwrap (); 180for namein [ "--help" , "-0" ] { 181 fs:: write ( dir. path (). join ( name), PRINT_ARGS ). unwrap (); 182let output =run ( dir. path (), & [ "run" , "--" , name, "--help" , "--" , "-0" ]); 183success ( & output); 184assert_args ( & output, & [ "--help" , "--" , "-0" ]); 185} 186} 187 188# [ test ] 189fn remote_entrypoints_get_arguments_without_consuming_their_offline_flag () { 190let dir = tempfile:: tempdir (). unwrap (); 191let http = http_fixture:: Http :: new (); 192 http. source ( "main.js" , PRINT_ARGS ); 193let output =run ( 194 dir. path (), 195& [ 196"run" , 197"-0" , 198"--cache-dir=cache" , 199& http. name ( "main.js" ), 200"--offline" , 201"--help" , 202], 203); 204success ( & output); 205assert_args ( & output, & [ "--offline" , "--help" ]); 206} 207 208# [ test ] 209fn serve_passes_arguments_without_parsing_them_as_listener_options () { 210let dir = tempfile:: tempdir (). unwrap (); 211 fs:: write ( 212 dir. path (). join ( "main.js" ), 213format! ( "{PRINT_ARGS}\nthrow Error('Stopped before listening');" ), 214) 215. unwrap (); 216let args =[ 217"--port=not-a-port" , 218"--unix=not-a-socket" , 219"--help" , 220"--env-file=missing.env" , 221]; 222let mut cli =vec! [ "serve" , "-0" , "--port=0" , "main.js" ]; 223 cli. extend ( args); 224let output =run ( dir. path (), & cli); 225assert! ( !output. status . success ()); 226assert! ( String :: from_utf8_lossy ( & output. stderr ). contains ( "Stopped before listening" )); 227assert_args ( & output, & args); 228} 229 230# [ test ] 231fn workers_and_nested_workers_inherit_frozen_arguments () { 232let dir = tempfile:: tempdir (). unwrap (); 233 fs:: write ( dir. path (). join ( "main.js" ), r#" 234import {process, avril} from 'avril:std'; 235const worker = new Worker(new URL('./worker.js', import.meta.url), { 236type: 'module', [avril]: {caps: {env: false, sys: false}}, 237}); 238worker.onerror = event => { throw Error(event.message); }; 239worker.onmessage = ({data}) => { 240if (JSON.stringify(data) !== JSON.stringify(process.args)) throw Error('Arguments differ'); 241console.log(JSON.stringify(data)); 242worker.terminate(); 243}; 244"# ). unwrap (); 245 fs:: write ( dir. path (). join ( "worker.js" ), r#" 246import {process} from 'avril:std'; 247if (!Object.isFrozen(process.args)) throw Error('Mutable worker arguments'); 248const worker = new Worker(new URL('./nested.js', import.meta.url), {type: 'module'}); 249worker.onerror = event => { throw Error(event.message); }; 250worker.onmessage = ({data}) => { 251if (JSON.stringify(data) !== JSON.stringify(process.args)) throw Error('Arguments differ'); 252postMessage(data); 253close(); 254}; 255"# ). unwrap (); 256 fs:: write ( 257 dir. path (). join ( "nested.js" ), 258r#" 259import {process} from 'avril:std'; 260if (!Object.isFrozen(process.args)) throw Error('Mutable nested arguments'); 261postMessage(process.args); 262close(); 263"# , 264) 265. unwrap (); 266let output =run ( 267 dir. path (), 268& [ 269"run" , 270"-0" , 271"--allow-js-exec=." , 272"main.js" , 273"" , 274"--help" , 275"hello" , 276], 277); 278success ( & output); 279assert_args ( & output, & [ "" , "--help" , "hello" ]); 280} 281 282# [ test ] 283fn repl_and_test_sessions_default_to_empty_script_arguments () { 284let dir = tempfile:: tempdir (). unwrap (); 285for argsin [ & [ "repl" , "-0" ][ ..], & [ "repl" , "-0" , "--" ], & [ "--" ]] { 286let output =repl ( 287 dir. path (), 288 args, 289"(await import('avril:std')).process.args\n" , 290); 291success ( & output); 292assert_eq! ( output. stdout , b"[]\n" ); 293} 294 fs:: write ( dir. path (). join ( "args_test.js" ), r#" 295import {process} from 'avril:std'; 296import {test} from 'avril:test'; 297test('empty args', () => { 298if (process.args.length !== 0 || !Object.isFrozen(process.args)) throw Error('Unexpected args'); 299}); 300"# ). unwrap (); 301for argsin [ 302& [ "test" , "-0" , "args_test.js" , "--filter=empty args" ][ ..], 303& [ "test" , "-0" , "args_test.js" , "--filter=empty args" , "--" ], 304] { 305let output =run ( dir. path (), args); 306success ( & output); 307assert! ( String :: from_utf8_lossy ( & output. stdout ). contains ( "ok - empty args" )); 308} 309} 310 311# [ test ] 312fn explicit_and_default_repls_preserve_arguments_after_the_separator () { 313let dir = tempfile:: tempdir (). unwrap (); 314let args =[ 315"" , 316"two words" , 317"héllo 🫐" , 318"a\nb" , 319"--help" , 320"-h" , 321"-0" , 322"--allow-read=." , 323"--env-file=missing.env" , 324"--unknown" , 325"--" , 326"--" , 327]; 328let expected = serde_json:: to_string ( & args). unwrap (); 329let source =format! ( 330"const args = (await import('avril:std')).process.args; Object.isFrozen(args) && JSON.stringify(args) === JSON.stringify({expected})\nargs === (await import('avril:std')).process.args\n" 331); 332for prefixin [ & [ "repl" , "-0" ][ ..], & [ "-0" ], & [ "repl" ], & []] { 333let mut cli = prefix. to_vec (); 334 cli. push ( "--" ); 335 cli. extend ( args); 336let output =repl ( dir. path (), & cli, & source); 337success ( & output); 338assert_eq! ( output. stdout , b"true\ntrue\n" ); 339} 340} 341 342# [ test ] 343fn test_targets_and_runner_options_are_separate_from_shared_script_arguments () { 344let dir = tempfile:: tempdir (). unwrap (); 345let args =[ 346"" , 347"héllo 🫐" , 348"a\nb" , 349"--help" , 350"-0" , 351"--no-run" , 352"--filter=other" , 353"--env-file=missing.env" , 354"--" , 355"not_a_target.js" , 356]; 357let expected = serde_json:: to_string ( & args). unwrap (); 358let source =format! ( 359r#" 360import {{process, env}} from 'avril:std'; 361import {{test}} from 'avril:test'; 362if (env.get('AVRIL_ARGS_FIXTURE') !== 'loaded') throw Error('Environment not loaded'); 363if (!Object.isFrozen(process.args) || JSON.stringify(process.args) !== JSON.stringify({expected})) 364throw Error('Wrong arguments during collection'); 365test('chosen', () => {{ 366if (JSON.stringify(process.args) !== JSON.stringify({expected})) throw Error('Wrong arguments during test'); 367}}); 368test('filtered', () => {{ throw Error('Filter ignored'); }}); 369"# 370); 371for namein [ "a_test.js" , "b_test.js" ] { 372 fs:: write ( dir. path (). join ( name), & source). unwrap (); 373} 374 fs:: write ( 375 dir. path (). join ( "settings.env" ), 376"AVRIL_ARGS_FIXTURE=loaded\n" , 377) 378. unwrap (); 379for targetsin [ 380& [ 381"a_test.js" , 382"b_test.js" , 383"--filter" , 384"chosen" , 385"--env-file" , 386"settings.env" , 387][ ..], 388& [ "--filter=chosen" , "--env-file=settings.env" ], 389] { 390let mut cli =vec! [ "test" , "-0" , "--allow-env=AVRIL_ARGS_FIXTURE" ]; 391 cli. extend ( targets); 392 cli. push ( "--" ); 393 cli. extend ( args); 394let output =run ( dir. path (), & cli); 395success ( & output); 396assert_eq! ( 397String :: from_utf8_lossy ( & output. stdout ) 398. matches ( "ok - chosen" ) 399. count (), 4002 401); 402} 403} 404 405# [ test ] 406fn repl_and_test_argument_flags_cannot_change_authority () { 407let dir = tempfile:: tempdir (). unwrap (); 408for restrictedin [ true , false ] { 409let check =format! ( 410r#" 411const {{process, errors}} = await import('avril:std'); 412let allowed = true; 413try {{ process.cwd(); }} 414catch (error) {{ if (!(error instanceof errors.PermissionDenied)) throw error; allowed = false; }} 415if (allowed !== {}) throw Error('Arguments changed authority'); 416return true; 417"# , 418 !restricted 419); 420let guest =if restricted{ 421& [ "--allow-read=." ][ ..] 422} else { 423& [ "-0" , "--deny-read=." ][ ..] 424}; 425let mut cli =vec! [ "repl" ]; 426if restricted{ 427 cli. push ( "-0" ); 428} 429 cli. push ( "--" ); 430 cli. extend ( guest); 431let source =format! ( "await (async () => {{{}}})()\n" , check. replace ( '\n' , " " )); 432let output =repl ( dir. path (), & cli, & source); 433success ( & output); 434assert_eq! ( output. stdout , b"true\n" ); 435 fs:: write ( 436 dir. path (). join ( "authority_test.js" ), 437format! ( 438"import {{test}} from 'avril:test'; test('authority', async () => {{{check}}});" 439), 440) 441. unwrap (); 442 cli =vec! [ "test" ]; 443if restricted{ 444 cli. push ( "-0" ); 445} 446 cli. extend ([ "authority_test.js" , "--" ]); 447 cli. extend ( guest); 448let output =run ( dir. path (), & cli); 449success ( & output); 450assert! ( String :: from_utf8_lossy ( & output. stdout ). contains ( "ok - authority" )); 451} 452} 453 454# [ test ] 455fn remote_tests_and_no_run_collection_receive_arguments () { 456let dir = tempfile:: tempdir (). unwrap (); 457let http = http_fixture:: Http :: new (); 458let source =r#" 459import {process} from 'avril:std'; 460import {test} from 'avril:test'; 461if (JSON.stringify(process.args) !== '["hello","--help"]') throw Error('Wrong arguments'); 462console.log('collected arguments'); 463test('remote arguments', () => { if (process.args[0] !== 'hello') throw Error('Wrong arguments'); }); 464"# ; 465 http. source ( "args_test.js" , source); 466let output =run ( 467 dir. path (), 468& [ 469"test" , 470"-0" , 471"--cache-dir=cache" , 472& http. name ( "args_test.js" ), 473"--" , 474"hello" , 475"--help" , 476], 477); 478success ( & output); 479assert! ( String :: from_utf8_lossy ( & output. stdout ). contains ( "ok - remote arguments" )); 480 fs:: write ( 481 dir. path (). join ( "args_test.js" ), 482format! ( "{source}\ntest('must not run', () => {{ throw Error('Test executed'); }});" ), 483) 484. unwrap (); 485let output =run ( 486 dir. path (), 487& [ 488"test" , 489"-0" , 490"--no-run" , 491"args_test.js" , 492"--" , 493"hello" , 494"--help" , 495], 496); 497success ( & output); 498assert! ( String :: from_utf8_lossy ( & output. stdout ). contains ( "collected arguments" )); 499} 500 501# [ test ] 502fn workers_in_repl_and_test_sessions_inherit_script_arguments () { 503let dir = tempfile:: tempdir (). unwrap (); 504 fs:: write ( 505 dir. path (). join ( "worker.js" ), 506r#" 507import {process} from 'avril:std'; 508postMessage({args: process.args, frozen: Object.isFrozen(process.args)}); 509close(); 510"# , 511) 512. unwrap (); 513let check =r#" 514const {process} = await import('avril:std'); 515const worker = new Worker('./worker.js', {type: 'module'}); 516const data = await new Promise((resolve, reject) => { 517worker.onmessage = ({data}) => resolve(data); 518worker.onerror = event => { event.preventDefault(); reject(Error(event.message)); }; 519}); 520worker.terminate(); 521if (!data.frozen || JSON.stringify(data.args) !== '["hello","--help"]' || 522JSON.stringify(data.args) !== JSON.stringify(process.args)) throw Error('Wrong worker arguments'); 523return true; 524"# ; 525let source =format! ( "await (async () => {{{}}})()\n" , check. replace ( '\n' , " " )); 526let output =repl ( 527 dir. path (), 528& [ "repl" , "-0" , "--allow-js-exec=." , "--" , "hello" , "--help" ], 529& source, 530); 531success ( & output); 532assert_eq! ( output. stdout , b"true\n" ); 533 fs:: write ( 534 dir. path (). join ( "args_test.js" ), 535format! ( 536"import {{test}} from 'avril:test'; test('worker arguments', async () => {{{check}}});" 537), 538) 539. unwrap (); 540let output =run ( 541 dir. path (), 542& [ 543"test" , 544"-0" , 545"--allow-js-exec=." , 546"args_test.js" , 547"--" , 548"hello" , 549"--help" , 550], 551); 552success ( & output); 553assert! ( String :: from_utf8_lossy ( & output. stdout ). contains ( "ok - worker arguments" )); 554} 555 556# [ test ] 557fn session_separators_preserve_runtime_help_and_option_validation () { 558let dir = tempfile:: tempdir (). unwrap (); 559for cliin [ 560& [ "repl" , "--help" , "--" , "--unknown" ][ ..], 561& [ "test" , "--help" , "--" , "--unknown" ], 562& [ "--help" , "--" , "--unknown" ], 563] { 564let output =run ( dir. path (), cli); 565success ( & output); 566let text =String :: from_utf8_lossy ( & output. stdout ); 567assert! ( 568 text. contains ( "Usage: avril" ) && text. contains ( "[-- args...]" ), 569"{text}" 570); 571} 572for ( cli, message) in [ 573( 574& [ "test" , "--filter" , "--" , "value" ][ ..], 575"Expected a pattern for --filter" , 576), 577( 578& [ "test" , "--env-file" , "--" , "value" ][ ..], 579"Expected a path for --env-file" , 580), 581( 582& [ "repl" , "--unknown" , "--" , "value" ][ ..], 583"Unknown option: --unknown" , 584), 585( 586& [ "repl" , "not-an-argument" , "--" , "value" ][ ..], 587"The REPL does not accept a module" , 588), 589] { 590let output =run ( dir. path (), cli); 591assert! ( !output. status . success ()); 592assert! ( 593String :: from_utf8_lossy ( & output. stderr ). contains ( message), 594"{output:?}" 595); 596} 597 fs:: write ( 598 dir. path (). join ( "-case.js" ), 599r#" 600import {process} from 'avril:std'; 601import {test} from 'avril:test'; 602test('-- title', () => { 603if (JSON.stringify(process.args) !== '["--","--help"]') throw Error('Wrong arguments'); 604}); 605"# , 606) 607. unwrap (); 608let output =run ( 609 dir. path (), 610& [ 611"test" , 612"-0" , 613"./-case.js" , 614"--filter=--" , 615"--" , 616"--" , 617"--help" , 618], 619); 620success ( & output); 621assert! ( String :: from_utf8_lossy ( & output. stdout ). contains ( "ok - -- title" )); 622}