char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somnpm: assume "module" package.json entries are es modules61e3521

main
29.3 KiB836 linesraw
1mod common;
2#[path = "common/registry.rs"]
3mod fixture;
4
5use std::io::Write;
6use std::process::{Command, Output, Stdio};
7use std::thread;
8use std::time::Duration;
9
10/// Resets SIGINT/SIGTERM to their defaults in the child, so results never
11/// depend on dispositions the test runner itself ignores.
12#[cfg(unix)]
13fn inherit_defaults(mut command: Command) -> Command {
14    use std::os::unix::process::CommandExt;
15    unsafe {
16        command.pre_exec(|| {
17            libc::signal(libc::SIGINT, libc::SIG_DFL);
18            libc::signal(libc::SIGTERM, libc::SIG_DFL);
19            Ok(())
20        });
21    }
22    command
23}
24
25fn start_repl(directory: &std::path::Path, arguments: &[&str]) -> std::process::Child {
26    #[cfg(unix)]
27    let mut command = inherit_defaults(Command::new(env!("CARGO_BIN_EXE_avril")));
28    #[cfg(not(unix))]
29    let mut command = Command::new(env!("CARGO_BIN_EXE_avril"));
30    command
31        .args(arguments)
32        .current_dir(directory)
33        .stdin(Stdio::piped())
34        .stdout(Stdio::piped())
35        .stderr(Stdio::piped())
36        .spawn()
37        .unwrap()
38}
39
40fn finish_repl(mut child: std::process::Child, input: &str) -> Output {
41    child
42        .stdin
43        .take()
44        .unwrap()
45        .write_all(input.as_bytes())
46        .unwrap();
47    common::wait(child)
48}
49
50#[cfg(unix)]
51#[test]
52fn repl_can_run_synchronous_processes_after_await_without_pumping_js() {
53    let directory = tempfile::tempdir().unwrap();
54    let output = finish_repl(
55        start_repl(directory.path(), &["repl"]),
56        concat!(
57            "const {runSync} = await import('avril:std');\n",
58            "await new Promise(resolve => setTimeout(resolve, 1)); ",
59            "let pumped = false; queueMicrotask(() => pumped = true); ",
60            "const result = runSync('/bin/sh', ['-c', 'cat; printf stderr >&2'], ",
61            "{input: 'repl sync', encoding: 'utf-8'}); ",
62            "if (pumped) throw Error('runSync pumped JS'); ",
63            "if (result.stderr !== 'stderr') throw Error('Missing stderr'); ",
64            "result.stdout\n.exit\n",
65        ),
66    );
67    assert!(output.status.success(), "{output:?}");
68    assert_eq!(output.stdout, b"undefined\n\"repl sync\"\n");
69    assert!(output.stderr.is_empty(), "{output:?}");
70}
71
72#[test]
73fn repl_persists_bindings_multiline_await_and_recovers_from_errors() {
74    let directory = tempfile::tempdir().unwrap();
75    let output = finish_repl(
76        start_repl(directory.path(), &["repl", "-0"]),
77        "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",
78    );
79    assert!(output.status.success());
80    let stdout = String::from_utf8(output.stdout).unwrap();
81    assert_eq!(
82        stdout,
83        "5\nundefined\n11\nundefined\n6\nundefined\n7\n\"Extension modules are private\"\n8\n"
84    );
85    let stderr = String::from_utf8(output.stderr).unwrap();
86    assert!(stderr.contains("expected"), "{stderr}");
87    assert!(stderr.contains("SyntaxError"), "{stderr}");
88}
89
90#[test]
91fn repl_underscore_tracks_successful_results_and_preserves_them_after_errors() {
92    let directory = tempfile::tempdir().unwrap();
93    let output = finish_repl(
94        start_repl(directory.path(), &["repl", "-0"]),
95        "_\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",
96    );
97    assert!(output.status.success());
98    assert_eq!(
99        String::from_utf8(output.stdout).unwrap(),
100        "undefined\n42\n43\n44\n44\n44\n44\nundefined\nundefined\n"
101    );
102    let stderr = String::from_utf8(output.stderr).unwrap();
103    for message in ["expected", "SyntaxError", "rejected"] {
104        assert!(stderr.contains(message), "{stderr}");
105    }
106}
107
108#[test]
109fn repl_underscore_retains_identity_and_non_json_values() {
110    let directory = tempfile::tempdir().unwrap();
111    let output = finish_repl(
112        start_repl(directory.path(), &["repl", "-0"]),
113        r#"const object = { answer: 42 }
114object
115_ === object
116const fn = () => 7
117fn
118_ === fn && _() === 7
119const symbol = Symbol('result')
120symbol
121_ === symbol
12211n
123_ + 1n
124NaN
125Number.isNaN(_)
126-0
127Object.is(_, -0)
128null
129_ === null
130void 0
131_ === undefined
132.exit
133"#,
134    );
135    assert!(output.status.success());
136    let stdout = String::from_utf8(output.stdout).unwrap();
137    assert!(
138        stdout.starts_with("undefined\n{ answer: 42 }\ntrue\n"),
139        "{stdout}"
140    );
141    assert_eq!(
142        stdout.lines().filter(|line| *line == "true").count(),
143        7,
144        "{stdout}"
145    );
146    assert!(
147        stdout.contains("11n\n12n\nNaN\ntrue\n0\ntrue\nnull\ntrue\nundefined\ntrue\n"),
148        "{stdout}"
149    );
150    assert!(
151        output.stderr.is_empty(),
152        "{}",
153        String::from_utf8_lossy(&output.stderr)
154    );
155}
156
157#[test]
158fn repl_import_declarations_support_all_binding_forms_and_persist() {
159    let directory = tempfile::tempdir().unwrap();
160    std::fs::write(
161        directory.path().join("local.js"),
162        "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__' };",
163    )
164    .unwrap();
165    let output = finish_repl(
166        start_repl(directory.path(), &["repl", "-0", "--allow-js-exec=."]),
167        r#"import answer from './local.js'
168answer
169import {
170  count as snapshot,
171  bump,
172  'odd-name' as odd,
173  '__proto__' as proto
174} from './local.js'
175[snapshot, odd, proto]
176import * as namespace from './local.js'
177import combined, {count as named} from './local.js'
178import other, * as both from './local.js'
179[combined, named, other, both === namespace]
180bump(); [snapshot, namespace.count]
181import './local.js'
182import {} from './local.js'
183loads
184import answer from './local.js'
185answer
186.exit
187"#,
188    );
189    assert!(output.status.success());
190    assert_eq!(String::from_utf8_lossy(&output.stderr), "");
191    assert_eq!(
192        String::from_utf8_lossy(&output.stdout),
193        "undefined\n42\nundefined\n[ 1, 7, 7 ]\nundefined\nundefined\nundefined\n[ 42, 1, 42, true ]\n[ 1, 2 ]\nundefined\nundefined\n1\nundefined\n42\n"
194    );
195}
196
197#[test]
198fn repl_imports_preserve_statement_order_completion_values_and_line_numbers() {
199    let directory = tempfile::tempdir().unwrap();
200    std::fs::write(
201        directory.path().join("local.js"),
202        "export default 42; export const count = 1;",
203    )
204    .unwrap();
205    let output = finish_repl(
206        start_repl(directory.path(), &[]),
207        r#"import answer from './local.js'; import {count} from './local.js'; [answer, count]
2081; import './local.js'
2092; import {} from './local.js'
2103; import other from './local.js'
211import aliased from './local.js' /*
212*/ (aliased)
213import {
214  count as another
215} from './local.js'; null.x
216'import declarations stay inside strings'
217.exit
218"#,
219    );
220    assert!(output.status.success());
221    assert_eq!(
222        String::from_utf8_lossy(&output.stdout),
223        "[ 42, 1 ]\n1\n2\n3\n42\n\"import declarations stay inside strings\"\n"
224    );
225    let stderr = String::from_utf8(output.stderr).unwrap();
226    assert!(stderr.contains(".avril-repl.js:3:"), "{stderr}");
227}
228
229#[test]
230fn imports_enqueued_after_microtask_only_await_wake_the_repl() {
231    let directory = tempfile::tempdir().unwrap();
232    let output = finish_repl(
233        start_repl(directory.path(), &["repl", "-0"]),
234        "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",
235    );
236    assert!(output.status.success());
237    assert!(
238        output.stderr.is_empty(),
239        "{}",
240        String::from_utf8_lossy(&output.stderr)
241    );
242    assert_eq!(output.stdout, b"\"42\"\n\"43\"\n");
243}
244
245#[test]
246fn native_ops_enqueued_after_microtask_only_await_wake_the_repl() {
247    let directory = tempfile::tempdir().unwrap();
248    std::fs::write(directory.path().join("hello.txt"), "hello").unwrap();
249    let output = finish_repl(
250        start_repl(directory.path(), &["repl", "-0", "--allow-read=."]),
251        "import {readTextFile} from 'avril:fs'\nawait 0; await readTextFile('./hello.txt')\n.exit\n",
252    );
253    assert!(output.status.success());
254    assert!(
255        output.stderr.is_empty(),
256        "{}",
257        String::from_utf8_lossy(&output.stderr)
258    );
259    assert_eq!(output.stdout, b"undefined\n\"hello\"\n");
260}
261
262#[test]
263fn repl_import_declarations_resolve_builtins_and_config_aliases() {
264    let directory = tempfile::tempdir().unwrap();
265    std::fs::write(directory.path().join("local.js"), "export default 42;").unwrap();
266    std::fs::write(
267        directory.path().join("avril.conf"),
268        "imports: { helper: './local.js' }",
269    )
270    .unwrap();
271    let output = finish_repl(
272        start_repl(directory.path(), &[]),
273        "import { inspect as show } from 'avril:std'\nimport answer from 'helper'\nshow(answer)\n.exit\n",
274    );
275    assert!(output.status.success());
276    assert!(
277        output.stderr.is_empty(),
278        "{}",
279        String::from_utf8_lossy(&output.stderr)
280    );
281    assert_eq!(output.stdout, b"undefined\nundefined\n\"42\"\n");
282}
283
284#[test]
285fn repl_import_declarations_enforce_permissions_and_recover_from_failures() {
286    let directory = tempfile::tempdir().unwrap();
287    for file in ["local.js", ".avril-repl.js"] {
288        std::fs::write(directory.path().join(file), "export default 42;").unwrap();
289    }
290    let denied = finish_repl(
291        start_repl(directory.path(), &["repl", "-0"]),
292        "import answer from './local.js'\nimport './.avril-repl.js'\nimport 'ext:core/ops'\n6 * 7\n.exit\n",
293    );
294    assert!(denied.status.success());
295    assert_eq!(denied.stdout, b"42\n");
296    let stderr = String::from_utf8(denied.stderr).unwrap();
297    assert_eq!(
298        stderr.matches("PermissionDenied: js-exec").count(),
299        2,
300        "{stderr}"
301    );
302    assert!(stderr.contains("Extension modules are private"), "{stderr}");
303
304    let revoked = finish_repl(
305        start_repl(directory.path(), &["repl", "-0", "--allow-js-exec=."]),
306        "import answer from './local.js'\nimport { pledge } from 'avril:std'\npledge({ jsExec: false })\nimport * as cached from './local.js'\nanswer\n.exit\n",
307    );
308    assert!(revoked.status.success());
309    assert!(String::from_utf8_lossy(&revoked.stderr).contains("PermissionDenied: js-exec"));
310    assert!(revoked.stdout.ends_with(b"42\n"));
311}
312
313#[test]
314fn repl_does_not_rewrite_invalid_or_nested_import_declarations() {
315    let directory = tempfile::tempdir().unwrap();
316    std::fs::write(
317        directory.path().join("side.js"),
318        "globalThis.loaded = true;",
319    )
320    .unwrap();
321    let output = finish_repl(
322        start_repl(directory.path(), &[]),
323        r#"import './side.js'; const = 1
324function invalid() { import './side.js'; }
325'import fake from "./side.js"'
326typeof loaded
327.exit
328"#,
329    );
330    assert!(output.status.success());
331    assert!(output.stdout.ends_with(b"\"undefined\"\n"));
332    let stderr = String::from_utf8(output.stderr).unwrap();
333    assert_eq!(stderr.matches("SyntaxError").count(), 2, "{stderr}");
334}
335
336#[test]
337fn repl_import_attributes_are_not_silently_discarded() {
338    let directory = tempfile::tempdir().unwrap();
339    std::fs::write(
340        directory.path().join("side.js"),
341        "globalThis.loaded = true;",
342    )
343    .unwrap();
344    let output = finish_repl(
345        start_repl(directory.path(), &[]),
346        "import './side.js' with { type: 'json' }\ntypeof loaded\n.exit\n",
347    );
348    assert!(output.status.success());
349    assert_eq!(output.stdout, b"\"undefined\"\n");
350    let stderr = String::from_utf8(output.stderr).unwrap();
351    assert!(
352        stderr.contains("Import attributes are not supported yet"),
353        "{stderr}"
354    );
355}
356
357#[test]
358fn repl_imports_use_live_execution_capabilities_without_an_entry_grant() {
359    let directory = tempfile::tempdir().unwrap();
360    std::fs::write(directory.path().join("local.js"), "export default 42;").unwrap();
361    std::fs::write(
362        directory.path().join(".avril-repl.js"),
363        "export default 24;",
364    )
365    .unwrap();
366
367    let denied = finish_repl(
368        start_repl(directory.path(), &["repl", "-0"]),
369        "try { await import('./local.js') } catch (error) { error.message }\ntry { await import('./.avril-repl.js') } catch (error) { error.message }\n.exit\n",
370    );
371    assert!(denied.status.success());
372    let stdout = String::from_utf8(denied.stdout).unwrap();
373    assert_eq!(
374        stdout.matches("PermissionDenied: js-exec").count(),
375        2,
376        "{stdout}"
377    );
378
379    let bare = finish_repl(
380        start_repl(directory.path(), &[]),
381        "(await import('./local.js')).default\n.exit\n",
382    );
383    assert!(bare.status.success());
384    assert_eq!(bare.stdout, b"42\n");
385
386    let allowed = finish_repl(
387        start_repl(directory.path(), &["repl", "-0", "--allow-js-exec=."]),
388        "(await import('./local.js')).default\n.exit\n",
389    );
390    assert!(allowed.status.success());
391    assert_eq!(allowed.stdout, b"42\n");
392
393    let revoked = finish_repl(
394        start_repl(directory.path(), &["repl", "-0", "--allow-js-exec=."]),
395        "(await import('avril:std')).pledge({ jsExec: false })\ntry { await import('./local.js') } catch (error) { error.message }\n.exit\n",
396    );
397    assert!(revoked.status.success());
398    assert!(
399        String::from_utf8(revoked.stdout)
400            .unwrap()
401            .contains("PermissionDenied: js-exec")
402    );
403}
404
405#[test]
406fn zero_capability_repl_has_no_ambient_read_or_network_access() {
407    let directory = tempfile::tempdir().unwrap();
408    std::fs::write(directory.path().join("secret.txt"), "secret").unwrap();
409    let output = finish_repl(
410        start_repl(directory.path(), &["repl", "-0"]),
411        "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",
412    );
413    assert!(output.status.success());
414    let stdout = String::from_utf8(output.stdout).unwrap();
415    assert!(stdout.contains("read ./secret.txt"), "{stdout}");
416    assert!(stdout.contains("PermissionDenied: net"), "{stdout}");
417}
418
419#[test]
420fn repl_disables_guest_stdin_while_it_owns_the_input_stream() {
421    let directory = tempfile::tempdir().unwrap();
422    let output = finish_repl(
423        start_repl(directory.path(), &["repl", "-0"]),
424        "try { await (await import('avril:std')).stdin.getReader().read() } catch (error) { error.message }\n.exit\n",
425    );
426    assert!(output.status.success());
427    assert!(
428        String::from_utf8(output.stdout)
429            .unwrap()
430            .contains("stdin in the REPL is not yet implemented")
431    );
432}
433
434#[cfg(unix)]
435#[test]
436fn ctrl_c_interrupts_running_javascript_without_ending_the_session() {
437    let directory = tempfile::tempdir().unwrap();
438    let mut child = start_repl(directory.path(), &["repl", "-0"]);
439    let mut stdin = child.stdin.take().unwrap();
440    stdin.write_all(b"while (true) {}\n").unwrap();
441    thread::sleep(Duration::from_millis(500));
442    assert_eq!(unsafe { libc::kill(child.id() as i32, libc::SIGINT) }, 0);
443    thread::sleep(Duration::from_millis(50));
444    stdin.write_all(b"2 + 2\n.exit\n").unwrap();
445    drop(stdin);
446    let output = common::wait(child);
447    assert!(output.status.success());
448    assert!(String::from_utf8(output.stdout).unwrap().contains("4\n"));
449    assert!(
450        String::from_utf8(output.stderr)
451            .unwrap()
452            .contains("Evaluation interrupted")
453    );
454}
455
456#[cfg(unix)]
457#[test]
458fn ctrl_c_interrupts_pending_top_level_await() {
459    let directory = tempfile::tempdir().unwrap();
460    let mut child = start_repl(directory.path(), &["repl", "-0"]);
461    let mut stdin = child.stdin.take().unwrap();
462    stdin
463        .write_all(b"await new Promise(resolve => setTimeout(resolve, 10_000))\n")
464        .unwrap();
465    thread::sleep(Duration::from_millis(500));
466    assert_eq!(unsafe { libc::kill(child.id() as i32, libc::SIGINT) }, 0);
467    thread::sleep(Duration::from_millis(50));
468    stdin.write_all(b"3 + 3\n.exit\n").unwrap();
469    drop(stdin);
470    let output = common::wait(child);
471    assert!(output.status.success());
472    assert!(String::from_utf8(output.stdout).unwrap().contains("6\n"));
473    assert!(
474        String::from_utf8(output.stderr)
475            .unwrap()
476            .contains("Evaluation interrupted")
477    );
478}
479
480#[test]
481fn background_timers_progress_while_waiting_for_input() {
482    let directory = tempfile::tempdir().unwrap();
483    let mut child = start_repl(directory.path(), &["repl", "-0"]);
484    let mut stdin = child.stdin.take().unwrap();
485    stdin
486        .write_all(b"let ticks = 0; const timer = setInterval(() => { if (++ticks === 1) console.log('background') }, 5)\n")
487        .unwrap();
488    thread::sleep(Duration::from_millis(100));
489    stdin
490        .write_all(b"clearInterval(timer); ticks\n.exit\n")
491        .unwrap();
492    drop(stdin);
493    let output = common::wait(child);
494    assert!(output.status.success());
495    let stdout = String::from_utf8(output.stdout).unwrap();
496    assert!(stdout.contains("background\n"), "{stdout}");
497    assert!(
498        stdout.lines().last().unwrap().parse::<u32>().unwrap() > 1,
499        "{stdout}"
500    );
501}
502
503#[test]
504fn repl_completions_multiline_literals_and_previews_preserve_state() {
505    let directory = tempfile::tempdir().unwrap();
506    let output = finish_repl(
507        start_repl(directory.path(), &["repl", "-0"]),
508        "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",
509    );
510    assert!(
511        output.status.success(),
512        "{}",
513        String::from_utf8_lossy(&output.stderr)
514    );
515    let stdout = String::from_utf8(output.stdout).unwrap();
516    assert!(stdout.starts_with("undefined\nundefined\n2\n"), "{stdout}");
517    assert!(stdout.contains("{ answer: 42, label: \"ok\" }"), "{stdout}");
518    assert!(stdout.contains("\"brace {\\ncontinuation }\""), "{stdout}");
519    assert!(stdout.ends_with("0\n17\n11n\nfalse\n"), "{stdout}");
520    assert_eq!(String::from_utf8(output.stderr).unwrap(), "123\n");
521}
522
523#[test]
524fn results_and_thrown_values_are_shown_like_console_log() {
525    let directory = tempfile::tempdir().unwrap();
526    let output = finish_repl(
527        start_repl(directory.path(), &["repl", "-0"]),
528        "({ 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",
529    );
530    assert!(
531        output.status.success(),
532        "{}",
533        String::from_utf8_lossy(&output.stderr)
534    );
535    assert_eq!(
536        String::from_utf8(output.stdout).unwrap(),
537        "{ a: { b: { c: [ 1, \"two\" ] } } }\nundefined\n[ Point(1) ]\n\"plain\"\n"
538    );
539    assert_eq!(
540        String::from_utf8(output.stderr).unwrap(),
541        "{ code: \"E_BAD\" }\n"
542    );
543}
544
545#[test]
546fn repl_package_aliases_and_typescript_dependencies_use_the_existing_graph() {
547    let directory = tempfile::tempdir().unwrap();
548    std::fs::create_dir(directory.path().join("math")).unwrap();
549    std::fs::write(directory.path().join("avril.conf"), "deps: ['./math']").unwrap();
550    std::fs::write(
551        directory.path().join("math/avril.conf"),
552        "name: '@acme/math', exports: {'.': './mod.ts'}",
553    )
554    .unwrap();
555    std::fs::write(
556        directory.path().join("math/mod.ts"),
557        "export const answer: number = 42;",
558    )
559    .unwrap();
560    let output = finish_repl(
561        start_repl(directory.path(), &["repl"]),
562        "(await import('@acme/math')).answer\n.exit\n",
563    );
564    assert!(
565        output.status.success(),
566        "{}",
567        String::from_utf8_lossy(&output.stderr)
568    );
569    assert_eq!(output.stdout, b"42\n");
570    assert!(
571        output.stderr.is_empty(),
572        "{}",
573        String::from_utf8_lossy(&output.stderr)
574    );
575}
576
577#[test]
578fn repl_imports_an_esm_module_entry_from_a_commonjs_package() {
579    let server = fixture::Registry::new();
580    server.publish_npm(
581        "dual-entry",
582        "1.0.0",
583        &[
584            (
585                "package.json",
586                r#"{"name":"dual-entry","version":"1.0.0","module":"entry.es6.js","main":"entry.js"}"#,
587            ),
588            ("entry.es6.js", "export default value => value.toUpperCase();"),
589            ("entry.js", "module.exports = value => value.toLowerCase();"),
590        ],
591    );
592    server.publish_npm(
593        "consumer",
594        "1.0.0",
595        &[
596            (
597                "package.json",
598                r#"{"name":"consumer","version":"1.0.0","exports":"./index.mjs","dependencies":{"dual-entry":"1.0.0"}}"#,
599            ),
600            ("index.mjs", "export { default } from 'dual-entry';"),
601        ],
602    );
603    let directory = tempfile::tempdir().unwrap();
604    let registry = format!("--npm-registry={}", server.url);
605    let output = finish_repl(
606        start_repl(directory.path(), &["repl", &registry]),
607        "import convert from 'npm:consumer@1'\nconvert('Hello')\n.exit\n",
608    );
609    assert!(output.status.success());
610    assert!(
611        output.stderr.is_empty(),
612        "{}",
613        String::from_utf8_lossy(&output.stderr)
614    );
615    assert_eq!(output.stdout, b"undefined\n\"HELLO\"\n");
616}
617
618#[test]
619fn repl_imports_packages_without_creating_or_updating_a_lockfile() {
620    let server = fixture::Registry::new();
621    server.package(
622        "@test/pkg",
623        "1.0.0",
624        &[("mod.ts", "export const value = 'jsr-one';")],
625        &[(".", "./mod.ts")],
626    );
627    server.publish_npm(
628        "tool",
629        "1.0.0",
630        &[
631            (
632                "package.json",
633                "{\"name\": \"tool\", \"version\": \"1.0.0\", \"type\": \"module\", \"exports\": {\".\": \"./index.js\"}}",
634            ),
635            ("index.js", "export default 'npm-one';"),
636        ],
637    );
638    let session = |directory: &std::path::Path| {
639        let options = server.options(directory);
640        let registries = [
641            format!("--jsr-registry={}", options.jsr_registry),
642            format!("--npm-registry={}", options.npm_registry),
643            format!("--cache-dir={}", options.cache.display()),
644        ];
645        let mut arguments = vec!["repl"];
646        arguments.extend(registries.iter().map(String::as_str));
647        finish_repl(
648            start_repl(directory, &arguments),
649            "(await import('jsr:@test/pkg@^1')).value\n(await import('npm:tool@^1')).default\n.exit\n",
650        )
651    };
652
653    // A session in a directory with no lockfile resolves and runs packages
654    // without creating one.
655    let directory = tempfile::tempdir().unwrap();
656    let output = session(directory.path());
657    assert!(
658        output.status.success(),
659        "{}",
660        String::from_utf8_lossy(&output.stderr)
661    );
662    assert_eq!(output.stdout, b"\"jsr-one\"\n\"npm-one\"\n");
663    assert!(!directory.path().join("avril.lock").exists());
664
665    // An existing lockfile still pins the session's selections, and is left
666    // byte-for-byte alone even once newer versions are available.
667    let directory = tempfile::tempdir().unwrap();
668    std::fs::write(
669        directory.path().join("main.ts"),
670        "import {value} from 'jsr:@test/pkg@^1'; import tool from 'npm:tool@^1'; console.log(value, tool);",
671    )
672    .unwrap();
673    let options = server.options(directory.path());
674    let run = Command::new(env!("CARGO_BIN_EXE_avril"))
675        .current_dir(directory.path())
676        .args(["run"])
677        .arg(format!("--jsr-registry={}", options.jsr_registry))
678        .arg(format!("--npm-registry={}", options.npm_registry))
679        .arg(format!("--cache-dir={}", options.cache.display()))
680        .arg("main.ts")
681        .stdin(Stdio::null())
682        .stdout(Stdio::piped())
683        .stderr(Stdio::piped())
684        .spawn()
685        .unwrap();
686    let output = common::wait(run);
687    assert!(
688        output.status.success(),
689        "{}",
690        String::from_utf8_lossy(&output.stderr)
691    );
692    let lock = std::fs::read(directory.path().join("avril.lock")).unwrap();
693    server.package(
694        "@test/pkg",
695        "1.1.0",
696        &[("mod.ts", "export const value = 'jsr-two';")],
697        &[(".", "./mod.ts")],
698    );
699    server.publish_npm(
700        "tool",
701        "1.1.0",
702        &[
703            (
704                "package.json",
705                "{\"name\": \"tool\", \"version\": \"1.1.0\", \"type\": \"module\", \"exports\": {\".\": \"./index.js\"}}",
706            ),
707            ("index.js", "export default 'npm-two';"),
708        ],
709    );
710    let output = session(directory.path());
711    assert!(
712        output.status.success(),
713        "{}",
714        String::from_utf8_lossy(&output.stderr)
715    );
716    assert_eq!(output.stdout, b"\"jsr-one\"\n\"npm-one\"\n");
717    assert_eq!(
718        std::fs::read(directory.path().join("avril.lock")).unwrap(),
719        lock
720    );
721}
722
723#[test]
724fn tasks_enqueued_during_a_microtask_checkpoint_wake_pending_await() {
725    let directory = tempfile::tempdir().unwrap();
726    let output = finish_repl(
727        start_repl(directory.path(), &["repl", "-0"]),
728        "await new Promise(resolve => { const {port1, port2} = new MessageChannel(); port2.onmessage = () => resolve(42); queueMicrotask(() => port1.postMessage(1)); })\n.exit\n",
729    );
730    assert!(
731        output.status.success(),
732        "{}",
733        String::from_utf8_lossy(&output.stderr)
734    );
735    assert_eq!(output.stdout, b"42\n");
736    assert!(output.stderr.is_empty());
737}
738
739#[test]
740fn console_output_precedes_results_and_background_errors_do_not_end_the_repl() {
741    let directory = tempfile::tempdir().unwrap();
742    let output = finish_repl(
743        start_repl(directory.path(), &["repl", "-0"]),
744        "console.log('first'); 7\nsetTimeout(() => { throw Error('background failure') }, 0)\nawait new Promise(resolve => setTimeout(() => resolve(8), 20))\n2 + 2\n.exit\n",
745    );
746    assert!(output.status.success());
747    let stdout = String::from_utf8(output.stdout).unwrap();
748    assert!(stdout.starts_with("first\n7\n"), "{stdout}");
749    assert!(stdout.ends_with("8\n4\n"), "{stdout}");
750    let stderr = String::from_utf8(output.stderr).unwrap();
751    assert_eq!(stderr.matches("background failure").count(), 1, "{stderr}");
752}
753
754#[test]
755fn console_logging_from_async_callbacks_does_not_nest_executors() {
756    let directory = tempfile::tempdir().unwrap();
757    let output = finish_repl(
758        start_repl(directory.path(), &["repl", "-0"]),
759        "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",
760    );
761    assert!(
762        output.status.success(),
763        "{}",
764        String::from_utf8_lossy(&output.stderr)
765    );
766    assert_eq!(
767        output.stdout,
768        b"from timer\n1\ncontinued\n2\nfrom message\n3\n"
769    );
770    assert!(output.stderr.is_empty());
771}
772
773#[test]
774fn piped_input_recovers_when_await_has_no_runnable_host_work() {
775    let directory = tempfile::tempdir().unwrap();
776    let output = finish_repl(
777        start_repl(directory.path(), &["repl", "-0"]),
778        "await new Promise(() => {})\n42\n",
779    );
780    assert!(output.status.success());
781    assert_eq!(output.stdout, b"42\n");
782    assert_eq!(
783        String::from_utf8(output.stderr).unwrap(),
784        "REPL evaluation is still pending with no runnable host work\n"
785    );
786}
787
788#[test]
789fn native_v8_background_work_completes_without_host_ops() {
790    let directory = tempfile::tempdir().unwrap();
791    let output = finish_repl(
792        start_repl(directory.path(), &["repl", "-0"]),
793        "for (let i = 0; i < 50; i++) await WebAssembly.compile(new Uint8Array([0,97,115,109,1,0,0,0])); 42\n.exit\n",
794    );
795    assert!(
796        output.status.success(),
797        "{}",
798        String::from_utf8_lossy(&output.stderr)
799    );
800    assert_eq!(output.stdout, b"42\n");
801    assert!(output.stderr.is_empty());
802}
803
804#[cfg(unix)]
805#[test]
806fn repl_signal_listeners_fire_alongside_ctrl_c() {
807    let directory = tempfile::tempdir().unwrap();
808    let mut child = start_repl(directory.path(), &["repl", "-0"]);
809    let mut stdin = child.stdin.take().unwrap();
810    stdin
811        .write_all(b"import {addSignalListener} from 'avril:std'; addSignalListener('SIGUSR1', () => console.log('repl-got-usr1')); addSignalListener('SIGINT', () => console.log('repl-got-int'));\n")
812        .unwrap();
813    thread::sleep(Duration::from_secs(1));
814    // A signal while idle fires its listener and the session continues.
815    assert_eq!(unsafe { libc::kill(child.id() as i32, libc::SIGUSR1) }, 0);
816    thread::sleep(Duration::from_millis(500));
817    // SIGINT still interrupts running evaluation, and its listener observes
818    // the interrupt rather than being clobbered by it.
819    stdin.write_all(b"while (true) {}\n").unwrap();
820    thread::sleep(Duration::from_millis(500));
821    assert_eq!(unsafe { libc::kill(child.id() as i32, libc::SIGINT) }, 0);
822    thread::sleep(Duration::from_millis(500));
823    stdin.write_all(b"40 + 2\n.exit\n").unwrap();
824    drop(stdin);
825    let output = common::wait(child);
826    assert!(output.status.success());
827    let stdout = String::from_utf8(output.stdout).unwrap();
828    assert!(stdout.contains("repl-got-usr1\n"), "{stdout}");
829    assert!(stdout.contains("repl-got-int\n"), "{stdout}");
830    assert!(stdout.contains("42\n"), "{stdout}");
831    assert!(
832        String::from_utf8(output.stderr)
833            .unwrap()
834            .contains("Evaluation interrupted")
835    );
836}