mod common; #[path = "common/registry.rs"] mod fixture; use std::io::Write; use std::process::{Command, Output, Stdio}; use std::thread; use std::time::Duration; /// Resets SIGINT/SIGTERM to their defaults in the child, so results never /// depend on dispositions the test runner itself ignores. #[cfg(unix)] fn inherit_defaults(mut command: Command) -> Command { use std::os::unix::process::CommandExt; unsafe { command.pre_exec(|| { libc::signal(libc::SIGINT, libc::SIG_DFL); libc::signal(libc::SIGTERM, libc::SIG_DFL); Ok(()) }); } command } fn start_repl(directory: &std::path::Path, arguments: &[&str]) -> std::process::Child { #[cfg(unix)] let mut command = inherit_defaults(Command::new(env!("CARGO_BIN_EXE_avril"))); #[cfg(not(unix))] let mut command = Command::new(env!("CARGO_BIN_EXE_avril")); command .args(arguments) .current_dir(directory) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() .unwrap() } fn finish_repl(mut child: std::process::Child, input: &str) -> Output { child .stdin .take() .unwrap() .write_all(input.as_bytes()) .unwrap(); common::wait(child) } #[cfg(unix)] #[test] fn repl_can_run_synchronous_processes_after_await_without_pumping_js() { let directory = tempfile::tempdir().unwrap(); let output = finish_repl( start_repl(directory.path(), &["repl"]), concat!( "const {runSync} = await import('avril:std');\n", "await new Promise(resolve => setTimeout(resolve, 1)); ", "let pumped = false; queueMicrotask(() => pumped = true); ", "const result = runSync('/bin/sh', ['-c', 'cat; printf stderr >&2'], ", "{input: 'repl sync', encoding: 'utf-8'}); ", "if (pumped) throw Error('runSync pumped JS'); ", "if (result.stderr !== 'stderr') throw Error('Missing stderr'); ", "result.stdout\n.exit\n", ), ); assert!(output.status.success(), "{output:?}"); assert_eq!(output.stdout, b"undefined\n\"repl sync\"\n"); assert!(output.stderr.is_empty(), "{output:?}"); } #[test] fn repl_persists_bindings_multiline_await_and_recovers_from_errors() { let directory = tempfile::tempdir().unwrap(); let output = finish_repl( start_repl(directory.path(), &["repl", "-0"]), "await Promise.resolve(5)\nconst interval = setInterval(() => {}, 0)\nawait new Promise(resolve => setTimeout(() => { clearInterval(interval); resolve(11) }, 20))\nlet value = 6\nvalue\nfunction add(input) {\n return input + value\n}\nadd(1)\ntry { await import('ext:core/ops') } catch (error) { error.message }\nthrow Error('expected')\nconst = 1\n8\n.exit\n", ); assert!(output.status.success()); let stdout = String::from_utf8(output.stdout).unwrap(); assert_eq!( stdout, "5\nundefined\n11\nundefined\n6\nundefined\n7\n\"Extension modules are private\"\n8\n" ); let stderr = String::from_utf8(output.stderr).unwrap(); assert!(stderr.contains("expected"), "{stderr}"); assert!(stderr.contains("SyntaxError"), "{stderr}"); } #[test] fn repl_underscore_tracks_successful_results_and_preserves_them_after_errors() { let directory = tempfile::tempdir().unwrap(); let output = finish_repl( start_repl(directory.path(), &["repl", "-0"]), "_\n6 * 7\n_ + 1\nawait Promise.resolve(_ + 1)\nthrow Error('expected')\n_\nconst = 1\n_\nawait Promise.reject(Error('rejected'))\n_\nlet value = 1\n_\n.exit\n", ); assert!(output.status.success()); assert_eq!( String::from_utf8(output.stdout).unwrap(), "undefined\n42\n43\n44\n44\n44\n44\nundefined\nundefined\n" ); let stderr = String::from_utf8(output.stderr).unwrap(); for message in ["expected", "SyntaxError", "rejected"] { assert!(stderr.contains(message), "{stderr}"); } } #[test] fn repl_underscore_retains_identity_and_non_json_values() { let directory = tempfile::tempdir().unwrap(); let output = finish_repl( start_repl(directory.path(), &["repl", "-0"]), r#"const object = { answer: 42 } object _ === object const fn = () => 7 fn _ === fn && _() === 7 const symbol = Symbol('result') symbol _ === symbol 11n _ + 1n NaN Number.isNaN(_) -0 Object.is(_, -0) null _ === null void 0 _ === undefined .exit "#, ); assert!(output.status.success()); let stdout = String::from_utf8(output.stdout).unwrap(); assert!( stdout.starts_with("undefined\n{ answer: 42 }\ntrue\n"), "{stdout}" ); assert_eq!( stdout.lines().filter(|line| *line == "true").count(), 7, "{stdout}" ); assert!( stdout.contains("11n\n12n\nNaN\ntrue\n0\ntrue\nnull\ntrue\nundefined\ntrue\n"), "{stdout}" ); assert!( output.stderr.is_empty(), "{}", String::from_utf8_lossy(&output.stderr) ); } #[test] fn repl_import_declarations_support_all_binding_forms_and_persist() { let directory = tempfile::tempdir().unwrap(); std::fs::write( directory.path().join("local.js"), "globalThis.loads = (globalThis.loads ?? 0) + 1; export default 42; export let count = 1; export function bump() { count++; } const odd = 7; export { odd as 'odd-name', odd as '__proto__' };", ) .unwrap(); let output = finish_repl( start_repl(directory.path(), &["repl", "-0", "--allow-js-exec=."]), r#"import answer from './local.js' answer import { count as snapshot, bump, 'odd-name' as odd, '__proto__' as proto } from './local.js' [snapshot, odd, proto] import * as namespace from './local.js' import combined, {count as named} from './local.js' import other, * as both from './local.js' [combined, named, other, both === namespace] bump(); [snapshot, namespace.count] import './local.js' import {} from './local.js' loads import answer from './local.js' answer .exit "#, ); assert!(output.status.success()); assert_eq!(String::from_utf8_lossy(&output.stderr), ""); assert_eq!( String::from_utf8_lossy(&output.stdout), "undefined\n42\nundefined\n[ 1, 7, 7 ]\nundefined\nundefined\nundefined\n[ 42, 1, 42, true ]\n[ 1, 2 ]\nundefined\nundefined\n1\nundefined\n42\n" ); } #[test] fn repl_imports_preserve_statement_order_completion_values_and_line_numbers() { let directory = tempfile::tempdir().unwrap(); std::fs::write( directory.path().join("local.js"), "export default 42; export const count = 1;", ) .unwrap(); let output = finish_repl( start_repl(directory.path(), &[]), r#"import answer from './local.js'; import {count} from './local.js'; [answer, count] 1; import './local.js' 2; import {} from './local.js' 3; import other from './local.js' import aliased from './local.js' /* */ (aliased) import { count as another } from './local.js'; null.x 'import declarations stay inside strings' .exit "#, ); assert!(output.status.success()); assert_eq!( String::from_utf8_lossy(&output.stdout), "[ 42, 1 ]\n1\n2\n3\n42\n\"import declarations stay inside strings\"\n" ); let stderr = String::from_utf8(output.stderr).unwrap(); assert!(stderr.contains(".avril-repl.js:3:"), "{stderr}"); } #[test] fn imports_enqueued_after_microtask_only_await_wake_the_repl() { let directory = tempfile::tempdir().unwrap(); let output = finish_repl( start_repl(directory.path(), &["repl", "-0"]), "await 0; import * as std from 'avril:std'; import {inspect} from 'avril:std'; inspect(42)\nawait 0; (await import('avril:std')).inspect(43)\n.exit\n", ); assert!(output.status.success()); assert!( output.stderr.is_empty(), "{}", String::from_utf8_lossy(&output.stderr) ); assert_eq!(output.stdout, b"\"42\"\n\"43\"\n"); } #[test] fn native_ops_enqueued_after_microtask_only_await_wake_the_repl() { let directory = tempfile::tempdir().unwrap(); std::fs::write(directory.path().join("hello.txt"), "hello").unwrap(); let output = finish_repl( start_repl(directory.path(), &["repl", "-0", "--allow-read=."]), "import {readTextFile} from 'avril:fs'\nawait 0; await readTextFile('./hello.txt')\n.exit\n", ); assert!(output.status.success()); assert!( output.stderr.is_empty(), "{}", String::from_utf8_lossy(&output.stderr) ); assert_eq!(output.stdout, b"undefined\n\"hello\"\n"); } #[test] fn repl_import_declarations_resolve_builtins_and_config_aliases() { let directory = tempfile::tempdir().unwrap(); std::fs::write(directory.path().join("local.js"), "export default 42;").unwrap(); std::fs::write( directory.path().join("avril.conf"), "imports: { helper: './local.js' }", ) .unwrap(); let output = finish_repl( start_repl(directory.path(), &[]), "import { inspect as show } from 'avril:std'\nimport answer from 'helper'\nshow(answer)\n.exit\n", ); assert!(output.status.success()); assert!( output.stderr.is_empty(), "{}", String::from_utf8_lossy(&output.stderr) ); assert_eq!(output.stdout, b"undefined\nundefined\n\"42\"\n"); } #[test] fn repl_import_declarations_enforce_permissions_and_recover_from_failures() { let directory = tempfile::tempdir().unwrap(); for file in ["local.js", ".avril-repl.js"] { std::fs::write(directory.path().join(file), "export default 42;").unwrap(); } let denied = finish_repl( start_repl(directory.path(), &["repl", "-0"]), "import answer from './local.js'\nimport './.avril-repl.js'\nimport 'ext:core/ops'\n6 * 7\n.exit\n", ); assert!(denied.status.success()); assert_eq!(denied.stdout, b"42\n"); let stderr = String::from_utf8(denied.stderr).unwrap(); assert_eq!( stderr.matches("PermissionDenied: js-exec").count(), 2, "{stderr}" ); assert!(stderr.contains("Extension modules are private"), "{stderr}"); let revoked = finish_repl( start_repl(directory.path(), &["repl", "-0", "--allow-js-exec=."]), "import answer from './local.js'\nimport { pledge } from 'avril:std'\npledge({ jsExec: false })\nimport * as cached from './local.js'\nanswer\n.exit\n", ); assert!(revoked.status.success()); assert!(String::from_utf8_lossy(&revoked.stderr).contains("PermissionDenied: js-exec")); assert!(revoked.stdout.ends_with(b"42\n")); } #[test] fn repl_does_not_rewrite_invalid_or_nested_import_declarations() { let directory = tempfile::tempdir().unwrap(); std::fs::write( directory.path().join("side.js"), "globalThis.loaded = true;", ) .unwrap(); let output = finish_repl( start_repl(directory.path(), &[]), r#"import './side.js'; const = 1 function invalid() { import './side.js'; } 'import fake from "./side.js"' typeof loaded .exit "#, ); assert!(output.status.success()); assert!(output.stdout.ends_with(b"\"undefined\"\n")); let stderr = String::from_utf8(output.stderr).unwrap(); assert_eq!(stderr.matches("SyntaxError").count(), 2, "{stderr}"); } #[test] fn repl_import_attributes_are_not_silently_discarded() { let directory = tempfile::tempdir().unwrap(); std::fs::write( directory.path().join("side.js"), "globalThis.loaded = true;", ) .unwrap(); let output = finish_repl( start_repl(directory.path(), &[]), "import './side.js' with { type: 'json' }\ntypeof loaded\n.exit\n", ); assert!(output.status.success()); assert_eq!(output.stdout, b"\"undefined\"\n"); let stderr = String::from_utf8(output.stderr).unwrap(); assert!( stderr.contains("Import attributes are not supported yet"), "{stderr}" ); } #[test] fn repl_imports_use_live_execution_capabilities_without_an_entry_grant() { let directory = tempfile::tempdir().unwrap(); std::fs::write(directory.path().join("local.js"), "export default 42;").unwrap(); std::fs::write( directory.path().join(".avril-repl.js"), "export default 24;", ) .unwrap(); let denied = finish_repl( start_repl(directory.path(), &["repl", "-0"]), "try { await import('./local.js') } catch (error) { error.message }\ntry { await import('./.avril-repl.js') } catch (error) { error.message }\n.exit\n", ); assert!(denied.status.success()); let stdout = String::from_utf8(denied.stdout).unwrap(); assert_eq!( stdout.matches("PermissionDenied: js-exec").count(), 2, "{stdout}" ); let bare = finish_repl( start_repl(directory.path(), &[]), "(await import('./local.js')).default\n.exit\n", ); assert!(bare.status.success()); assert_eq!(bare.stdout, b"42\n"); let allowed = finish_repl( start_repl(directory.path(), &["repl", "-0", "--allow-js-exec=."]), "(await import('./local.js')).default\n.exit\n", ); assert!(allowed.status.success()); assert_eq!(allowed.stdout, b"42\n"); let revoked = finish_repl( start_repl(directory.path(), &["repl", "-0", "--allow-js-exec=."]), "(await import('avril:std')).pledge({ jsExec: false })\ntry { await import('./local.js') } catch (error) { error.message }\n.exit\n", ); assert!(revoked.status.success()); assert!( String::from_utf8(revoked.stdout) .unwrap() .contains("PermissionDenied: js-exec") ); } #[test] fn zero_capability_repl_has_no_ambient_read_or_network_access() { let directory = tempfile::tempdir().unwrap(); std::fs::write(directory.path().join("secret.txt"), "secret").unwrap(); let output = finish_repl( start_repl(directory.path(), &["repl", "-0"]), "try { await (await import('avril:fs')).readTextFile('./secret.txt') } catch (error) { error.message }\ntry { await fetch('https://example.com') } catch (error) { error.message }\n.exit\n", ); assert!(output.status.success()); let stdout = String::from_utf8(output.stdout).unwrap(); assert!(stdout.contains("read ./secret.txt"), "{stdout}"); assert!(stdout.contains("PermissionDenied: net"), "{stdout}"); } #[test] fn repl_disables_guest_stdin_while_it_owns_the_input_stream() { let directory = tempfile::tempdir().unwrap(); let output = finish_repl( start_repl(directory.path(), &["repl", "-0"]), "try { await (await import('avril:std')).stdin.getReader().read() } catch (error) { error.message }\n.exit\n", ); assert!(output.status.success()); assert!( String::from_utf8(output.stdout) .unwrap() .contains("stdin in the REPL is not yet implemented") ); } #[cfg(unix)] #[test] fn ctrl_c_interrupts_running_javascript_without_ending_the_session() { let directory = tempfile::tempdir().unwrap(); let mut child = start_repl(directory.path(), &["repl", "-0"]); let mut stdin = child.stdin.take().unwrap(); stdin.write_all(b"while (true) {}\n").unwrap(); thread::sleep(Duration::from_millis(500)); assert_eq!(unsafe { libc::kill(child.id() as i32, libc::SIGINT) }, 0); thread::sleep(Duration::from_millis(50)); stdin.write_all(b"2 + 2\n.exit\n").unwrap(); drop(stdin); let output = common::wait(child); assert!(output.status.success()); assert!(String::from_utf8(output.stdout).unwrap().contains("4\n")); assert!( String::from_utf8(output.stderr) .unwrap() .contains("Evaluation interrupted") ); } #[cfg(unix)] #[test] fn ctrl_c_interrupts_pending_top_level_await() { let directory = tempfile::tempdir().unwrap(); let mut child = start_repl(directory.path(), &["repl", "-0"]); let mut stdin = child.stdin.take().unwrap(); stdin .write_all(b"await new Promise(resolve => setTimeout(resolve, 10_000))\n") .unwrap(); thread::sleep(Duration::from_millis(500)); assert_eq!(unsafe { libc::kill(child.id() as i32, libc::SIGINT) }, 0); thread::sleep(Duration::from_millis(50)); stdin.write_all(b"3 + 3\n.exit\n").unwrap(); drop(stdin); let output = common::wait(child); assert!(output.status.success()); assert!(String::from_utf8(output.stdout).unwrap().contains("6\n")); assert!( String::from_utf8(output.stderr) .unwrap() .contains("Evaluation interrupted") ); } #[test] fn background_timers_progress_while_waiting_for_input() { let directory = tempfile::tempdir().unwrap(); let mut child = start_repl(directory.path(), &["repl", "-0"]); let mut stdin = child.stdin.take().unwrap(); stdin .write_all(b"let ticks = 0; const timer = setInterval(() => { if (++ticks === 1) console.log('background') }, 5)\n") .unwrap(); thread::sleep(Duration::from_millis(100)); stdin .write_all(b"clearInterval(timer); ticks\n.exit\n") .unwrap(); drop(stdin); let output = common::wait(child); assert!(output.status.success()); let stdout = String::from_utf8(output.stdout).unwrap(); assert!(stdout.contains("background\n"), "{stdout}"); assert!( stdout.lines().last().unwrap().parse::().unwrap() > 1, "{stdout}" ); } #[test] fn repl_completions_multiline_literals_and_previews_preserve_state() { let directory = tempfile::tempdir().unwrap(); let output = finish_repl( start_repl(directory.path(), &["repl", "-0"]), "let value = 1\nlet value = 2\nvalue\n{\n \"answer\": 42,\n label: 'ok'\n}\n`brace {\ncontinuation }`\nlet getters = 0\n({ get value() { getters++; return 4 } })\ngetters\nawait Promise.resolve(17)\nthrow 123\n11n\nSymbol.for('__avril.ops') in globalThis\n.exit\n", ); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); let stdout = String::from_utf8(output.stdout).unwrap(); assert!(stdout.starts_with("undefined\nundefined\n2\n"), "{stdout}"); assert!(stdout.contains("{ answer: 42, label: \"ok\" }"), "{stdout}"); assert!(stdout.contains("\"brace {\\ncontinuation }\""), "{stdout}"); assert!(stdout.ends_with("0\n17\n11n\nfalse\n"), "{stdout}"); assert_eq!(String::from_utf8(output.stderr).unwrap(), "123\n"); } #[test] fn results_and_thrown_values_are_shown_like_console_log() { let directory = tempfile::tempdir().unwrap(); let output = finish_repl( start_repl(directory.path(), &["repl", "-0"]), "({ a: { b: { c: [1, 'two'] } } })\nconst point = { x: 1, [Symbol.for('runtime.avril.inspect')]: (inspect, options) => `Point(${inspect(point.x, options)})` }\n[point]\nthrow { code: 'E_BAD' }\n'plain'\n.exit\n", ); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); assert_eq!( String::from_utf8(output.stdout).unwrap(), "{ a: { b: { c: [ 1, \"two\" ] } } }\nundefined\n[ Point(1) ]\n\"plain\"\n" ); assert_eq!( String::from_utf8(output.stderr).unwrap(), "{ code: \"E_BAD\" }\n" ); } #[test] fn repl_package_aliases_and_typescript_dependencies_use_the_existing_graph() { let directory = tempfile::tempdir().unwrap(); std::fs::create_dir(directory.path().join("math")).unwrap(); std::fs::write(directory.path().join("avril.conf"), "deps: ['./math']").unwrap(); std::fs::write( directory.path().join("math/avril.conf"), "name: '@acme/math', exports: {'.': './mod.ts'}", ) .unwrap(); std::fs::write( directory.path().join("math/mod.ts"), "export const answer: number = 42;", ) .unwrap(); let output = finish_repl( start_repl(directory.path(), &["repl"]), "(await import('@acme/math')).answer\n.exit\n", ); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); assert_eq!(output.stdout, b"42\n"); assert!( output.stderr.is_empty(), "{}", String::from_utf8_lossy(&output.stderr) ); } #[test] fn repl_imports_an_esm_module_entry_from_a_commonjs_package() { let server = fixture::Registry::new(); server.publish_npm( "dual-entry", "1.0.0", &[ ( "package.json", r#"{"name":"dual-entry","version":"1.0.0","module":"entry.es6.js","main":"entry.js"}"#, ), ("entry.es6.js", "export default value => value.toUpperCase();"), ("entry.js", "module.exports = value => value.toLowerCase();"), ], ); server.publish_npm( "consumer", "1.0.0", &[ ( "package.json", r#"{"name":"consumer","version":"1.0.0","exports":"./index.mjs","dependencies":{"dual-entry":"1.0.0"}}"#, ), ("index.mjs", "export { default } from 'dual-entry';"), ], ); let directory = tempfile::tempdir().unwrap(); let registry = format!("--npm-registry={}", server.url); let output = finish_repl( start_repl(directory.path(), &["repl", ®istry]), "import convert from 'npm:consumer@1'\nconvert('Hello')\n.exit\n", ); assert!(output.status.success()); assert!( output.stderr.is_empty(), "{}", String::from_utf8_lossy(&output.stderr) ); assert_eq!(output.stdout, b"undefined\n\"HELLO\"\n"); } #[test] fn repl_imports_packages_without_creating_or_updating_a_lockfile() { let server = fixture::Registry::new(); server.package( "@test/pkg", "1.0.0", &[("mod.ts", "export const value = 'jsr-one';")], &[(".", "./mod.ts")], ); server.publish_npm( "tool", "1.0.0", &[ ( "package.json", "{\"name\": \"tool\", \"version\": \"1.0.0\", \"type\": \"module\", \"exports\": {\".\": \"./index.js\"}}", ), ("index.js", "export default 'npm-one';"), ], ); let session = |directory: &std::path::Path| { let options = server.options(directory); let registries = [ format!("--jsr-registry={}", options.jsr_registry), format!("--npm-registry={}", options.npm_registry), format!("--cache-dir={}", options.cache.display()), ]; let mut arguments = vec!["repl"]; arguments.extend(registries.iter().map(String::as_str)); finish_repl( start_repl(directory, &arguments), "(await import('jsr:@test/pkg@^1')).value\n(await import('npm:tool@^1')).default\n.exit\n", ) }; // A session in a directory with no lockfile resolves and runs packages // without creating one. let directory = tempfile::tempdir().unwrap(); let output = session(directory.path()); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); assert_eq!(output.stdout, b"\"jsr-one\"\n\"npm-one\"\n"); assert!(!directory.path().join("avril.lock").exists()); // An existing lockfile still pins the session's selections, and is left // byte-for-byte alone even once newer versions are available. let directory = tempfile::tempdir().unwrap(); std::fs::write( directory.path().join("main.ts"), "import {value} from 'jsr:@test/pkg@^1'; import tool from 'npm:tool@^1'; console.log(value, tool);", ) .unwrap(); let options = server.options(directory.path()); let run = Command::new(env!("CARGO_BIN_EXE_avril")) .current_dir(directory.path()) .args(["run"]) .arg(format!("--jsr-registry={}", options.jsr_registry)) .arg(format!("--npm-registry={}", options.npm_registry)) .arg(format!("--cache-dir={}", options.cache.display())) .arg("main.ts") .stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() .unwrap(); let output = common::wait(run); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); let lock = std::fs::read(directory.path().join("avril.lock")).unwrap(); server.package( "@test/pkg", "1.1.0", &[("mod.ts", "export const value = 'jsr-two';")], &[(".", "./mod.ts")], ); server.publish_npm( "tool", "1.1.0", &[ ( "package.json", "{\"name\": \"tool\", \"version\": \"1.1.0\", \"type\": \"module\", \"exports\": {\".\": \"./index.js\"}}", ), ("index.js", "export default 'npm-two';"), ], ); let output = session(directory.path()); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); assert_eq!(output.stdout, b"\"jsr-one\"\n\"npm-one\"\n"); assert_eq!( std::fs::read(directory.path().join("avril.lock")).unwrap(), lock ); } #[test] fn tasks_enqueued_during_a_microtask_checkpoint_wake_pending_await() { let directory = tempfile::tempdir().unwrap(); let output = finish_repl( start_repl(directory.path(), &["repl", "-0"]), "await new Promise(resolve => { const {port1, port2} = new MessageChannel(); port2.onmessage = () => resolve(42); queueMicrotask(() => port1.postMessage(1)); })\n.exit\n", ); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); assert_eq!(output.stdout, b"42\n"); assert!(output.stderr.is_empty()); } #[test] fn console_output_precedes_results_and_background_errors_do_not_end_the_repl() { let directory = tempfile::tempdir().unwrap(); let output = finish_repl( start_repl(directory.path(), &["repl", "-0"]), "console.log('first'); 7\nsetTimeout(() => { throw Error('background failure') }, 0)\nawait new Promise(resolve => setTimeout(() => resolve(8), 20))\n2 + 2\n.exit\n", ); assert!(output.status.success()); let stdout = String::from_utf8(output.stdout).unwrap(); assert!(stdout.starts_with("first\n7\n"), "{stdout}"); assert!(stdout.ends_with("8\n4\n"), "{stdout}"); let stderr = String::from_utf8(output.stderr).unwrap(); assert_eq!(stderr.matches("background failure").count(), 1, "{stderr}"); } #[test] fn console_logging_from_async_callbacks_does_not_nest_executors() { let directory = tempfile::tempdir().unwrap(); let output = finish_repl( start_repl(directory.path(), &["repl", "-0"]), "await new Promise(resolve => setTimeout(() => { console.log('from timer'); resolve(1) }, 10))\nawait null; console.log('continued'); 2\nawait new Promise(resolve => { const {port1, port2} = new MessageChannel(); port2.onmessage = () => { console.log('from message'); resolve(3) }; port1.postMessage(1) })\n.exit\n", ); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); assert_eq!( output.stdout, b"from timer\n1\ncontinued\n2\nfrom message\n3\n" ); assert!(output.stderr.is_empty()); } #[test] fn piped_input_recovers_when_await_has_no_runnable_host_work() { let directory = tempfile::tempdir().unwrap(); let output = finish_repl( start_repl(directory.path(), &["repl", "-0"]), "await new Promise(() => {})\n42\n", ); assert!(output.status.success()); assert_eq!(output.stdout, b"42\n"); assert_eq!( String::from_utf8(output.stderr).unwrap(), "REPL evaluation is still pending with no runnable host work\n" ); } #[test] fn native_v8_background_work_completes_without_host_ops() { let directory = tempfile::tempdir().unwrap(); let output = finish_repl( start_repl(directory.path(), &["repl", "-0"]), "for (let i = 0; i < 50; i++) await WebAssembly.compile(new Uint8Array([0,97,115,109,1,0,0,0])); 42\n.exit\n", ); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); assert_eq!(output.stdout, b"42\n"); assert!(output.stderr.is_empty()); } #[cfg(unix)] #[test] fn repl_signal_listeners_fire_alongside_ctrl_c() { let directory = tempfile::tempdir().unwrap(); let mut child = start_repl(directory.path(), &["repl", "-0"]); let mut stdin = child.stdin.take().unwrap(); stdin .write_all(b"import {addSignalListener} from 'avril:std'; addSignalListener('SIGUSR1', () => console.log('repl-got-usr1')); addSignalListener('SIGINT', () => console.log('repl-got-int'));\n") .unwrap(); thread::sleep(Duration::from_secs(1)); // A signal while idle fires its listener and the session continues. assert_eq!(unsafe { libc::kill(child.id() as i32, libc::SIGUSR1) }, 0); thread::sleep(Duration::from_millis(500)); // SIGINT still interrupts running evaluation, and its listener observes // the interrupt rather than being clobbered by it. stdin.write_all(b"while (true) {}\n").unwrap(); thread::sleep(Duration::from_millis(500)); assert_eq!(unsafe { libc::kill(child.id() as i32, libc::SIGINT) }, 0); thread::sleep(Duration::from_millis(500)); stdin.write_all(b"40 + 2\n.exit\n").unwrap(); drop(stdin); let output = common::wait(child); assert!(output.status.success()); let stdout = String::from_utf8(output.stdout).unwrap(); assert!(stdout.contains("repl-got-usr1\n"), "{stdout}"); assert!(stdout.contains("repl-got-int\n"), "{stdout}"); assert!(stdout.contains("42\n"), "{stdout}"); assert!( String::from_utf8(output.stderr) .unwrap() .contains("Evaluation interrupted") ); }