summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock63
-rw-r--r--Cargo.toml2
-rw-r--r--cli/tests/integration/repl_tests.rs808
-rw-r--r--cli/tests/integration/run_tests.rs580
-rw-r--r--cli/tests/integration/task_tests.rs9
-rw-r--r--cli/tests/integration/test_tests.rs5
-rw-r--r--cli/tests/testdata/run/stdio_streams_are_locked_in_permission_prompt/text.txt1
-rw-r--r--cli/tools/repl/editor.rs108
-rw-r--r--runtime/js/41_prompt.js9
-rw-r--r--runtime/permissions/prompter.rs31
-rw-r--r--test_util/Cargo.toml3
-rw-r--r--test_util/src/builders.rs151
-rw-r--r--test_util/src/lib.rs101
-rw-r--r--test_util/src/pty.rs399
14 files changed, 1195 insertions, 1075 deletions
diff --git a/Cargo.lock b/Cargo.lock
index f28c00b55..bb383249c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -13,15 +13,6 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -209,21 +200,6 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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]]
@@ -1999,12 +1975,6 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2725,15 +2695,6 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2913,15 +2874,6 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3589,12 +3541,6 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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 b4ee768be..b8cfa99a1 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 f7bd627c3..82cae5024 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.expect("Bad character escape sequence, expected 4 hex characters");
+
console.write_line("'");
- console.write_line("[{'a'}];");
- console.write_line("close();");
+ console.expect("Unterminated string constant");
- 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("[{'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 <eof> 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 <eof>");
+ // 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 = <div />;"]),
- 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 = <div />;");
+ 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 4a0581b84..4504c970d 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 <CWD>.\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 expected_output = r#"\x1b[1;1H\x1b[0JAre you sure you want to continue?"#;
- assert_eq!(output, expected_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");
+
+ // 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
-
-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#"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");
+
+ 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 3dce90a0c..f090deff5 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 107d137e7..3a7f37db8 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 e6177e9cf..000000000
--- 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 e12b9314b..79467a996 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;
-
- for item in deno_ast::lex(input, deno_ast::MediaType::TypeScript) {
- let current_line_index = line_info.line_index(item.range.start);
+ 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 (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 {
- return ValidationResult::Valid(None);
- }
+ 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), _) => {
- 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::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::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 137f17dcf..37fdaed77 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 d94264b1a..502771636 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<str> {
- 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 a6e985b6d..68443e492 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 9b300b911..84befb57a 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<String> {
+ 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::<Vec<_>>();
- 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::<Vec<_>>()
+ }
+
+ 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::<Vec<_>>();
+ 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),
+ ))
+ }
- println!("command {} {}", command_name, args.join(" "));
+ 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<str>) -> &Self {
self.inner_assert_matches_text(self.combined_output(), expected_text)
}
+ #[track_caller]
pub fn assert_matches_file(&self, file_path: impl AsRef<Path>) -> &Self {
self.inner_assert_matches_file(self.combined_output(), file_path)
}
+ #[track_caller]
pub fn assert_stdout_matches_text(
&self,
expected_text: impl AsRef<str>,
@@ -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<Path>,
@@ -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<str>,
@@ -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<Path>,
@@ -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);
- }
- }
- }
- }
-
- "<unknown>".to_string()
-}
diff --git a/test_util/src/lib.rs b/test_util/src/lib.rs
index d4effd88b..b38d72cd9 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 <armin.ronacher@active-4.com>. 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<HttpServerCount> = Mutex::new(HttpServerCount::default());
+ static ref GUARD: Mutex<HttpServerCount> =
+ 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<str> {
- 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<PtyData>) {
- use std::io::BufRead;
-
- with_pty(&args.split_whitespace().collect::<Vec<_>>(), |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<dyn pty::Pty>)) {
- 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<dyn pty::Pty>)) {
"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 f3bb2829f..80d06881e 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;
+
+use crate::strip_ansi_codes;
+
+/// 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<dyn SystemPty>,
+ read_bytes: Vec<u8>,
+ last_index: usize,
+}
+
+impl Pty {
+ pub fn new(
+ program: &Path,
+ args: &[&str],
+ cwd: &Path,
+ env_vars: Option<HashMap<String, String>>,
+ ) -> 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
+ }
+
+ 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<str>) {
+ 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<str>) {
+ 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<str>) {
+ self.write_raw(format!("{}\n", line.as_ref()));
+ }
+
+ #[track_caller]
+ pub fn read_until(&mut self, end_text: impl AsRef<str>) -> 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<str>) {
+ 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<usize> = 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<str>) {
+ 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<usize>,
+ ) -> 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;
+ }
+ }
-pub trait Pty: Read {
- fn write_text(&mut self, text: &str);
+ 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 write_line(&mut self, text: &str) {
- self.write_text(&format!("{text}\n"));
+ 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()
}
- /// 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
+ 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<Path>,
+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<Path>,
+ cwd: &Path,
env_vars: Option<HashMap<String, String>>,
-) -> Box<dyn Pty> {
+) -> Box<dyn SystemPty> {
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,46 +272,55 @@ 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;
- 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();
+ if let pty2::fork::Fork::Parent(child_pid, _) = self.fork {
+ let pid = Pid::from_raw(child_pid);
+ kill(pid, Signal::SIGTERM).unwrap()
+ }
}
}
+ impl SystemPty for UnixPty {}
+
impl Read for UnixPty {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let mut master = self.fork.is_parent().unwrap();
master.read(buf)
}
}
+
+ impl Write for UnixPty {
+ fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
+ 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<Path>,
+fn create_pty(
+ program: &Path,
args: &[&str],
- cwd: impl AsRef<Path>,
+ cwd: &Path,
env_vars: Option<HashMap<String, String>>,
-) -> Box<dyn Pty> {
- let pty = windows::WinPseudoConsole::new(
- program,
- args,
- &cwd.as_ref().to_string_lossy(),
- env_vars,
- );
+) -> Box<dyn SystemPty> {
+ 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<Path>,
+ program: &Path,
args: &[&str],
- cwd: &str,
+ cwd: &Path,
maybe_env_vars: Option<HashMap<String, String>>,
) -> 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::<Vec<_>>()
+ .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<usize> {
- 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."));
}
- }
- }
- 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();
+ 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 SystemPty for WinPseudoConsole {}
+
impl std::io::Write for WinPseudoConsole {
fn write(&mut self, buffer: &[u8]) -> std::io::Result<usize> {
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)
});