diff --git a/Cargo.lock b/Cargo.lock index f28c00b55e..bb383249c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12,15 +12,6 @@ dependencies = [ "regex", ] -[[package]] -name = "addr2line" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" -dependencies = [ - "gimli", -] - [[package]] name = "adler" version = "1.0.2" @@ -208,21 +199,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" -[[package]] -name = "backtrace" -version = "0.3.67" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" -dependencies = [ - "addr2line", - "cc", - "cfg-if", - "libc", - "miniz_oxide 0.6.2", - "object", - "rustc-demangle", -] - [[package]] name = "base16ct" version = "0.1.1" @@ -484,9 +460,9 @@ dependencies = [ [[package]] name = "console_static_text" -version = "0.7.1" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953d2c3cf53213a4eccdbe8f2e0b49b5d0f77e87a2a9060117bbf9346f92b64e" +checksum = "f4be93df536dfbcbd39ff7c129635da089901116b88bfc29ec1acb9b56f8ff35" dependencies = [ "unicode-width", "vte", @@ -1777,7 +1753,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" dependencies = [ "crc32fast", - "miniz_oxide 0.5.4", + "miniz_oxide", ] [[package]] @@ -1998,12 +1974,6 @@ dependencies = [ "polyval", ] -[[package]] -name = "gimli" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4" - [[package]] name = "glibc_version" version = "0.1.2" @@ -2724,15 +2694,6 @@ dependencies = [ "adler", ] -[[package]] -name = "miniz_oxide" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" -dependencies = [ - "adler", -] - [[package]] name = "mio" version = "0.8.6" @@ -2912,15 +2873,6 @@ dependencies = [ "libc", ] -[[package]] -name = "object" -version = "0.30.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea86265d3d3dcb6a27fc51bd29a4bf387fae9d2986b823079d4986af253eb439" -dependencies = [ - "memchr", -] - [[package]] name = "once_cell" version = "1.17.1" @@ -3588,12 +3540,6 @@ dependencies = [ "smallvec", ] -[[package]] -name = "rustc-demangle" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" - [[package]] name = "rustc-hash" version = "1.1.0" @@ -4669,13 +4615,14 @@ dependencies = [ "anyhow", "async-stream", "atty", - "backtrace", "base64 0.13.1", + "console_static_text", "flate2", "futures", "hyper", "lazy_static", "lsp-types", + "nix", "once_cell", "os_pipe", "parking_lot 0.12.1", diff --git a/Cargo.toml b/Cargo.toml index b4ee768be5..b8cfa99a1c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -86,7 +86,7 @@ bencher = "0.1" bytes = "1.4.0" cache_control = "=0.2.0" cbc = { version = "=0.1.2", features = ["alloc"] } -console_static_text = "=0.7.1" +console_static_text = "=0.8.1" data-url = "=0.2.0" dlopen = "0.1.8" encoding_rs = "=0.8.31" diff --git a/cli/tests/integration/repl_tests.rs b/cli/tests/integration/repl_tests.rs index f7bd627c30..82cae5024c 100644 --- a/cli/tests/integration/repl_tests.rs +++ b/cli/tests/integration/repl_tests.rs @@ -6,35 +6,31 @@ use test_util::assert_ends_with; use test_util::assert_not_contains; use util::TempDir; -#[ignore] #[test] fn pty_multiline() { util::with_pty(&["repl"], |mut console| { console.write_line("(\n1 + 2\n)"); + console.expect("3"); console.write_line("{\nfoo: \"foo\"\n}"); + console.expect("{ foo: \"foo\" }"); console.write_line("`\nfoo\n`"); + console.expect("\"\\nfoo\\n\""); console.write_line("`\n\\`\n`"); + console.expect(r#""\n`\n""#); console.write_line("'{'"); + console.expect(r#""{""#); console.write_line("'('"); + console.expect(r#""(""#); console.write_line("'['"); + console.expect(r#""[""#); console.write_line("/{/"); + console.expect("/{/"); console.write_line("/\\(/"); + console.expect("/\\(/"); console.write_line("/\\[/"); + console.expect("/\\[/"); console.write_line("console.log(\"{test1} abc {test2} def {{test3}}\".match(/{([^{].+?)}/));"); - console.write_line("close();"); - - let output = console.read_all_output(); - assert_contains!(output, '3'); - assert_contains!(output, "{ foo: \"foo\" }"); - assert_contains!(output, "\"\\nfoo\\n\""); - assert_contains!(output, "\"\\n`\\n\""); - assert_contains!(output, "\"{\""); - assert_contains!(output, "\"(\""); - assert_contains!(output, "\"[\""); - assert_contains!(output, "/{/"); - assert_contains!(output, "/\\(/"); - assert_contains!(output, "/\\[/"); - assert_contains!(output, "[ \"{test1}\", \"test1\" ]"); + console.expect("[ \"{test1}\", \"test1\" ]"); }); } @@ -42,10 +38,7 @@ fn pty_multiline() { fn pty_null() { util::with_pty(&["repl"], |mut console| { console.write_line("null"); - console.write_line("close();"); - - let output = console.read_all_output(); - assert_contains!(output, "null"); + console.expect("null"); }); } @@ -54,10 +47,7 @@ fn pty_unpaired_braces() { for right_brace in &[")", "]", "}"] { util::with_pty(&["repl"], |mut console| { console.write_line(right_brace); - console.write_line("close();"); - - let output = console.read_all_output(); - assert_contains!(output, "Expression expected"); + console.expect("parse error: Expression expected"); }); } } @@ -66,10 +56,7 @@ fn pty_unpaired_braces() { fn pty_bad_input() { util::with_pty(&["repl"], |mut console| { console.write_line("'\\u{1f3b5}'[0]"); - console.write_line("close();"); - - let output = console.read_all_output(); - assert_contains!(output, "Unterminated string literal"); + console.expect("Unterminated string literal"); }); } @@ -77,28 +64,21 @@ fn pty_bad_input() { fn pty_syntax_error_input() { util::with_pty(&["repl"], |mut console| { console.write_line("('\\u')"); - console.write_line("'"); - console.write_line("[{'a'}];"); - console.write_line("close();"); + console.expect("Bad character escape sequence, expected 4 hex characters"); - let output = console.read_all_output(); - assert_contains!( - output, - "Bad character escape sequence, expected 4 hex characters" - ); - assert_contains!(output, "Unterminated string constant"); - assert_contains!(output, "Expected a semicolon"); + console.write_line("'"); + console.expect("Unterminated string constant"); + + console.write_line("[{'a'}];"); + console.expect("Expected a semicolon"); }); } #[test] fn pty_complete_symbol() { util::with_pty(&["repl"], |mut console| { - console.write_line("Symbol.it\t"); - console.write_line("close();"); - - let output = console.read_all_output(); - assert_contains!(output, "Symbol(Symbol.iterator)"); + console.write_line_raw("Symbol.it\t"); + console.expect("Symbol(Symbol.iterator)"); }); } @@ -106,14 +86,13 @@ fn pty_complete_symbol() { fn pty_complete_declarations() { util::with_pty(&["repl"], |mut console| { console.write_line("class MyClass {}"); - console.write_line("My\t"); - console.write_line("let myVar;"); - console.write_line("myV\t"); - console.write_line("close();"); - - let output = console.read_all_output(); - assert_contains!(output, "> MyClass"); - assert_contains!(output, "> myVar"); + console.expect("undefined"); + console.write_line_raw("My\t"); + console.expect("[Class: MyClass]"); + console.write_line("let myVar = 2 + 3;"); + console.expect("undefined"); + console.write_line_raw("myV\t"); + console.expect("5"); }); } @@ -121,37 +100,31 @@ fn pty_complete_declarations() { fn pty_complete_primitives() { util::with_pty(&["repl"], |mut console| { console.write_line("let func = function test(){}"); - console.write_line("func.appl\t"); + console.expect("undefined"); + console.write_line_raw("func.appl\t"); + console.expect("func.apply"); console.write_line("let str = ''"); - console.write_line("str.leng\t"); - console.write_line("false.valueO\t"); - console.write_line("5n.valueO\t"); + console.expect("undefined"); + console.write_line_raw("str.leng\t"); + console.expect("str.length"); + console.write_line_raw("false.valueO\t"); + console.expect("false.valueOf"); + console.write_line_raw("5n.valueO\t"); + console.expect("5n.valueOf"); console.write_line("let num = 5"); - console.write_line("num.toStrin\t"); - console.write_line("close();"); - - let output = console.read_all_output(); - assert_contains!(output, "> func.apply"); - assert_contains!(output, "> str.length"); - assert_contains!(output, "> 5n.valueOf"); - assert_contains!(output, "> false.valueOf"); - assert_contains!(output, "> num.toString"); + console.expect("undefined"); + console.write_line_raw("num.toStrin\t"); + console.expect("num.toString"); }); } #[test] fn pty_complete_expression() { util::with_pty(&["repl"], |mut console| { - console.write_text("Deno.\t\t"); - console.write_text("y"); - console.write_line(""); - console.write_line("close();"); - let output = console.read_all_output(); - assert_contains!(output, "Display all"); - assert_contains!(output, "args"); - assert_contains!(output, "exit"); - assert_contains!(output, "symlink"); - assert_contains!(output, "permissions"); + console.write_raw("Deno.\t\t"); + console.expect("Display all"); + console.write_raw("y"); + console.expect_all(&["symlink", "args", "permissions", "exit"]); }); } @@ -159,66 +132,51 @@ fn pty_complete_expression() { fn pty_complete_imports() { util::with_pty(&["repl", "-A"], |mut console| { // single quotes - console.write_line("import './run/001_hel\t'"); + console.write_line_raw("import './run/001_hel\t'"); + console.expect("Hello World"); // double quotes - console.write_line("import { output } from \"./run/045_out\t\""); - console.write_line("output('testing output');"); - console.write_line("close();"); - - let output = console.read_all_output(); - assert_contains!(output, "Hello World"); - assert_contains!( - output, - // on windows, could any (it's flaky) - "\ntesting output", - "testing output\u{1b}", - "\r\n\u{1b}[?25htesting output", - ); + console.write_line_raw("import { output } from \"./run/045_out\t\""); + console.expect("\"./run/045_output.ts\""); + console.write_line_raw("output('testing output');"); + console.expect("testing output"); }); // ensure when the directory changes that the suggestions come from the cwd util::with_pty(&["repl", "-A"], |mut console| { console.write_line("Deno.chdir('./subdir');"); - console.write_line("import '../run/001_hel\t'"); - console.write_line("close();"); - - let output = console.read_all_output(); - assert_contains!(output, "Hello World"); + console.expect("undefined"); + console.write_line_raw("import '../run/001_hel\t'"); + console.expect("Hello World"); }); } #[test] fn pty_complete_imports_no_panic_empty_specifier() { // does not panic when tabbing when empty - util::with_pty(&["repl"], |mut console| { - console.write_line("import '\t';"); - console.write_line("close();"); + util::with_pty(&["repl", "-A"], |mut console| { + if cfg!(windows) { + console.write_line_raw("import '\t'"); + console.expect_any(&["not prefixed with", "https://deno.land"]); + } else { + console.write_raw("import '\t"); + console.expect("import 'https://deno.land"); + } }); } #[test] fn pty_ignore_symbols() { util::with_pty(&["repl"], |mut console| { - console.write_line("Array.Symbol\t"); - console.write_line("close();"); - - let output = console.read_all_output(); - assert_contains!(output, "undefined"); - assert_not_contains!( - output, - "Uncaught TypeError: Array.Symbol is not a function" - ); + console.write_line_raw("Array.Symbol\t"); + console.expect("undefined"); }); } #[test] fn pty_assign_global_this() { util::with_pty(&["repl"], |mut console| { - console.write_line("globalThis = 42;"); - console.write_line("close();"); - - let output = console.read_all_output(); - assert_not_contains!(output, "panicked"); + console.write_line("globalThis = 40 + 2;"); + console.expect("42"); }); } @@ -228,13 +186,12 @@ fn pty_assign_deno_keys_and_deno() { console.write_line( "Object.keys(Deno).forEach((key)=>{try{Deno[key] = undefined} catch {}})", ); + console.expect("undefined"); console.write_line("delete globalThis.Deno"); - console.write_line("console.log('testing ' + 'this out')"); - console.write_line("close();"); - - let output = console.read_all_output(); - assert_not_contains!(output, "panicked"); - assert_contains!(output, "testing this out"); + console.expect("true"); + console.write_line("console.log('testing ' + 'this out');"); + console.expect("testing this out"); + console.expect("undefined"); }); } @@ -242,9 +199,14 @@ fn pty_assign_deno_keys_and_deno() { fn pty_internal_repl() { util::with_pty(&["repl"], |mut console| { console.write_line("globalThis"); - console.write_line("__\t\t"); - console.write_line("close();"); - let output = console.read_all_output(); + console.write_line_raw("1 + 256"); + let output = console.read_until("257"); + assert_contains!(output, "clear:"); + assert_not_contains!(output, "__DENO_"); + + console.write_line_raw("__\t\t"); + console.expect("> __"); + let output = console.read_until("> __"); assert_contains!(output, "__defineGetter__"); // should not contain the internal repl variable // in the `globalThis` or completions output @@ -257,190 +219,139 @@ fn pty_emoji() { // windows was having issues displaying this util::with_pty(&["repl"], |mut console| { console.write_line(r#"console.log('\u{1F995}');"#); - console.write_line("close();"); - - let output = console.read_all_output(); - // only one for the output (since input is escaped) - let emoji_count = output.chars().filter(|c| *c == 'πŸ¦•').count(); - assert_eq!(emoji_count, 1); + console.expect("πŸ¦•"); }); } #[test] fn console_log() { - let (out, err) = util::run_and_collect_output( - true, - "repl", - Some(vec!["console.log('hello')", "'world'"]), - Some(vec![("NO_COLOR".to_owned(), "1".to_owned())]), - false, - ); - assert_ends_with!(out, "hello\nundefined\n\"world\"\n"); - assert!(err.is_empty()); + util::with_pty(&["repl"], |mut console| { + console.write_line("console.log('hello');"); + console.expect("hello"); + console.write_line("'world'"); + console.expect("\"world\""); + }); } #[test] fn object_literal() { - let (out, err) = util::run_and_collect_output( - true, - "repl", - Some(vec!["{}", "{ foo: 'bar' }"]), - Some(vec![("NO_COLOR".to_owned(), "1".to_owned())]), - false, - ); - assert_ends_with!(out, "{}\n{ foo: \"bar\" }\n"); - assert!(err.is_empty()); + util::with_pty(&["repl"], |mut console| { + console.write_line("{}"); + console.expect("{}"); + console.write_line("{ foo: 'bar' }"); + console.expect("{ foo: \"bar\" }"); + }); } #[test] fn block_expression() { - let (out, err) = util::run_and_collect_output( - true, - "repl", - Some(vec!["{};", "{\"\"}"]), - Some(vec![("NO_COLOR".to_owned(), "1".to_owned())]), - false, - ); - assert_ends_with!(out, "undefined\n\"\"\n"); - assert!(err.is_empty()); + util::with_pty(&["repl"], |mut console| { + console.write_line("{};"); + console.expect("undefined"); + console.write_line("{\"\"}"); + console.expect("\"\""); + }); } #[test] fn await_resolve() { - let (out, err) = util::run_and_collect_output( - true, - "repl", - Some(vec!["await Promise.resolve('done')"]), - Some(vec![("NO_COLOR".to_owned(), "1".to_owned())]), - false, - ); - assert_ends_with!(out, "\"done\"\n"); - assert!(err.is_empty()); + util::with_pty(&["repl"], |mut console| { + console.write_line("await Promise.resolve('done')"); + console.expect("\"done\""); + }); } #[test] fn await_timeout() { - let (out, err) = util::run_and_collect_output( - true, - "repl", - Some(vec!["await new Promise((r) => setTimeout(r, 0, 'done'))"]), - Some(vec![("NO_COLOR".to_owned(), "1".to_owned())]), - false, - ); - assert_ends_with!(out, "\"done\"\n"); - assert!(err.is_empty()); + util::with_pty(&["repl"], |mut console| { + console.write_line("await new Promise((r) => setTimeout(r, 0, 'done'))"); + console.expect("\"done\""); + }); } #[test] fn let_redeclaration() { - let (out, err) = util::run_and_collect_output( - true, - "repl", - Some(vec!["let foo = 0;", "foo", "let foo = 1;", "foo"]), - Some(vec![("NO_COLOR".to_owned(), "1".to_owned())]), - false, - ); - assert_ends_with!(out, "undefined\n0\nundefined\n1\n"); - assert!(err.is_empty()); + util::with_pty(&["repl"], |mut console| { + console.write_line("let foo = 0;"); + console.expect("undefined"); + console.write_line("foo"); + console.expect("0"); + console.write_line("let foo = 1;"); + console.expect("undefined"); + console.write_line("foo"); + console.expect("1"); + }); } #[test] fn repl_cwd() { - let (_out, err) = util::run_and_collect_output( - true, - "repl", - Some(vec!["Deno.cwd()"]), - Some(vec![("NO_COLOR".to_owned(), "1".to_owned())]), - false, - ); - assert!(err.is_empty()); + util::with_pty(&["repl", "-A"], |mut console| { + console.write_line("Deno.cwd()"); + console.expect("testdata"); + }); } #[test] fn typescript() { - let (out, err) = util::run_and_collect_output( - true, - "repl", - Some(vec![ - "function add(a: number, b: number) { return a + b }", - "const result: number = add(1, 2) as number;", - "result", - ]), - Some(vec![("NO_COLOR".to_owned(), "1".to_owned())]), - false, - ); - assert_ends_with!(out, "undefined\nundefined\n3\n"); - assert!(err.is_empty()); + util::with_pty(&["repl"], |mut console| { + console.write_line("function add(a: number, b: number) { return a + b }"); + console.expect("undefined"); + console.write_line("const result: number = add(1, 2) as number;"); + console.expect("undefined"); + console.write_line("result"); + console.expect("3"); + }); } #[test] fn typescript_declarations() { - let (out, err) = util::run_and_collect_output( - true, - "repl", - Some(vec![ - "namespace Test { export enum Values { A, B, C } }", - "Test.Values.A", - "Test.Values.C", - "interface MyInterface { prop: string; }", - "type MyTypeAlias = string;", - ]), - Some(vec![("NO_COLOR".to_owned(), "1".to_owned())]), - false, - ); - let expected_end_text = "undefined\n0\n2\nundefined\nundefined\n"; - assert_ends_with!(out, expected_end_text); - assert!(err.is_empty()); + util::with_pty(&["repl"], |mut console| { + console.write_line("namespace Test { export enum Values { A, B, C } }"); + console.expect("undefined"); + console.write_line("Test.Values.A"); + console.expect("0"); + console.write_line("Test.Values.C"); + console.expect("2"); + console.write_line("interface MyInterface { prop: string; }"); + console.expect("undefined"); + console.write_line("type MyTypeAlias = string;"); + console.expect("undefined"); + }); } #[test] fn typescript_decorators() { - let (out, err) = util::run_and_collect_output( - true, - "repl", - Some(vec![ - "function dec(target) { target.prototype.test = () => 2; }", - "@dec class Test {}", - "new Test().test()", - ]), - Some(vec![("NO_COLOR".to_owned(), "1".to_owned())]), - false, - ); - assert_ends_with!(out, "undefined\n[Class: Test]\n2\n"); - assert!(err.is_empty()); + util::with_pty(&["repl"], |mut console| { + console + .write_line("function dec(target) { target.prototype.test = () => 2; }"); + console.expect("undefined"); + console.write_line("@dec class Test {}"); + console.expect("[Class: Test]"); + console.write_line("new Test().test()"); + console.expect("2"); + }); } #[test] fn eof() { - let (out, err) = util::run_and_collect_output( - true, - "repl", - Some(vec!["1 + 2"]), - Some(vec![("NO_COLOR".to_owned(), "1".to_owned())]), - false, - ); - assert_ends_with!(out, "3\n"); - assert!(err.is_empty()); + util::with_pty(&["repl"], |mut console| { + console.write_line("1 + 2"); + console.expect("3"); + }); } #[test] fn strict() { - let (out, err) = util::run_and_collect_output( - true, - "repl", - Some(vec![ - "let a = {};", - "Object.preventExtensions(a);", - "a.c = 1;", - ]), - None, - false, - ); - assert_contains!( - out, - "Uncaught TypeError: Cannot add property c, object is not extensible" - ); - assert!(err.is_empty()); + util::with_pty(&["repl"], |mut console| { + console.write_line("let a = {};"); + console.expect("undefined"); + console.write_line("Object.preventExtensions(a)"); + console.expect("{}"); + console.write_line("a.c = 1;"); + console.expect( + "Uncaught TypeError: Cannot add property c, object is not extensible", + ); + }); } #[test] @@ -459,176 +370,118 @@ fn close_command() { #[test] fn function() { - let (out, err) = util::run_and_collect_output( - true, - "repl", - Some(vec!["Deno.writeFileSync"]), - Some(vec![("NO_COLOR".to_owned(), "1".to_owned())]), - false, - ); - assert_ends_with!(out, "[Function: writeFileSync]\n"); - assert!(err.is_empty()); + util::with_pty(&["repl"], |mut console| { + console.write_line("Deno.writeFileSync"); + console.expect("[Function: writeFileSync]"); + }); } #[test] -#[ignore] fn multiline() { - let (out, err) = util::run_and_collect_output( - true, - "repl", - Some(vec!["(\n1 + 2\n)"]), - Some(vec![("NO_COLOR".to_owned(), "1".to_owned())]), - false, - ); - assert_ends_with!(out, "3\n"); - assert!(err.is_empty()); + util::with_pty(&["repl"], |mut console| { + console.write_line("(\n1 + 2\n)"); + console.expect("3"); + }); } #[test] fn import() { - let (out, _) = util::run_and_collect_output_with_args( - true, - vec![], - Some(vec!["import('./subdir/auto_print_hello.ts')"]), - Some(vec![("NO_COLOR".to_owned(), "1".to_owned())]), - false, - ); - assert_contains!(out, "hello!\n"); + util::with_pty(&["repl", "-A"], |mut console| { + console.write_line("import('./subdir/auto_print_hello.ts')"); + console.expect("hello!"); + }); } #[test] fn import_declarations() { - let (out, _) = util::run_and_collect_output_with_args( - true, - vec!["repl", "--allow-read"], - Some(vec!["import './subdir/auto_print_hello.ts';"]), - Some(vec![("NO_COLOR".to_owned(), "1".to_owned())]), - false, - ); - assert_contains!(out, "hello!\n"); + util::with_pty(&["repl", "-A"], |mut console| { + console.write_line("import './subdir/auto_print_hello.ts'"); + console.expect("hello!"); + }); } #[test] fn exports_stripped() { - let (out, err) = util::run_and_collect_output( - true, - "repl", - Some(vec!["export default 5;", "export class Test {}"]), - Some(vec![("NO_COLOR".to_owned(), "1".to_owned())]), - false, - ); - assert_contains!(out, "5\n"); - assert!(err.is_empty()); + util::with_pty(&["repl"], |mut console| { + console.write_line("const test = 5 + 1; export default test;"); + console.expect("6"); + console.write_line("export class Test {}"); + console.expect("undefined"); + }); } #[test] fn call_eval_unterminated() { - let (out, err) = util::run_and_collect_output( - true, - "repl", - Some(vec!["eval('{')"]), - None, - false, - ); - assert_contains!(out, "Unexpected end of input"); - assert!(err.is_empty()); + util::with_pty(&["repl"], |mut console| { + console.write_line("eval('{')"); + console.expect("Unexpected end of input"); + }); } #[test] fn unpaired_braces() { - for right_brace in &[")", "]", "}"] { - let (out, err) = util::run_and_collect_output( - true, - "repl", - Some(vec![right_brace]), - None, - false, - ); - assert_contains!(out, "Expression expected"); - assert!(err.is_empty()); - } + util::with_pty(&["repl"], |mut console| { + for right_brace in &[")", "]", "}"] { + console.write_line(right_brace); + console.expect("Expression expected"); + } + }); } #[test] fn reference_error() { - let (out, err) = util::run_and_collect_output( - true, - "repl", - Some(vec!["not_a_variable"]), - None, - false, - ); - assert_contains!(out, "not_a_variable is not defined"); - assert!(err.is_empty()); + util::with_pty(&["repl"], |mut console| { + console.write_line("not_a_variable"); + console.expect("not_a_variable is not defined"); + }); } #[test] fn syntax_error() { - let (out, err) = util::run_and_collect_output( - true, - "repl", - Some(vec![ - "syntax error", - "2", // ensure it keeps accepting input after - ]), - Some(vec![("NO_COLOR".to_owned(), "1".to_owned())]), - false, - ); - assert_ends_with!(out, "parse error: Expected ';', '}' or at 1:8\n2\n"); - assert!(err.is_empty()); + util::with_pty(&["repl"], |mut console| { + console.write_line("syntax error"); + console.expect("parse error: Expected ';', '}' or "); + // ensure it keeps accepting input after + console.write_line("7 * 6"); + console.expect("42"); + }); } #[test] fn syntax_error_jsx() { // JSX is not supported in the REPL - let (out, err) = util::run_and_collect_output( - true, - "repl", - Some(vec!["const element =
;"]), - Some(vec![("NO_COLOR".to_owned(), "1".to_owned())]), - false, - ); - assert_contains!(out, "Expression expected"); - assert!(err.is_empty()); + util::with_pty(&["repl"], |mut console| { + console.write_line("const element =
;"); + console.expect("Expression expected"); + }); } #[test] fn type_error() { - let (out, err) = util::run_and_collect_output( - true, - "repl", - Some(vec!["console()"]), - None, - false, - ); - assert_contains!(out, "console is not a function"); - assert!(err.is_empty()); + util::with_pty(&["repl"], |mut console| { + console.write_line("console()"); + console.expect("console is not a function"); + }); } #[test] fn variable() { - let (out, err) = util::run_and_collect_output( - true, - "repl", - Some(vec!["var a = 123;", "a"]), - Some(vec![("NO_COLOR".to_owned(), "1".to_owned())]), - false, - ); - assert_ends_with!(out, "undefined\n123\n"); - assert!(err.is_empty()); + util::with_pty(&["repl"], |mut console| { + console.write_line("var a = 123 + 456;"); + console.expect("undefined"); + console.write_line("a"); + console.expect("579"); + }); } #[test] fn lexical_scoped_variable() { - let (out, err) = util::run_and_collect_output( - true, - "repl", - Some(vec!["let a = 123;", "a"]), - Some(vec![("NO_COLOR".to_owned(), "1".to_owned())]), - false, - ); - assert_ends_with!(out, "undefined\n123\n"); - assert!(err.is_empty()); + util::with_pty(&["repl"], |mut console| { + console.write_line("let a = 123 + 456;"); + console.expect("undefined"); + console.write_line("a"); + console.expect("579"); + }); } #[test] @@ -702,95 +555,70 @@ fn disable_history_file() { #[test] fn save_last_eval() { - let (out, err) = util::run_and_collect_output( - true, - "repl", - Some(vec!["1", "_"]), - Some(vec![("NO_COLOR".to_owned(), "1".to_owned())]), - false, - ); - assert_ends_with!(out, "1\n1\n"); - assert!(err.is_empty()); + util::with_pty(&["repl"], |mut console| { + console.write_line("1 + 2"); + console.expect("3"); + console.write_line("_ + 3"); + console.expect("6"); + }); } #[test] fn save_last_thrown() { - let (out, err) = util::run_and_collect_output( - true, - "repl", - Some(vec!["throw 1", "_error"]), - Some(vec![("NO_COLOR".to_owned(), "1".to_owned())]), - false, - ); - assert_ends_with!(out, "Uncaught 1\n1\n"); - assert!(err.is_empty()); + util::with_pty(&["repl"], |mut console| { + console.write_line("throw 1 + 2"); + console.expect("Uncaught 3"); + console.write_line("_error + 3"); + console.expect("6"); + }); } #[test] fn assign_underscore() { - let (out, err) = util::run_and_collect_output( - true, - "repl", - Some(vec!["_ = 1", "2", "_"]), - Some(vec![("NO_COLOR".to_owned(), "1".to_owned())]), - false, - ); - assert_ends_with!( - out, - "Last evaluation result is no longer saved to _.\n1\n2\n1\n" - ); - assert!(err.is_empty()); + util::with_pty(&["repl"], |mut console| { + console.write_line("_ = 1"); + console.expect("Last evaluation result is no longer saved to _."); + console.write_line("2 + 3"); + console.expect("5"); + console.write_line("_"); + console.expect("1"); + }); } #[test] fn assign_underscore_error() { - let (out, err) = util::run_and_collect_output( - true, - "repl", - Some(vec!["_error = 1", "throw 2", "_error"]), - Some(vec![("NO_COLOR".to_owned(), "1".to_owned())]), - false, - ); - println!("{out}"); - assert_ends_with!( - out, - "Last thrown error is no longer saved to _error.\n1\nUncaught 2\n1\n" - ); - assert!(err.is_empty()); + util::with_pty(&["repl"], |mut console| { + console.write_line("_error = 1"); + console.expect("Last thrown error is no longer saved to _error."); + console.write_line("throw 2"); + console.expect("Uncaught 2"); + console.write_line("_error"); + console.expect("1"); + }); } #[test] fn custom_inspect() { - let (out, err) = util::run_and_collect_output( - true, - "repl", - Some(vec![ + util::with_pty(&["repl"], |mut console| { + console.write_line( r#"const o = { - [Symbol.for("Deno.customInspect")]() { - throw new Error('Oops custom inspect error'); - }, - };"#, - "o", - ]), - Some(vec![("NO_COLOR".to_owned(), "1".to_owned())]), - false, - ); - - assert_contains!(out, "Oops custom inspect error"); - assert!(err.is_empty()); + [Symbol.for("Deno.customInspect")]() { + throw new Error('Oops custom inspect error'); + }, + };"#, + ); + console.expect("undefined"); + console.write_line("o"); + console.expect("Oops custom inspect error"); + }); } #[test] fn eval_flag_valid_input() { - let (out, err) = util::run_and_collect_output_with_args( - true, - vec!["repl", "--eval", "const t = 10;"], - Some(vec!["t * 500;"]), - None, - false, - ); - assert_contains!(out, "5000"); - assert!(err.is_empty()); + util::with_pty(&["repl", "--eval", "const t = 10;"], |mut console| { + console.write_line("t * 500"); + console.expect("5000"); + }); } #[test] @@ -879,25 +707,23 @@ fn eval_file_flag_multiple_files() { #[test] fn pty_clear_function() { util::with_pty(&["repl"], |mut console| { - console.write_line("console.log('hello');"); - console.write_line("clear();"); - console.write_line("const clear = 1234 + 2000;"); - console.write_line("clear;"); - console.write_line("close();"); - - let output = console.read_all_output(); + console.write_line("console.log('h' + 'ello');"); + console.expect_all(&["hello", "undefined"]); + console.write_line_raw("clear();"); if cfg!(windows) { - // Windows will overwrite what's in the console buffer before - // we read from it. It contains this string repeated many times - // to clear the screen. - assert_contains!(output, "\r\n\u{1b}[K\r\n\u{1b}[K\r\n\u{1b}[K"); + // expect a bunch of these in the output + console.expect_raw_in_current_output( + "\r\n\u{1b}[K\r\n\u{1b}[K\r\n\u{1b}[K\r\n\u{1b}[K\r\n\u{1b}[K", + ); } else { - assert_contains!(output, "hello"); - assert_contains!(output, "[1;1H"); + console.expect_raw_in_current_output("[1;1H"); } - assert_contains!(output, "undefined"); - assert_contains!(output, "const clear = 1234 + 2000;"); - assert_contains!(output, "3234"); + console.expect("undefined"); // advance past the "clear()"'s undefined + console.expect("> "); + console.write_line("const clear = 1234 + 2000;"); + console.expect("undefined"); + console.write_line("clear;"); + console.expect("3234"); }); } @@ -905,53 +731,42 @@ fn pty_clear_function() { fn pty_tab_handler() { // If the last character is **not** whitespace, we show the completions util::with_pty(&["repl"], |mut console| { - console.write_line("a\t\t"); - console.write_line("close();"); - let output = console.read_all_output(); - assert_contains!(output, "addEventListener"); - assert_contains!(output, "alert"); - assert_contains!(output, "atob"); + console.write_raw("a\t\t"); + console.expect_all(&["addEventListener", "alert", "atob"]); }); // If the last character is whitespace, we just insert a tab util::with_pty(&["repl"], |mut console| { - console.write_line("a; \t\t"); // last character is whitespace - console.write_line("close();"); - let output = console.read_all_output(); - assert_not_contains!(output, "addEventListener"); - assert_not_contains!(output, "alert"); - assert_not_contains!(output, "atob"); + console.write_line("const a = 5;"); + console.expect("undefined"); + console.write_raw("a; \t\ta + 2;\n"); // last character is whitespace + console.expect_any(&[ + // windows + "a; a + 2;", + // unix + "a; \t\ta + 2;", + ]); }); } #[test] fn repl_report_error() { - let (out, err) = util::run_and_collect_output( - true, - "repl", - Some(vec![ - r#"console.log(1); reportError(new Error("foo")); console.log(2);"#, - ]), - Some(vec![("NO_COLOR".to_owned(), "1".to_owned())]), - false, - ); - - // TODO(nayeemrmn): The REPL should report event errors and rejections. - assert_contains!(out, "1\n2\nundefined\n"); - assert!(err.is_empty()); + util::with_pty(&["repl"], |mut console| { + console.write_line("console.log(1);"); + console.expect_all(&["1", "undefined"]); + // TODO(nayeemrmn): The REPL should report event errors and rejections. + console.write_line(r#"reportError(new Error("foo"));"#); + console.expect("undefined"); + console.write_line("console.log(2);"); + console.expect("2"); + }); } #[test] fn pty_aggregate_error() { - let (out, err) = util::run_and_collect_output( - true, - "repl", - Some(vec!["await Promise.any([])"]), - Some(vec![("NO_COLOR".to_owned(), "1".to_owned())]), - false, - ); - - assert_contains!(out, "AggregateError"); - assert!(err.is_empty()); + util::with_pty(&["repl"], |mut console| { + console.write_line("await Promise.any([])"); + console.expect("AggregateError"); + }); } #[test] @@ -1062,11 +877,10 @@ fn npm_packages() { fn pty_tab_indexable_props() { util::with_pty(&["repl"], |mut console| { console.write_line("const arr = [1, 2, 3]"); - console.write_line("arr.\t\t"); - console.write_line("close();"); - - let output = console.read_all_output(); - println!("output"); + console.expect("undefined"); + console.write_raw("arr.\t\t"); + console.expect("> arr."); + let output = console.read_until("> arr."); assert_contains!(output, "constructor"); assert_contains!(output, "sort"); assert_contains!(output, "at"); diff --git a/cli/tests/integration/run_tests.rs b/cli/tests/integration/run_tests.rs index 4a0581b84c..4504c970d9 100644 --- a/cli/tests/integration/run_tests.rs +++ b/cli/tests/integration/run_tests.rs @@ -6,6 +6,7 @@ use std::io::Read; use std::io::Write; use std::process::Command; use std::process::Stdio; +use std::time::Duration; use test_util as util; use test_util::TempDir; use tokio::task::LocalSet; @@ -570,88 +571,183 @@ itest!(_089_run_allow_list { #[test] fn _090_run_permissions_request() { - let args = "run --quiet run/090_run_permissions_request.ts"; - use util::PtyData::*; - util::test_pty2(args, vec![ - Output("⚠️ ️Deno requests run access to \"ls\". Run again with --allow-run to bypass this prompt.\r\n Allow? [y/n (y = yes allow, n = no deny)]"), - Input("y\n"), - Output("⚠️ ️Deno requests run access to \"cat\". Run again with --allow-run to bypass this prompt.\r\n Allow? [y/n (y = yes allow, n = no deny)]"), - Input("n\n"), - Output("granted\r\n"), - Output("prompt\r\n"), - Output("denied\r\n"), - ]); + util::with_pty( + &["run", "--quiet", "run/090_run_permissions_request.ts"], + |mut console| { + console.expect(concat!( + "β”Œ ⚠️ Deno requests run access to \"ls\".\r\n", + "β”œ Requested by `Deno.permissions.query()` API.\r\n", + "β”œ Run again with --allow-run to bypass this prompt.\r\n", + "β”” Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all run permissions)", + )); + console.write_line_raw("y"); + console.expect("Granted run access to \"ls\"."); + console.expect(concat!( + "β”Œ ⚠️ Deno requests run access to \"cat\".\r\n", + "β”œ Requested by `Deno.permissions.query()` API.\r\n", + "β”œ Run again with --allow-run to bypass this prompt.\r\n", + "β”” Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all run permissions)", + )); + console.write_line_raw("n"); + console.expect("Denied run access to \"cat\"."); + console.expect("granted"); + console.expect("denied"); + }, + ); } #[test] fn _090_run_permissions_request_sync() { - let args = "run --quiet run/090_run_permissions_request_sync.ts"; - use util::PtyData::*; - util::test_pty2(args, vec![ - Output("⚠️ ️Deno requests run access to \"ls\". Run again with --allow-run to bypass this prompt.\r\n Allow? [y/n (y = yes allow, n = no deny)]"), - Input("y\n"), - Output("⚠️ ️Deno requests run access to \"cat\". Run again with --allow-run to bypass this prompt.\r\n Allow? [y/n (y = yes allow, n = no deny)]"), - Input("n\n"), - Output("granted\r\n"), - Output("prompt\r\n"), - Output("denied\r\n"), - ]); + util::with_pty( + &["run", "--quiet", "run/090_run_permissions_request_sync.ts"], + |mut console| { + console.expect(concat!( + "β”Œ ⚠️ Deno requests run access to \"ls\".\r\n", + "β”œ Requested by `Deno.permissions.query()` API.\r\n", + "β”œ Run again with --allow-run to bypass this prompt.\r\n", + "β”” Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all run permissions)", + )); + console.write_line_raw("y"); + console.expect("Granted run access to \"ls\"."); + console.expect(concat!( + "β”Œ ⚠️ Deno requests run access to \"cat\".\r\n", + "β”œ Requested by `Deno.permissions.query()` API.\r\n", + "β”œ Run again with --allow-run to bypass this prompt.\r\n", + "β”” Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all run permissions)", + )); + console.write_line_raw("n"); + console.expect("Denied run access to \"cat\"."); + console.expect("granted"); + console.expect("denied"); + }, + ); } #[test] fn permissions_prompt_allow_all() { - let args = "run --quiet run/permissions_prompt_allow_all.ts"; - use util::PtyData::*; - util::test_pty2(args, vec![ - // "run" permissions - Output("β”Œ ⚠️ Deno requests run access to \"FOO\".\r\nβ”œ Requested by `Deno.permissions.query()` API\r\n β”œ Run again with --allow-run to bypass this prompt.\r\nβ”” Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all run permissions) >"), - Input("a\n"), - Output("βœ… Granted all run access.\r\n"), - // "read" permissions - Output("β”Œ ⚠️ Deno requests read access to \"FOO\".\r\nβ”œ Requested by `Deno.permissions.query()` API\r\n β”œ Run again with --allow-read to bypass this prompt.\r\nβ”” Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all read permissions) >"), - Input("a\n"), - Output("βœ… Granted all read access.\r\n"), - // "write" permissions - Output("β”Œ ⚠️ Deno requests write access to \"FOO\".\r\nβ”œ Requested by `Deno.permissions.query()` API\r\n β”œ Run again with --allow-write to bypass this prompt.\r\nβ”” Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all write permissions) >"), - Input("a\n"), - Output("βœ… Granted all write access.\r\n"), - // "net" permissions - Output("β”Œ ⚠️ Deno requests net access to \"FOO\".\r\nβ”œ Requested by `Deno.permissions.query()` API\r\n β”œ Run again with --allow-net to bypass this prompt.\r\nβ”” Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all net permissions) >"), - Input("a\n"), - Output("βœ… Granted all net access.\r\n"), - // "env" permissions - Output("β”Œ ⚠️ Deno requests env access to \"FOO\".\r\nβ”œ Requested by `Deno.permissions.query()` API\r\n β”œ Run again with --allow-env to bypass this prompt.\r\nβ”” Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all env permissions) >"), - Input("a\n"), - Output("βœ… Granted all env access.\r\n"), - // "sys" permissions - Output("β”Œ ⚠️ Deno requests sys access to \"loadavg\".\r\nβ”œ Requested by `Deno.permissions.query()` API\r\n β”œ Run again with --allow-sys to bypass this prompt.\r\nβ”” Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all sys permissions) >"), - Input("a\n"), - Output("βœ… Granted all sys access.\r\n"), - // "ffi" permissions - Output("β”Œ ⚠️ Deno requests ffi access to \"FOO\".\r\nβ”œ Requested by `Deno.permissions.query()` API\r\n β”œ Run again with --allow-ffi to bypass this prompt.\r\nβ”” Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all ffi permissions) >"), - Input("a\n"), - Output("βœ… Granted all ffi access.\r\n") - ]); + util::with_pty( + &["run", "--quiet", "run/permissions_prompt_allow_all.ts"], + |mut console| { + // "run" permissions + console.expect(concat!( + "β”Œ ⚠️ Deno requests run access to \"FOO\".\r\n", + "β”œ Requested by `Deno.permissions.query()` API.\r\n", + "β”œ Run again with --allow-run to bypass this prompt.\r\n", + "β”” Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all run permissions)", + )); + console.write_line_raw("A"); + console.expect("βœ… Granted all run access."); + // "read" permissions + console.expect(concat!( + "β”Œ ⚠️ Deno requests read access to \"FOO\".\r\n", + "β”œ Requested by `Deno.permissions.query()` API.\r\n", + "β”œ Run again with --allow-read to bypass this prompt.\r\n", + "β”” Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all read permissions)", + )); + console.write_line_raw("A"); + console.expect("βœ… Granted all read access."); + // "write" permissions + console.expect(concat!( + "β”Œ ⚠️ Deno requests write access to \"FOO\".\r\n", + "β”œ Requested by `Deno.permissions.query()` API.\r\n", + "β”œ Run again with --allow-write to bypass this prompt.\r\n", + "β”” Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all write permissions)", + )); + console.write_line_raw("A"); + console.expect("βœ… Granted all write access."); + // "net" permissions + console.expect(concat!( + "β”Œ ⚠️ Deno requests network access to \"foo\".\r\n", + "β”œ Requested by `Deno.permissions.query()` API.\r\n", + "β”œ Run again with --allow-net to bypass this prompt.\r\n", + "β”” Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all net permissions)", + )); + console.write_line_raw("A\n"); + console.expect("βœ… Granted all net access."); + // "env" permissions + console.expect(concat!( + "β”Œ ⚠️ Deno requests env access to \"FOO\".\r\n", + "β”œ Requested by `Deno.permissions.query()` API.\r\n", + "β”œ Run again with --allow-env to bypass this prompt.\r\n", + "β”” Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all env permissions)", + )); + console.write_line_raw("A\n"); + console.expect("βœ… Granted all env access."); + // "sys" permissions + console.expect(concat!( + "β”Œ ⚠️ Deno requests sys access to \"loadavg\".\r\n", + "β”œ Requested by `Deno.permissions.query()` API.\r\n", + "β”œ Run again with --allow-sys to bypass this prompt.\r\n", + "β”” Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all sys permissions)", + )); + console.write_line_raw("A\n"); + console.expect("βœ… Granted all sys access."); + // "ffi" permissions + console.expect(concat!( + "β”Œ ⚠️ Deno requests ffi access to \"FOO\".\r\n", + "β”œ Requested by `Deno.permissions.query()` API.\r\n", + "β”œ Run again with --allow-ffi to bypass this prompt.\r\n", + "β”” Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all ffi permissions)", + )); + console.write_line_raw("A\n"); + console.expect("βœ… Granted all ffi access.") + }, + ); } #[test] fn permissions_prompt_allow_all_2() { - let args = "run --quiet run/permissions_prompt_allow_all_2.ts"; - use util::PtyData::*; - util::test_pty2(args, vec![ - // "env" permissions - Output("β”Œ ⚠️ Deno requests env access to \"FOO\".\r\nβ”œ Run again with --allow-env to bypass this prompt.\r\nβ”” Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all env permissions) >"), - Input("d\n"), - Output("βœ… Granted all env access.\r\n"), - // "sys" permissions - Output("β”Œ ⚠️ Deno requests sys access to \"FOO\".\r\nβ”œ Run again with --allow-sys to bypass this prompt.\r\nβ”” Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all sys permissions) >"), - Input("d\n"), - Output("βœ… Granted all sys access.\r\n"), - // "read" permissions - Output("β”Œ ⚠️ Deno requests read access to \"FOO\".\r\nβ”œ Run again with --allow-read to bypass this prompt.\r\nβ”” Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all read permissions) >"), - Input("d\n"), - Output("βœ… Granted all read access.\r\n"), - ]); + util::with_pty( + &["run", "--quiet", "run/permissions_prompt_allow_all_2.ts"], + |mut console| { + // "env" permissions + console.expect(concat!( + "β”Œ ⚠️ Deno requests env access to \"FOO\".\r\n", + "β”œ Run again with --allow-env to bypass this prompt.\r\n", + "β”” Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all env permissions)", + )); + console.write_line_raw("A"); + console.expect("βœ… Granted all env access."); + + // "sys" permissions + console.expect(concat!( + "β”Œ ⚠️ Deno requests sys access to \"loadavg\".\r\n", + "β”œ Requested by `Deno.loadavg()` API.\r\n", + "β”œ Run again with --allow-sys to bypass this prompt.\r\n", + "β”” Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all sys permissions)", + )); + console.write_line_raw("A"); + console.expect("βœ… Granted all sys access."); + + // "read" permissions + console.expect(concat!( + "β”Œ ⚠️ Deno requests read access to .\r\n", + "β”œ Requested by `Deno.cwd()` API.\r\n", + "β”œ Run again with --allow-read to bypass this prompt.\r\n", + "β”” Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all read permissions)", + )); + console.write_line_raw("A"); + console.expect("βœ… Granted all read access."); + }, + ); +} + +#[test] +fn permissions_prompt_allow_all_lowercase_a() { + util::with_pty( + &["run", "--quiet", "run/permissions_prompt_allow_all.ts"], + |mut console| { + // "run" permissions + console.expect(concat!( + "β”Œ ⚠️ Deno requests run access to \"FOO\".\r\n", + "β”œ Requested by `Deno.permissions.query()` API.\r\n", + "β”œ Run again with --allow-run to bypass this prompt.\r\n", + "β”” Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all run permissions)", + )); + console.write_line_raw("a"); + console.expect("Unrecognized option."); + }, + ); } itest!(_091_use_define_for_class_fields { @@ -2407,58 +2503,102 @@ mod permissions { #[test] fn _061_permissions_request() { - let args = "run --quiet run/061_permissions_request.ts"; - use util::PtyData::*; - util::test_pty2(args, vec![ - Output("⚠️ ️Deno requests read access to \"foo\". Run again with --allow-read to bypass this prompt.\r\n Allow? [y/n (y = yes allow, n = no deny)] "), - Input("y\n"), - Output("⚠️ ️Deno requests read access to \"bar\". Run again with --allow-read to bypass this prompt.\r\n Allow? [y/n (y = yes allow, n = no deny)]"), - Input("n\n"), - Output("granted\r\n"), - Output("prompt\r\n"), - Output("denied\r\n"), - ]); + util::with_pty( + &["run", "--quiet", "run/061_permissions_request.ts"], + |mut console| { + console.expect(concat!( + "β”Œ ⚠️ Deno requests read access to \"foo\".\r\n", + "β”œ Requested by `Deno.permissions.query()` API.\r\n", + "β”œ Run again with --allow-read to bypass this prompt.\r\n", + "β”” Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all read permissions)", + )); + console.write_line_raw("y"); + console.expect(concat!( + "β”Œ ⚠️ Deno requests read access to \"bar\".\r\n", + "β”œ Requested by `Deno.permissions.query()` API.\r\n", + "β”œ Run again with --allow-read to bypass this prompt.\r\n", + "β”” Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all read permissions)", + )); + console.write_line_raw("n"); + console.expect("granted"); + console.expect("prompt"); + console.expect("denied"); + }, + ); } #[test] fn _061_permissions_request_sync() { - let args = "run --quiet run/061_permissions_request_sync.ts"; - use util::PtyData::*; - util::test_pty2(args, vec![ - Output("⚠️ ️Deno requests read access to \"foo\". Run again with --allow-read to bypass this prompt.\r\n Allow? [y/n (y = yes allow, n = no deny)] "), - Input("y\n"), - Output("⚠️ ️Deno requests read access to \"bar\". Run again with --allow-read to bypass this prompt.\r\n Allow? [y/n (y = yes allow, n = no deny)]"), - Input("n\n"), - Output("granted\r\n"), - Output("prompt\r\n"), - Output("denied\r\n"), - ]); + util::with_pty( + &["run", "--quiet", "run/061_permissions_request_sync.ts"], + |mut console| { + console.expect(concat!( + "β”Œ ⚠️ Deno requests read access to \"foo\".\r\n", + "β”œ Requested by `Deno.permissions.query()` API.\r\n", + "β”œ Run again with --allow-read to bypass this prompt.\r\n", + "β”” Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all read permissions)", + )); + console.write_line_raw("y"); + console.expect(concat!( + "β”Œ ⚠️ Deno requests read access to \"bar\".\r\n", + "β”œ Requested by `Deno.permissions.query()` API.\r\n", + "β”œ Run again with --allow-read to bypass this prompt.\r\n", + "β”” Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all read permissions)", + )); + console.write_line_raw("n"); + console.expect("granted"); + console.expect("prompt"); + console.expect("denied"); + }, + ); } #[test] fn _062_permissions_request_global() { - let args = "run --quiet run/062_permissions_request_global.ts"; - use util::PtyData::*; - util::test_pty2(args, vec![ - Output("⚠️ ️Deno requests read access. Run again with --allow-read to bypass this prompt.\r\n Allow? [y/n (y = yes allow, n = no deny)] "), - Input("y\n"), - Output("PermissionStatus { state: \"granted\", onchange: null }\r\n"), - Output("PermissionStatus { state: \"granted\", onchange: null }\r\n"), - Output("PermissionStatus { state: \"granted\", onchange: null }\r\n"), - ]); + util::with_pty( + &["run", "--quiet", "run/062_permissions_request_global.ts"], + |mut console| { + console.expect(concat!( + "β”Œ ⚠️ Deno requests read access.\r\n", + "β”œ Requested by `Deno.permissions.query()` API.\r\n", + "β”œ Run again with --allow-read to bypass this prompt.\r\n", + "β”” Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all read permissions)", + )); + console.write_line_raw("y\n"); + console + .expect("PermissionStatus { state: \"granted\", onchange: null }"); + console + .expect("PermissionStatus { state: \"granted\", onchange: null }"); + console + .expect("PermissionStatus { state: \"granted\", onchange: null }"); + }, + ); } #[test] fn _062_permissions_request_global_sync() { - let args = "run --quiet run/062_permissions_request_global_sync.ts"; - use util::PtyData::*; - util::test_pty2(args, vec![ - Output("⚠️ ️Deno requests read access. Run again with --allow-read to bypass this prompt.\r\n Allow? [y/n (y = yes allow, n = no deny)] "), - Input("y\n"), - Output("PermissionStatus { state: \"granted\", onchange: null }\r\n"), - Output("PermissionStatus { state: \"granted\", onchange: null }\r\n"), - Output("PermissionStatus { state: \"granted\", onchange: null }\r\n"), - ]); + util::with_pty( + &[ + "run", + "--quiet", + "run/062_permissions_request_global_sync.ts", + ], + |mut console| { + console.expect(concat!( + "β”Œ ⚠️ Deno requests read access.\r\n", + "β”œ Requested by `Deno.permissions.query()` API.\r\n", + "β”œ Run again with --allow-read to bypass this prompt.\r\n", + "β”” Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all read permissions)", + )); + console.write_line_raw("y"); + console + .expect("PermissionStatus { state: \"granted\", onchange: null }"); + console + .expect("PermissionStatus { state: \"granted\", onchange: null }"); + console + .expect("PermissionStatus { state: \"granted\", onchange: null }"); + }, + ); } itest!(_063_permissions_revoke { @@ -2483,44 +2623,42 @@ mod permissions { #[test] fn _066_prompt() { - let args = "run --quiet --unstable run/066_prompt.ts"; - use util::PtyData::*; - util::test_pty2( - args, - vec![ - Output("What is your name? [Jane Doe] "), - Input("John Doe\n"), - Output("Your name is John Doe.\r\n"), - Output("What is your name? [Jane Doe] "), - Input("\n"), - Output("Your name is Jane Doe.\r\n"), - Output("Prompt "), - Input("foo\n"), - Output("Your input is foo.\r\n"), - Output("Question 0 [y/N] "), - Input("Y\n"), - Output("Your answer is true\r\n"), - Output("Question 1 [y/N] "), - Input("N\n"), - Output("Your answer is false\r\n"), - Output("Question 2 [y/N] "), - Input("yes\n"), - Output("Your answer is false\r\n"), - Output("Confirm [y/N] "), - Input("\n"), - Output("Your answer is false\r\n"), - Output("What is Windows EOL? "), - Input("windows\n"), - Output("Your answer is \"windows\"\r\n"), - Output("Hi [Enter] "), - Input("\n"), - Output("Alert [Enter] "), - Input("\n"), - Output("The end of test\r\n"), - Output("What is EOF? "), - Input("\n"), - Output("Your answer is null\r\n"), - ], + util::with_pty( + &["run", "--quiet", "--unstable", "run/066_prompt.ts"], + |mut console| { + console.expect("What is your name? [Jane Doe] "); + console.write_line_raw("John Doe"); + console.expect("Your name is John Doe."); + console.expect("What is your name? [Jane Doe] "); + console.write_line_raw(""); + console.expect("Your name is Jane Doe."); + console.expect("Prompt "); + console.write_line_raw("foo"); + console.expect("Your input is foo."); + console.expect("Question 0 [y/N] "); + console.write_line_raw("Y"); + console.expect("Your answer is true"); + console.expect("Question 1 [y/N] "); + console.write_line_raw("N"); + console.expect("Your answer is false"); + console.expect("Question 2 [y/N] "); + console.write_line_raw("yes"); + console.expect("Your answer is false"); + console.expect("Confirm [y/N] "); + console.write_line(""); + console.expect("Your answer is false"); + console.expect("What is Windows EOL? "); + console.write_line("windows"); + console.expect("Your answer is \"windows\""); + console.expect("Hi [Enter] "); + console.write_line(""); + console.expect("Alert [Enter] "); + console.write_line(""); + console.expect("The end of test"); + console.expect("What is EOF? "); + console.write_line(""); + console.expect("Your answer is null"); + }, ); } @@ -2577,19 +2715,28 @@ itest!(byte_order_mark { #[test] fn issue9750() { - use util::PtyData::*; - util::test_pty2( - "run --prompt run/issue9750.js", - vec![ - Output("Enter 'yy':\r\n"), - Input("yy\n"), - Output("⚠️ ️Deno requests env access. Run again with --allow-env to bypass this prompt.\r\n Allow? [y/n (y = yes allow, n = no deny)]"), - Input("n\n"), - Output("⚠️ ️Deno requests env access to \"SECRET\". Run again with --allow-env to bypass this prompt.\r\n Allow? [y/n (y = yes allow, n = no deny)]"), - Input("n\n"), - Output("error: Uncaught (in promise) PermissionDenied: Requires env access to \"SECRET\", run again with the --allow-env flag\r\n"), - ], - ); + util::with_pty(&["run", "--prompt", "run/issue9750.js"], |mut console| { + console.expect("Enter 'yy':"); + console.write_line_raw("yy"); + console.expect(concat!( + "β”Œ ⚠️ Deno requests env access.\r\n", + "β”œ Requested by `Deno.permissions.query()` API.\r\n", + "β”œ Run again with --allow-env to bypass this prompt.\r\n", + "β”” Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all env permissions)", + )); + console.write_line_raw("n"); + console.expect("Denied env access."); + console.expect(concat!( + "β”Œ ⚠️ Deno requests env access to \"SECRET\".\r\n", + "β”œ Run again with --allow-env to bypass this prompt.\r\n", + "β”” Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all env permissions)", + )); + console.write_line_raw("n"); + console.expect_all(&[ + "Denied env access to \"SECRET\".", + "PermissionDenied: Requires env access to \"SECRET\", run again with the --allow-env flag", + ]); + }); } // Regression test for https://github.com/denoland/deno/issues/11451. @@ -4100,87 +4247,94 @@ itest!(permission_args_quiet { }); // Regression test for https://github.com/denoland/deno/issues/16772 -#[ignore] #[test] +// todo(dsherret): getting a dns error on windows for some reason +#[cfg(unix)] fn file_fetcher_preserves_permissions() { let _guard = util::http_server(); - util::with_pty(&["repl"], |mut console| { - console.write_text( - "const a = import('http://127.0.0.1:4545/run/019_media_types.ts');", + util::with_pty(&["repl", "--quiet"], |mut console| { + console.write_line( + "const a = await import('http://localhost:4545/run/019_media_types.ts');", ); - console.write_text("y"); - console.write_line(""); - console.write_line("close();"); - let output = console.read_all_output(); - assert_contains!(output, "success"); - assert_contains!(output, "true"); + console.expect("Allow?"); + console.write_line_raw("y"); + console.expect_all(&["success", "true"]); }); } -#[ignore] #[test] fn stdio_streams_are_locked_in_permission_prompt() { - let _guard = util::http_server(); - util::with_pty(&[ - "repl", - "--allow-read=run/stdio_streams_are_locked_in_permission_prompt/worker.js,run/stdio_streams_are_locked_in_permission_prompt/text.txt" - ], |mut console| { - console.write_line( - r#"new Worker(`${Deno.cwd()}/run/stdio_streams_are_locked_in_permissions_prompt/worker.js`, { type: "module" }); - await Deno.writeTextFile("./run/stdio_streams_are_locked_in_permissions_prompt/text.txt", "some code");"#, - ); - console.write_line("y"); - console.write_line("close();"); - let output = console.read_all_output(); + let context = TestContextBuilder::new() + .use_http_server() + .use_copy_temp_dir("run/stdio_streams_are_locked_in_permission_prompt") + .build(); + context + .new_command() + .args("repl --allow-read") + .with_pty(|mut console| { + console.write_line(r#"const url = "file://" + Deno.cwd().replace("\\", "/") + "/run/stdio_streams_are_locked_in_permission_prompt/worker.js";"#); + console.expect("undefined"); + // ensure this file exists + console.write_line(r#"const _file = Deno.readTextFileSync("./run/stdio_streams_are_locked_in_permission_prompt/worker.js");"#); + console.expect("undefined"); + console.write_line(r#"new Worker(url, { type: "module" }); await Deno.writeTextFile("./text.txt", "some code");"#); + console.expect("Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all write permissions)"); + std::thread::sleep(Duration::from_millis(50)); // give the other thread some time to output + console.write_line_raw("invalid"); + console.expect("Unrecognized option."); + console.write_line_raw("y"); + console.expect("Granted write access to"); - let expected_output = r#"\x1b[1;1H\x1b[0JAre you sure you want to continue?"#; - assert_eq!(output, expected_output); - }); + // this output should now be shown below and not above + let expected_output = r#"Are you sure you want to continue?"#; + console.expect(expected_output); + }); } #[test] -#[ignore] fn permission_prompt_strips_ansi_codes_and_control_chars() { let _guard = util::http_server(); util::with_pty(&["repl"], |mut console| { console.write_line( r#"Deno.permissions.request({ name: "env", variable: "\rDo you like ice cream? y/n" });"# ); - console.write_line("close();"); - let output = console.read_all_output(); - - assert!(output.contains( - "β”Œ ⚠️ Deno requests env access to \"Do you like ice cream? y/n\"." - )); + console.expect( + "β”Œ ⚠️ Deno requests env access to \"Do you like ice cream? y/n\".", + ) }); util::with_pty(&["repl"], |mut console| { - console.write_line( - r#" -const boldANSI = "\u001b[1m" // bold -const unboldANSI = "\u001b[22m" // unbold + console.write_line_raw(r#"const boldANSI = "\u001b[1m";"#); + console.expect("undefined"); + console.write_line_raw(r#"const unboldANSI = "\u001b[22m";"#); + console.expect("undefined"); + console.write_line_raw(r#"const prompt = `β”Œ ⚠️ ${boldANSI}Deno requests run access to "echo"${unboldANSI}\n β”œ Requested by \`Deno.Command().output()`"#); + console.expect("undefined"); + console.write_line_raw(r#"const moveANSIUp = "\u001b[1A";"#); + console.expect("undefined"); + console.write_line_raw(r#"const clearANSI = "\u001b[2K";"#); + console.expect("undefined"); + console.write_line_raw(r#"const moveANSIStart = "\u001b[1000D";"#); + console.expect("undefined"); -const prompt = `β”Œ ⚠️ ${boldANSI}Deno requests run access to "echo"${unboldANSI} -β”œ Requested by \`Deno.Command().output()` - -const moveANSIUp = "\u001b[1A" // moves to the start of the line -const clearANSI = "\u001b[2K" // clears the line -const moveANSIStart = "\u001b[1000D" // moves to the start of the line - -Deno[Object.getOwnPropertySymbols(Deno)[0]].core.ops.op_spawn_child({ + console.write_line_raw( + r#"Deno[Deno.internal].core.ops.op_spawn_child({ cmd: "cat", - args: ["/etc/passwd"], + args: ["file.txt"], clearEnv: false, + cwd: undefined, env: [], + uid: undefined, + gid: undefined, stdin: "null", stdout: "inherit", - stderr: "piped" + stderr: "piped", + signal: undefined, + windowsRawArguments: false, }, moveANSIUp + clearANSI + moveANSIStart + prompt)"#, ); - console.write_line("close();"); - let output = console.read_all_output(); - assert!(output.contains(r#"β”Œ ⚠️ Deno requests run access to "cat""#)); + console.expect(r#"β”Œ ⚠️ Deno requests run access to "cat""#); }); } diff --git a/cli/tests/integration/task_tests.rs b/cli/tests/integration/task_tests.rs index 3dce90a0cb..f090deff5d 100644 --- a/cli/tests/integration/task_tests.rs +++ b/cli/tests/integration/task_tests.rs @@ -53,9 +53,12 @@ itest!(task_non_existent { #[test] fn task_emoji() { // this bug only appears when using a pty/tty - let args = "task --config task/deno_json/deno.json echo_emoji"; - use test_util::PtyData::*; - test_util::test_pty2(args, vec![Output("Task echo_emoji echo πŸ”₯\r\nπŸ”₯")]); + test_util::with_pty( + &["task", "--config", "task/deno_json/deno.json", "echo_emoji"], + |mut console| { + console.expect("Task echo_emoji echo πŸ”₯\r\nπŸ”₯"); + }, + ); } itest!(task_boolean_logic { diff --git a/cli/tests/integration/test_tests.rs b/cli/tests/integration/test_tests.rs index 107d137e7d..3a7f37db8b 100644 --- a/cli/tests/integration/test_tests.rs +++ b/cli/tests/integration/test_tests.rs @@ -446,6 +446,8 @@ itest!(parallel_output { }); #[test] +// todo(#18480): re-enable +#[ignore] fn sigint_with_hanging_test() { util::with_pty( &[ @@ -457,9 +459,10 @@ fn sigint_with_hanging_test() { |mut console| { std::thread::sleep(std::time::Duration::from_secs(1)); console.write_line("\x03"); + let text = console.read_until("hanging_test.ts:10:15"); wildcard_match( include_str!("../testdata/test/sigint_with_hanging_test.out"), - &console.read_all_output(), + &text, ); }, ); diff --git a/cli/tests/testdata/run/stdio_streams_are_locked_in_permission_prompt/text.txt b/cli/tests/testdata/run/stdio_streams_are_locked_in_permission_prompt/text.txt deleted file mode 100644 index e6177e9cf8..0000000000 --- a/cli/tests/testdata/run/stdio_streams_are_locked_in_permission_prompt/text.txt +++ /dev/null @@ -1 +0,0 @@ -\x1B[2J\x1B[1;1H \ No newline at end of file diff --git a/cli/tools/repl/editor.rs b/cli/tools/repl/editor.rs index e12b9314b1..79467a9961 100644 --- a/cli/tools/repl/editor.rs +++ b/cli/tools/repl/editor.rs @@ -249,69 +249,79 @@ fn validate(input: &str) -> ValidationResult { let mut in_template = false; let mut div_token_count_on_current_line = 0; let mut last_line_index = 0; + let mut queued_validation_error = None; + let tokens = deno_ast::lex(input, deno_ast::MediaType::TypeScript) + .into_iter() + .filter_map(|item| match item.inner { + deno_ast::TokenOrComment::Token(token) => Some((token, item.range)), + deno_ast::TokenOrComment::Comment { .. } => None, + }); - for item in deno_ast::lex(input, deno_ast::MediaType::TypeScript) { - let current_line_index = line_info.line_index(item.range.start); + for (token, range) in tokens { + let current_line_index = line_info.line_index(range.start); if current_line_index != last_line_index { div_token_count_on_current_line = 0; last_line_index = current_line_index; + + if let Some(error) = queued_validation_error { + return error; + } } - if let deno_ast::TokenOrComment::Token(token) = item.inner { - match token { - Token::BinOp(BinOpToken::Div) - | Token::AssignOp(AssignOp::DivAssign) => { - // it's too complicated to write code to detect regular expression literals - // which are no longer tokenized, so if a `/` or `/=` happens twice on the same - // line, then we bail - div_token_count_on_current_line += 1; - if div_token_count_on_current_line >= 2 { + match token { + Token::BinOp(BinOpToken::Div) | Token::AssignOp(AssignOp::DivAssign) => { + // it's too complicated to write code to detect regular expression literals + // which are no longer tokenized, so if a `/` or `/=` happens twice on the same + // line, then we bail + div_token_count_on_current_line += 1; + if div_token_count_on_current_line >= 2 { + return ValidationResult::Valid(None); + } + } + Token::BackQuote => in_template = !in_template, + Token::LParen | Token::LBracket | Token::LBrace | Token::DollarLBrace => { + stack.push(token) + } + Token::RParen | Token::RBracket | Token::RBrace => { + match (stack.pop(), token) { + (Some(Token::LParen), Token::RParen) + | (Some(Token::LBracket), Token::RBracket) + | (Some(Token::LBrace), Token::RBrace) + | (Some(Token::DollarLBrace), Token::RBrace) => {} + (Some(left), _) => { + // queue up a validation error to surface once we've finished examininig the current line + queued_validation_error = Some(ValidationResult::Invalid(Some( + format!("Mismatched pairs: {left:?} is not properly closed"), + ))); + } + (None, _) => { + // While technically invalid when unpaired, it should be V8's task to output error instead. + // Thus marked as valid with no info. return ValidationResult::Valid(None); } } - Token::BackQuote => in_template = !in_template, - Token::LParen - | Token::LBracket - | Token::LBrace - | Token::DollarLBrace => stack.push(token), - Token::RParen | Token::RBracket | Token::RBrace => { - match (stack.pop(), token) { - (Some(Token::LParen), Token::RParen) - | (Some(Token::LBracket), Token::RBracket) - | (Some(Token::LBrace), Token::RBrace) - | (Some(Token::DollarLBrace), Token::RBrace) => {} - (Some(left), _) => { - return ValidationResult::Invalid(Some(format!( - "Mismatched pairs: {left:?} is not properly closed" - ))) - } - (None, _) => { - // While technically invalid when unpaired, it should be V8's task to output error instead. - // Thus marked as valid with no info. - return ValidationResult::Valid(None); - } - } - } - Token::Error(error) => { - match error.kind() { - // If there is unterminated template, it continues to read input. - SyntaxError::UnterminatedTpl => {} - _ => { - // If it failed parsing, it should be V8's task to output error instead. - // Thus marked as valid with no info. - return ValidationResult::Valid(None); - } - } - } - _ => {} } + Token::Error(error) => { + match error.kind() { + // If there is unterminated template, it continues to read input. + SyntaxError::UnterminatedTpl => {} + _ => { + // If it failed parsing, it should be V8's task to output error instead. + // Thus marked as valid with no info. + return ValidationResult::Valid(None); + } + } + } + _ => {} } } - if !stack.is_empty() || in_template { - return ValidationResult::Incomplete; + if let Some(error) = queued_validation_error { + error + } else if !stack.is_empty() || in_template { + ValidationResult::Incomplete + } else { + ValidationResult::Valid(None) } - - ValidationResult::Valid(None) } impl Highlighter for EditorHelper { diff --git a/runtime/js/41_prompt.js b/runtime/js/41_prompt.js index 137f17dcfd..37fdaed77b 100644 --- a/runtime/js/41_prompt.js +++ b/runtime/js/41_prompt.js @@ -37,12 +37,15 @@ function prompt(message = "Prompt", defaultValue) { return null; } - core.print(`${message} `, false); - if (defaultValue) { - core.print(`[${defaultValue}] `, false); + message += ` [${defaultValue}]`; } + message += " "; + + // output in one shot to make the tests more reliable + core.print(message, false); + return readLineFromStdinSync() || defaultValue; } diff --git a/runtime/permissions/prompter.rs b/runtime/permissions/prompter.rs index d94264b1a0..502771636e 100644 --- a/runtime/permissions/prompter.rs +++ b/runtime/permissions/prompter.rs @@ -4,10 +4,11 @@ use crate::colors; use deno_core::error::AnyError; use deno_core::parking_lot::Mutex; use once_cell::sync::Lazy; +use std::fmt::Write; /// Helper function to strip ansi codes and ASCII control characters. fn strip_ansi_codes_and_ascii_control(s: &str) -> std::borrow::Cow { - console_static_text::strip_ansi_codes(s) + console_static_text::ansi::strip_ansi_codes(s) .chars() .filter(|c| !c.is_ascii_control()) .collect() @@ -221,17 +222,25 @@ impl PermissionPrompter for TtyPrompter { } else { "[y/n] (y = yes, allow; n = no, deny)".to_string() }; - eprint!("β”Œ {PERMISSION_EMOJI} "); - eprint!("{}", colors::bold("Deno requests ")); - eprint!("{}", colors::bold(message.clone())); - eprintln!("{}", colors::bold(".")); - if let Some(api_name) = api_name.clone() { - eprintln!("β”œ Requested by `{api_name}` API"); + + // output everything in one shot to make the tests more reliable + { + let mut output = String::new(); + write!(&mut output, "β”Œ {PERMISSION_EMOJI} ").unwrap(); + write!(&mut output, "{}", colors::bold("Deno requests ")).unwrap(); + write!(&mut output, "{}", colors::bold(message.clone())).unwrap(); + writeln!(&mut output, "{}", colors::bold(".")).unwrap(); + if let Some(api_name) = api_name.clone() { + writeln!(&mut output, "β”œ Requested by `{api_name}` API.").unwrap(); + } + let msg = format!("Run again with --allow-{name} to bypass this prompt."); + writeln!(&mut output, "β”œ {}", colors::italic(&msg)).unwrap(); + write!(&mut output, "β”” {}", colors::bold("Allow?")).unwrap(); + write!(&mut output, " {opts} > ").unwrap(); + + eprint!("{}", output); } - let msg = format!("Run again with --allow-{name} to bypass this prompt."); - eprintln!("β”œ {}", colors::italic(&msg)); - eprint!("β”” {}", colors::bold("Allow?")); - eprint!(" {opts} > "); + let value = loop { let mut input = String::new(); let stdin = std::io::stdin(); diff --git a/test_util/Cargo.toml b/test_util/Cargo.toml index a6e985b6dc..68443e4921 100644 --- a/test_util/Cargo.toml +++ b/test_util/Cargo.toml @@ -17,13 +17,14 @@ path = "src/test_server.rs" anyhow.workspace = true async-stream = "0.3.3" atty.workspace = true -backtrace = "0.3.67" base64.workspace = true +console_static_text.workspace = true flate2.workspace = true futures.workspace = true hyper = { workspace = true, features = ["server", "http1", "http2", "runtime"] } lazy_static = "1.4.0" lsp-types.workspace = true +nix.workspace = true once_cell.workspace = true os_pipe.workspace = true parking_lot.workspace = true diff --git a/test_util/src/builders.rs b/test_util/src/builders.rs index 9b300b9113..84befb57aa 100644 --- a/test_util/src/builders.rs +++ b/test_util/src/builders.rs @@ -10,7 +10,6 @@ use std::process::Command; use std::process::Stdio; use std::rc::Rc; -use backtrace::Backtrace; use os_pipe::pipe; use pretty_assertions::assert_eq; @@ -20,6 +19,7 @@ use crate::env_vars_for_npm_tests_no_sync_download; use crate::http_server; use crate::lsp::LspClientBuilder; use crate::new_deno_dir; +use crate::pty::Pty; use crate::strip_ansi_codes; use crate::testdata_path; use crate::wildcard_match; @@ -268,34 +268,29 @@ impl TestCommandBuilder { self } - pub fn run(&self) -> TestCommandOutput { - fn read_pipe_to_string(mut pipe: os_pipe::PipeReader) -> String { - let mut output = String::new(); - pipe.read_to_string(&mut output).unwrap(); - output - } - - fn sanitize_output(text: String, args: &[String]) -> String { - let mut text = strip_ansi_codes(&text).to_string(); - // deno test's output capturing flushes with a zero-width space in order to - // synchronize the output pipes. Occassionally this zero width space - // might end up in the output so strip it from the output comparison here. - if args.first().map(|s| s.as_str()) == Some("test") { - text = text.replace('\u{200B}', ""); - } - text - } - + fn build_cwd(&self) -> PathBuf { let cwd = self.cwd.as_ref().or(self.context.cwd.as_ref()); - let cwd = if self.context.use_temp_cwd { + if self.context.use_temp_cwd { assert!(cwd.is_none()); self.context.temp_dir.path().to_owned() } else if let Some(cwd_) = cwd { self.context.testdata_dir.join(cwd_) } else { self.context.testdata_dir.clone() - }; - let args = if self.args_vec.is_empty() { + } + } + + fn build_command_path(&self) -> PathBuf { + let command_name = &self.command_name; + if command_name == "deno" { + deno_exe_path() + } else { + PathBuf::from(command_name) + } + } + + fn build_args(&self) -> Vec { + if self.args_vec.is_empty() { std::borrow::Cow::Owned( self .args @@ -314,21 +309,58 @@ impl TestCommandBuilder { .map(|arg| { arg.replace("$TESTDATA", &self.context.testdata_dir.to_string_lossy()) }) - .collect::>(); - let command_name = &self.command_name; - let mut command = if command_name == "deno" { - Command::new(deno_exe_path()) - } else { - Command::new(command_name) - }; - command.env("DENO_DIR", self.context.deno_dir.path()); + .collect::>() + } - println!("command {} {}", command_name, args.join(" ")); + pub fn with_pty(&self, mut action: impl FnMut(Pty)) { + if !Pty::is_supported() { + return; + } + + let args = self.build_args(); + let args = args.iter().map(|s| s.as_str()).collect::>(); + let mut envs = self.envs.clone(); + if !envs.contains_key("NO_COLOR") { + // set this by default for pty tests + envs.insert("NO_COLOR".to_string(), "1".to_string()); + } + action(Pty::new( + &self.build_command_path(), + &args, + &self.build_cwd(), + Some(envs), + )) + } + + pub fn run(&self) -> TestCommandOutput { + fn read_pipe_to_string(mut pipe: os_pipe::PipeReader) -> String { + let mut output = String::new(); + pipe.read_to_string(&mut output).unwrap(); + output + } + + fn sanitize_output(text: String, args: &[String]) -> String { + let mut text = strip_ansi_codes(&text).to_string(); + // deno test's output capturing flushes with a zero-width space in order to + // synchronize the output pipes. Occassionally this zero width space + // might end up in the output so strip it from the output comparison here. + if args.first().map(|s| s.as_str()) == Some("test") { + text = text.replace('\u{200B}', ""); + } + text + } + + let cwd = self.build_cwd(); + let args = self.build_args(); + let mut command = Command::new(self.build_command_path()); + + println!("command {} {}", self.command_name, args.join(" ")); println!("command cwd {:?}", &cwd); command.args(args.iter()); if self.env_clear { command.env_clear(); } + command.env("DENO_DIR", self.context.deno_dir.path()); command.envs({ let mut envs = self.context.envs.clone(); for (key, value) in &self.envs { @@ -423,13 +455,10 @@ impl Drop for TestCommandOutput { fn drop(&mut self) { fn panic_unasserted_output(text: &str) { println!("OUTPUT\n{text}\nOUTPUT"); - panic!( - concat!( - "The non-empty text of the command was not asserted at {}. ", - "Call `output.skip_output_check()` to skip if necessary.", - ), - failed_position() - ); + panic!(concat!( + "The non-empty text of the command was not asserted. ", + "Call `output.skip_output_check()` to skip if necessary.", + ),); } if std::thread::panicking() { @@ -438,9 +467,8 @@ impl Drop for TestCommandOutput { // force the caller to assert these if !*self.asserted_exit_code.borrow() && self.exit_code != Some(0) { panic!( - "The non-zero exit code of the command was not asserted: {:?} at {}.", + "The non-zero exit code of the command was not asserted: {:?}", self.exit_code, - failed_position(), ) } @@ -511,6 +539,7 @@ impl TestCommandOutput { .expect("call .split_output() on the builder") } + #[track_caller] pub fn assert_exit_code(&self, expected_exit_code: i32) -> &Self { let actual_exit_code = self.exit_code(); @@ -518,26 +547,22 @@ impl TestCommandOutput { if *exit_code != expected_exit_code { self.print_output(); panic!( - "bad exit code, expected: {:?}, actual: {:?} at {}", - expected_exit_code, - exit_code, - failed_position(), + "bad exit code, expected: {:?}, actual: {:?}", + expected_exit_code, exit_code, ); } } else { self.print_output(); if let Some(signal) = self.signal() { panic!( - "process terminated by signal, expected exit code: {:?}, actual signal: {:?} at {}", + "process terminated by signal, expected exit code: {:?}, actual signal: {:?}", actual_exit_code, signal, - failed_position(), ); } else { panic!( - "process terminated without status code on non unix platform, expected exit code: {:?} at {}", + "process terminated without status code on non unix platform, expected exit code: {:?}", actual_exit_code, - failed_position(), ); } } @@ -554,14 +579,17 @@ impl TestCommandOutput { } } + #[track_caller] pub fn assert_matches_text(&self, expected_text: impl AsRef) -> &Self { self.inner_assert_matches_text(self.combined_output(), expected_text) } + #[track_caller] pub fn assert_matches_file(&self, file_path: impl AsRef) -> &Self { self.inner_assert_matches_file(self.combined_output(), file_path) } + #[track_caller] pub fn assert_stdout_matches_text( &self, expected_text: impl AsRef, @@ -569,6 +597,7 @@ impl TestCommandOutput { self.inner_assert_matches_text(self.stdout(), expected_text) } + #[track_caller] pub fn assert_stdout_matches_file( &self, file_path: impl AsRef, @@ -576,6 +605,7 @@ impl TestCommandOutput { self.inner_assert_matches_file(self.stdout(), file_path) } + #[track_caller] pub fn assert_stderr_matches_text( &self, expected_text: impl AsRef, @@ -583,6 +613,7 @@ impl TestCommandOutput { self.inner_assert_matches_text(self.stderr(), expected_text) } + #[track_caller] pub fn assert_stderrr_matches_file( &self, file_path: impl AsRef, @@ -590,6 +621,7 @@ impl TestCommandOutput { self.inner_assert_matches_file(self.stderr(), file_path) } + #[track_caller] fn inner_assert_matches_text( &self, actual: &str, @@ -597,15 +629,16 @@ impl TestCommandOutput { ) -> &Self { let expected = expected.as_ref(); if !expected.contains("[WILDCARD]") { - assert_eq!(actual, expected, "at {}", failed_position()); + assert_eq!(actual, expected); } else if !wildcard_match(expected, actual) { println!("OUTPUT START\n{actual}\nOUTPUT END"); println!("EXPECTED START\n{expected}\nEXPECTED END"); - panic!("pattern match failed at {}", failed_position()); + panic!("pattern match failed"); } self } + #[track_caller] fn inner_assert_matches_file( &self, actual: &str, @@ -620,21 +653,3 @@ impl TestCommandOutput { self.inner_assert_matches_text(actual, expected_text) } } - -fn failed_position() -> String { - let backtrace = Backtrace::new(); - - for frame in backtrace.frames() { - for symbol in frame.symbols() { - if let Some(filename) = symbol.filename() { - if !filename.to_string_lossy().ends_with("builders.rs") { - let line_num = symbol.lineno().unwrap_or(0); - let line_col = symbol.colno().unwrap_or(0); - return format!("{}:{}:{}", filename.display(), line_num, line_col); - } - } - } - } - - "".to_string() -} diff --git a/test_util/src/lib.rs b/test_util/src/lib.rs index d4effd88b1..b38d72cd9c 100644 --- a/test_util/src/lib.rs +++ b/test_util/src/lib.rs @@ -16,6 +16,7 @@ use hyper::StatusCode; use lazy_static::lazy_static; use npm::CUSTOM_NPM_PACKAGE_CACHE; use pretty_assertions::assert_eq; +use pty::Pty; use regex::Regex; use rustls::Certificate; use rustls::PrivateKey; @@ -24,7 +25,6 @@ use std::collections::HashMap; use std::convert::Infallible; use std::env; use std::io; -use std::io::Read; use std::io::Write; use std::mem::replace; use std::net::SocketAddr; @@ -92,13 +92,8 @@ pub const PERMISSION_VARIANTS: [&str; 5] = pub const PERMISSION_DENIED_PATTERN: &str = "PermissionDenied"; lazy_static! { - // STRIP_ANSI_RE and strip_ansi_codes are lifted from the "console" crate. - // Copyright 2017 Armin Ronacher . MIT License. - static ref STRIP_ANSI_RE: Regex = Regex::new( - r"[\x1b\x9b][\[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-PRZcf-nqry=><]" - ).unwrap(); - - static ref GUARD: Mutex = Mutex::new(HttpServerCount::default()); + static ref GUARD: Mutex = + Mutex::new(HttpServerCount::default()); } pub fn env_vars_for_npm_tests_no_sync_download() -> Vec<(String, String)> { @@ -1758,7 +1753,7 @@ pub fn http_server() -> HttpServerGuard { /// Helper function to strip ansi codes. pub fn strip_ansi_codes(s: &str) -> std::borrow::Cow { - STRIP_ANSI_RE.replace_all(s, "") + console_static_text::ansi::strip_ansi_codes(s) } pub fn run( @@ -2171,82 +2166,8 @@ pub fn pattern_match(pattern: &str, s: &str, wildcard: &str) -> bool { t.1.is_empty() } -pub enum PtyData { - Input(&'static str), - Output(&'static str), -} - -pub fn test_pty2(args: &str, data: Vec) { - use std::io::BufRead; - - with_pty(&args.split_whitespace().collect::>(), |console| { - let mut buf_reader = std::io::BufReader::new(console); - for d in data.iter() { - match d { - PtyData::Input(s) => { - println!("INPUT {}", s.escape_debug()); - buf_reader.get_mut().write_text(s); - - // Because of tty echo, we should be able to read the same string back. - assert!(s.ends_with('\n')); - let mut echo = String::new(); - buf_reader.read_line(&mut echo).unwrap(); - println!("ECHO: {}", echo.escape_debug()); - - // Windows may also echo the previous line, so only check the end - assert_ends_with!(normalize_text(&echo), normalize_text(s)); - } - PtyData::Output(s) => { - let mut line = String::new(); - if s.ends_with('\n') { - buf_reader.read_line(&mut line).unwrap(); - } else { - // assumes the buffer won't have overlapping virtual terminal sequences - while normalize_text(&line).len() < normalize_text(s).len() { - let mut buf = [0; 64 * 1024]; - let bytes_read = buf_reader.read(&mut buf).unwrap(); - assert!(bytes_read > 0); - let buf_str = std::str::from_utf8(&buf) - .unwrap() - .trim_end_matches(char::from(0)); - line += buf_str; - } - } - println!("OUTPUT {}", line.escape_debug()); - assert_eq!(normalize_text(&line), normalize_text(s)); - } - } - } - }); - - // This normalization function is not comprehensive - // and may need to updated as new scenarios emerge. - fn normalize_text(text: &str) -> String { - lazy_static! { - static ref MOVE_CURSOR_RIGHT_ONE_RE: Regex = - Regex::new(r"\x1b\[1C").unwrap(); - static ref FOUND_SEQUENCES_RE: Regex = - Regex::new(r"(\x1b\]0;[^\x07]*\x07)*(\x08)*(\x1b\[\d+X)*").unwrap(); - static ref CARRIAGE_RETURN_RE: Regex = - Regex::new(r"[^\n]*\r([^\n])").unwrap(); - } - - // any "move cursor right" sequences should just be a space - let text = MOVE_CURSOR_RIGHT_ONE_RE.replace_all(text, " "); - // replace additional virtual terminal sequences that strip ansi codes doesn't catch - let text = FOUND_SEQUENCES_RE.replace_all(&text, ""); - // strip any ansi codes, which also strips more terminal sequences - let text = strip_ansi_codes(&text); - // get rid of any text that is overwritten with only a carriage return - let text = CARRIAGE_RETURN_RE.replace_all(&text, "$1"); - // finally, trim surrounding whitespace - text.trim().to_string() - } -} - -pub fn with_pty(deno_args: &[&str], mut action: impl FnMut(Box)) { - if !atty::is(atty::Stream::Stdin) || !atty::is(atty::Stream::Stderr) { - eprintln!("Ignoring non-tty environment."); +pub fn with_pty(deno_args: &[&str], mut action: impl FnMut(Pty)) { + if !Pty::is_supported() { return; } @@ -2257,14 +2178,12 @@ pub fn with_pty(deno_args: &[&str], mut action: impl FnMut(Box)) { "DENO_DIR".to_string(), deno_dir.path().to_string_lossy().to_string(), ); - let pty = pty::create_pty( - &deno_exe_path().to_string_lossy().to_string(), + action(Pty::new( + &deno_exe_path(), deno_args, - testdata_path(), + &testdata_path(), Some(env_vars), - ); - - action(pty); + )) } pub struct WrkOutput { diff --git a/test_util/src/pty.rs b/test_util/src/pty.rs index f3bb2829f1..80d06881e4 100644 --- a/test_util/src/pty.rs +++ b/test_util/src/pty.rs @@ -1,36 +1,253 @@ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. use std::collections::HashMap; +use std::collections::HashSet; use std::io::Read; +use std::io::Write; use std::path::Path; +use std::time::Duration; +use std::time::Instant; -pub trait Pty: Read { - fn write_text(&mut self, text: &str); +use crate::strip_ansi_codes; - fn write_line(&mut self, text: &str) { - self.write_text(&format!("{text}\n")); +/// Points to know about when writing pty tests: +/// +/// - Consecutive writes cause issues where you might write while a prompt +/// is not showing. So when you write, always `.expect(...)` on the output. +/// - Similar to the last point, using `.expect(...)` can help make the test +/// more deterministic. If the test is flaky, try adding more `.expect(...)`s +pub struct Pty { + pty: Box, + read_bytes: Vec, + last_index: usize, +} + +impl Pty { + pub fn new( + program: &Path, + args: &[&str], + cwd: &Path, + env_vars: Option>, + ) -> Self { + let pty = create_pty(program, args, cwd, env_vars); + let mut pty = Self { + pty, + read_bytes: Vec::new(), + last_index: 0, + }; + if args[0] == "repl" && !args.contains(&"--quiet") { + // wait for the repl to start up before writing to it + pty.expect("exit using ctrl+d, ctrl+c, or close()"); + } + pty } - /// Reads the output to the EOF. - fn read_all_output(&mut self) -> String { - let mut text = String::new(); - self.read_to_string(&mut text).unwrap(); - text + pub fn is_supported() -> bool { + let is_mac_or_windows = cfg!(target_os = "macos") || cfg!(windows); + if is_mac_or_windows && std::env::var("CI").is_ok() { + // the pty tests give a ENOTTY error for Mac and don't really start up + // on the windows CI for some reason so ignore them for now + eprintln!("Ignoring windows CI."); + false + } else { + true + } + } + + #[track_caller] + pub fn write_raw(&mut self, line: impl AsRef) { + let line = if cfg!(windows) { + line.as_ref().replace('\n', "\r\n") + } else { + line.as_ref().to_string() + }; + if let Err(err) = self.pty.write(line.as_bytes()) { + panic!("{:#}", err) + } + self.pty.flush().unwrap(); + } + + #[track_caller] + pub fn write_line(&mut self, line: impl AsRef) { + self.write_line_raw(&line); + + // expect what was written to show up in the output + // due to "pty echo" + for line in line.as_ref().lines() { + self.expect(line); + } + } + + /// Writes a line without checking if it's in the output. + #[track_caller] + pub fn write_line_raw(&mut self, line: impl AsRef) { + self.write_raw(format!("{}\n", line.as_ref())); + } + + #[track_caller] + pub fn read_until(&mut self, end_text: impl AsRef) -> String { + self.read_until_with_advancing(|text| { + text + .find(end_text.as_ref()) + .map(|index| index + end_text.as_ref().len()) + }) + } + + #[track_caller] + pub fn expect(&mut self, text: impl AsRef) { + self.read_until(text.as_ref()); + } + + #[track_caller] + pub fn expect_any(&mut self, texts: &[&str]) { + self.read_until_with_advancing(|text| { + for find_text in texts { + if let Some(index) = text.find(find_text) { + return Some(index); + } + } + None + }); + } + + /// Consumes and expects to find all the text until a timeout is hit. + #[track_caller] + pub fn expect_all(&mut self, texts: &[&str]) { + let mut pending_texts: HashSet<&&str> = HashSet::from_iter(texts); + let mut max_index: Option = None; + self.read_until_with_advancing(|text| { + for pending_text in pending_texts.clone() { + if let Some(index) = text.find(pending_text) { + let index = index + pending_text.len(); + match &max_index { + Some(current) => { + if *current < index { + max_index = Some(index); + } + } + None => { + max_index = Some(index); + } + } + pending_texts.remove(pending_text); + } + } + if pending_texts.is_empty() { + max_index + } else { + None + } + }); + } + + /// Expects the raw text to be found, which may include ANSI codes. + /// Note: this expects the raw bytes in any output that has already + /// occurred or may occur within the next few seconds. + #[track_caller] + pub fn expect_raw_in_current_output(&mut self, text: impl AsRef) { + self.read_until_condition(|pty| { + let data = String::from_utf8_lossy(&pty.read_bytes); + data.contains(text.as_ref()) + }); + } + + #[track_caller] + fn read_until_with_advancing( + &mut self, + mut condition: impl FnMut(&str) -> Option, + ) -> String { + let mut final_text = String::new(); + self.read_until_condition(|pty| { + let text = pty.next_text(); + if let Some(end_index) = condition(&text) { + pty.last_index += end_index; + final_text = text[..end_index].to_string(); + true + } else { + false + } + }); + final_text + } + + #[track_caller] + fn read_until_condition( + &mut self, + mut condition: impl FnMut(&mut Self) -> bool, + ) { + let timeout_time = + Instant::now().checked_add(Duration::from_secs(5)).unwrap(); + while Instant::now() < timeout_time { + self.fill_more_bytes(); + if condition(self) { + return; + } + } + + let text = self.next_text(); + eprintln!( + "------ Start Full Text ------\n{:?}\n------- End Full Text -------", + String::from_utf8_lossy(&self.read_bytes) + ); + eprintln!("Next text: {:?}", text); + panic!("Timed out.") + } + + fn next_text(&self) -> String { + let text = String::from_utf8_lossy(&self.read_bytes).to_string(); + let text = strip_ansi_codes(&text); + text[self.last_index..].to_string() + } + + fn fill_more_bytes(&mut self) { + let mut buf = [0; 256]; + if let Ok(count) = self.pty.read(&mut buf) { + self.read_bytes.extend(&buf[..count]); + } else { + std::thread::sleep(Duration::from_millis(10)); + } } } +trait SystemPty: Read + Write {} + #[cfg(unix)] -pub fn create_pty( - program: impl AsRef, +fn setup_pty(master: &pty2::fork::Master) { + use nix::fcntl::fcntl; + use nix::fcntl::FcntlArg; + use nix::fcntl::OFlag; + use nix::sys::termios; + use nix::sys::termios::tcgetattr; + use nix::sys::termios::tcsetattr; + use nix::sys::termios::SetArg; + use std::os::fd::AsRawFd; + + let fd = master.as_raw_fd(); + let mut term = tcgetattr(fd).unwrap(); + // disable cooked mode + term.local_flags.remove(termios::LocalFlags::ICANON); + tcsetattr(fd, SetArg::TCSANOW, &term).unwrap(); + + // turn on non-blocking mode so we get timeouts + let flags = fcntl(fd, FcntlArg::F_GETFL).unwrap(); + let new_flags = OFlag::from_bits_truncate(flags) | OFlag::O_NONBLOCK; + fcntl(fd, FcntlArg::F_SETFL(new_flags)).unwrap(); +} + +#[cfg(unix)] +fn create_pty( + program: &Path, args: &[&str], - cwd: impl AsRef, + cwd: &Path, env_vars: Option>, -) -> Box { +) -> Box { let fork = pty2::fork::Fork::from_ptmx().unwrap(); if fork.is_parent().is_ok() { + let master = fork.is_parent().unwrap(); + setup_pty(&master); Box::new(unix::UnixPty { fork }) } else { - std::process::Command::new(program.as_ref()) + std::process::Command::new(program) .current_dir(cwd) .args(args) .envs(env_vars.unwrap_or_default()) @@ -47,7 +264,7 @@ mod unix { use std::io::Read; use std::io::Write; - use super::Pty; + use super::SystemPty; pub struct UnixPty { pub fork: pty2::fork::Fork, @@ -55,16 +272,18 @@ mod unix { impl Drop for UnixPty { fn drop(&mut self) { - self.fork.wait().unwrap(); + use nix::sys::signal::kill; + use nix::sys::signal::Signal; + use nix::unistd::Pid; + + if let pty2::fork::Fork::Parent(child_pid, _) = self.fork { + let pid = Pid::from_raw(child_pid); + kill(pid, Signal::SIGTERM).unwrap() + } } } - impl Pty for UnixPty { - fn write_text(&mut self, text: &str) { - let mut master = self.fork.is_parent().unwrap(); - master.write_all(text.as_bytes()).unwrap(); - } - } + impl SystemPty for UnixPty {} impl Read for UnixPty { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { @@ -72,29 +291,36 @@ mod unix { master.read(buf) } } + + impl Write for UnixPty { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + let mut master = self.fork.is_parent().unwrap(); + master.write(buf) + } + + fn flush(&mut self) -> std::io::Result<()> { + let mut master = self.fork.is_parent().unwrap(); + master.flush() + } + } } #[cfg(target_os = "windows")] -pub fn create_pty( - program: impl AsRef, +fn create_pty( + program: &Path, args: &[&str], - cwd: impl AsRef, + cwd: &Path, env_vars: Option>, -) -> Box { - let pty = windows::WinPseudoConsole::new( - program, - args, - &cwd.as_ref().to_string_lossy(), - env_vars, - ); +) -> Box { + let pty = windows::WinPseudoConsole::new(program, args, cwd, env_vars); Box::new(pty) } #[cfg(target_os = "windows")] mod windows { use std::collections::HashMap; + use std::io::ErrorKind; use std::io::Read; - use std::io::Write; use std::path::Path; use std::ptr; use std::time::Duration; @@ -105,11 +331,13 @@ mod windows { use winapi::shared::winerror::S_OK; use winapi::um::consoleapi::ClosePseudoConsole; use winapi::um::consoleapi::CreatePseudoConsole; + use winapi::um::fileapi::FlushFileBuffers; use winapi::um::fileapi::ReadFile; use winapi::um::fileapi::WriteFile; use winapi::um::handleapi::DuplicateHandle; use winapi::um::handleapi::INVALID_HANDLE_VALUE; use winapi::um::namedpipeapi::CreatePipe; + use winapi::um::namedpipeapi::PeekNamedPipe; use winapi::um::processthreadsapi::CreateProcessW; use winapi::um::processthreadsapi::DeleteProcThreadAttributeList; use winapi::um::processthreadsapi::GetCurrentProcess; @@ -127,7 +355,7 @@ mod windows { use winapi::um::winnt::DUPLICATE_SAME_ACCESS; use winapi::um::winnt::HANDLE; - use super::Pty; + use super::SystemPty; macro_rules! assert_win_success { ($expression:expr) => { @@ -138,6 +366,15 @@ mod windows { }; } + macro_rules! handle_err { + ($expression:expr) => { + let success = $expression; + if success != TRUE { + return Err(std::io::Error::last_os_error()); + } + }; + } + pub struct WinPseudoConsole { stdin_write_handle: WinHandle, stdout_read_handle: WinHandle, @@ -149,9 +386,9 @@ mod windows { impl WinPseudoConsole { pub fn new( - program: impl AsRef, + program: &Path, args: &[&str], - cwd: &str, + cwd: &Path, maybe_env_vars: Option>, ) -> Self { // https://docs.microsoft.com/en-us/windows/console/creating-a-pseudoconsole-session @@ -184,15 +421,19 @@ mod windows { let mut proc_info: PROCESS_INFORMATION = std::mem::zeroed(); let command = format!( "\"{}\" {}", - program.as_ref().to_string_lossy(), - args.join(" ") + program.to_string_lossy(), + args + .iter() + .map(|a| format!("\"{}\"", a)) + .collect::>() + .join(" ") ) .trim() .to_string(); - let mut application_str = - to_windows_str(&program.as_ref().to_string_lossy()); + let mut application_str = to_windows_str(&program.to_string_lossy()); let mut command_str = to_windows_str(&command); - let mut cwd = to_windows_str(cwd); + let cwd = cwd.to_string_lossy().replace('/', "\\"); + let mut cwd = to_windows_str(&cwd); assert_win_success!(CreateProcessW( application_str.as_mut_ptr(), @@ -242,45 +483,47 @@ mod windows { impl Read for WinPseudoConsole { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { - loop { - let mut bytes_read = 0; - // SAFETY: - // winapi call - let success = unsafe { - ReadFile( - self.stdout_read_handle.as_raw_handle(), - buf.as_mut_ptr() as _, - buf.len() as u32, - &mut bytes_read, - ptr::null_mut(), - ) - }; - - // ignore zero-byte writes - let is_zero_byte_write = bytes_read == 0 && success == TRUE; - if !is_zero_byte_write { - return Ok(bytes_read as usize); - } + // don't do a blocking read in order to support timing out + let mut bytes_available = 0; + // SAFETY: winapi call + handle_err!(unsafe { + PeekNamedPipe( + self.stdout_read_handle.as_raw_handle(), + ptr::null_mut(), + 0, + ptr::null_mut(), + &mut bytes_available, + ptr::null_mut(), + ) + }); + if bytes_available == 0 { + return Err(std::io::Error::new(ErrorKind::WouldBlock, "Would block.")); } + + let mut bytes_read = 0; + // SAFETY: winapi call + handle_err!(unsafe { + ReadFile( + self.stdout_read_handle.as_raw_handle(), + buf.as_mut_ptr() as _, + buf.len() as u32, + &mut bytes_read, + ptr::null_mut(), + ) + }); + + Ok(bytes_read as usize) } } - impl Pty for WinPseudoConsole { - fn write_text(&mut self, text: &str) { - // windows pseudo console requires a \r\n to do a newline - let newline_re = regex::Regex::new("\r?\n").unwrap(); - self - .write_all(newline_re.replace_all(text, "\r\n").as_bytes()) - .unwrap(); - } - } + impl SystemPty for WinPseudoConsole {} impl std::io::Write for WinPseudoConsole { fn write(&mut self, buffer: &[u8]) -> std::io::Result { let mut bytes_written = 0; // SAFETY: // winapi call - assert_win_success!(unsafe { + handle_err!(unsafe { WriteFile( self.stdin_write_handle.as_raw_handle(), buffer.as_ptr() as *const _, @@ -293,6 +536,10 @@ mod windows { } fn flush(&mut self) -> std::io::Result<()> { + // SAFETY: winapi call + handle_err!(unsafe { + FlushFileBuffers(self.stdin_write_handle.as_raw_handle()) + }); Ok(()) } } @@ -307,12 +554,10 @@ mod windows { } pub fn duplicate(&self) -> WinHandle { - // SAFETY: - // winapi call + // SAFETY: winapi call let process_handle = unsafe { GetCurrentProcess() }; let mut duplicate_handle = ptr::null_mut(); - // SAFETY: - // winapi call + // SAFETY: winapi call assert_win_success!(unsafe { DuplicateHandle( process_handle, @@ -410,8 +655,7 @@ mod windows { impl Drop for ProcThreadAttributeList { fn drop(&mut self) { - // SAFETY: - // winapi call + // SAFETY: winapi call unsafe { DeleteProcThreadAttributeList(self.as_mut_ptr()) }; } } @@ -420,8 +664,7 @@ mod windows { let mut read_handle = std::ptr::null_mut(); let mut write_handle = std::ptr::null_mut(); - // SAFETY: - // Creating an anonymous pipe with winapi. + // SAFETY: Creating an anonymous pipe with winapi. assert_win_success!(unsafe { CreatePipe(&mut read_handle, &mut write_handle, ptr::null_mut(), 0) });