nushell/tests/parsing/mod.rs
YizhePKU 6d2cb4382a
Fix circular source causing Nushell to crash (#12262)
# Description

EngineState now tracks the script currently running, instead of the
parent directory of the script. This also provides an easy way to expose
the current running script to the user (Issue #12195).

Similarly, StateWorkingSet now tracks scripts instead of directories.
`parsed_module_files` and `currently_parsed_pwd` are merged into one
variable, `scripts`, which acts like a stack for tracking the current
running script (which is on the top of the stack).

Circular import check is added for `source` operations, in addition to
module import. A simple testcase is added for circular source.

<!--
if this PR closes one or more issues, you can automatically link the PR
with
them by using one of the [*linking
keywords*](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword),
e.g.
- this PR should close #xxxx
- fixes #xxxx

you can also mention related issues, PRs or discussions!
-->


<!--
Thank you for improving Nushell. Please, check our [contributing
guide](../CONTRIBUTING.md) and talk to the core team before making major
changes.

Description of your pull request goes here. **Provide examples and/or
screenshots** if your changes affect the user experience.
-->

# User-Facing Changes
<!-- List of all changes that impact the user experience here. This
helps us keep track of breaking changes. -->

It shouldn't have any user facing changes.
2024-04-19 09:38:08 +03:00

369 lines
9.5 KiB
Rust

use nu_test_support::fs::Stub::FileWithContentToBeTrimmed;
use nu_test_support::playground::Playground;
use nu_test_support::{nu, pipeline};
use pretty_assertions::assert_eq;
use rstest::rstest;
#[test]
fn source_file_relative_to_file() {
let actual = nu!(cwd: "tests/parsing/samples", "
nu source_file_relative.nu
");
assert_eq!(actual.out, "5");
}
#[test]
fn source_file_relative_to_config() {
let actual = nu!("
nu --config tests/parsing/samples/source_file_relative.nu --commands ''
");
assert_eq!(actual.out, "5");
}
#[test]
fn source_const_file() {
let actual = nu!(cwd: "tests/parsing/samples",
"
const file = 'single_line.nu'
source $file
");
assert_eq!(actual.out, "5");
}
#[test]
fn source_circular() {
let actual = nu!(cwd: "tests/parsing/samples", "
nu source_circular_1.nu
");
assert!(actual.err.contains("nu::parser::circular_import"));
}
#[test]
fn run_nu_script_single_line() {
let actual = nu!(cwd: "tests/parsing/samples", "
nu single_line.nu
");
assert_eq!(actual.out, "5");
}
#[test]
fn run_nu_script_multiline_start_pipe() {
let actual = nu!(cwd: "tests/parsing/samples", "
nu multiline_start_pipe.nu
");
assert_eq!(actual.out, "4");
}
#[test]
fn run_nu_script_multiline_start_pipe_win() {
let actual = nu!(cwd: "tests/parsing/samples", "
nu multiline_start_pipe_win.nu
");
assert_eq!(actual.out, "3");
}
#[test]
fn run_nu_script_multiline_end_pipe() {
let actual = nu!(cwd: "tests/parsing/samples", "
nu multiline_end_pipe.nu
");
assert_eq!(actual.out, "2");
}
#[test]
fn run_nu_script_multiline_end_pipe_win() {
let actual = nu!(cwd: "tests/parsing/samples", "
nu multiline_end_pipe_win.nu
");
assert_eq!(actual.out, "3");
}
#[test]
fn parse_file_relative_to_parsed_file_simple() {
Playground::setup("relative_files_simple", |dirs, sandbox| {
sandbox
.mkdir("lol")
.mkdir("lol/lol")
.with_files(vec![FileWithContentToBeTrimmed(
"lol/lol/lol.nu",
"
use ../lol_shell.nu
$env.LOL = (lol_shell ls)
",
)])
.with_files(vec![FileWithContentToBeTrimmed(
"lol/lol_shell.nu",
r#"
export def ls [] { "lol" }
"#,
)]);
let actual = nu!(
cwd: dirs.test(), pipeline(
"
source-env lol/lol/lol.nu;
$env.LOL
"
));
assert_eq!(actual.out, "lol");
})
}
#[test]
fn predecl_signature_single_inp_out_type() {
Playground::setup("predecl_signature_single_inp_out_type", |dirs, sandbox| {
sandbox.with_files(vec![FileWithContentToBeTrimmed(
"spam1.nu",
"
def main [] { foo }
def foo []: nothing -> nothing { print 'foo' }
",
)]);
let actual = nu!(cwd: dirs.test(), pipeline("nu spam1.nu"));
assert_eq!(actual.out, "foo");
})
}
#[test]
fn predecl_signature_multiple_inp_out_types() {
Playground::setup(
"predecl_signature_multiple_inp_out_types",
|dirs, sandbox| {
sandbox.with_files(vec![FileWithContentToBeTrimmed(
"spam2.nu",
"
def main [] { foo }
def foo []: [nothing -> string, string -> string] { 'foo' }
",
)]);
let actual = nu!(cwd: dirs.test(), pipeline("nu spam2.nu"));
assert_eq!(actual.out, "foo");
},
)
}
#[ignore]
#[test]
fn parse_file_relative_to_parsed_file() {
Playground::setup("relative_files", |dirs, sandbox| {
sandbox
.mkdir("lol")
.mkdir("lol/lol")
.with_files(vec![FileWithContentToBeTrimmed(
"lol/lol/lol.nu",
"
source-env ../../foo.nu
use ../lol_shell.nu
overlay use ../../lol/lol_shell.nu
$env.LOL = $'($env.FOO) (lol_shell ls) (ls)'
",
)])
.with_files(vec![FileWithContentToBeTrimmed(
"lol/lol_shell.nu",
r#"
export def ls [] { "lol" }
"#,
)])
.with_files(vec![FileWithContentToBeTrimmed(
"foo.nu",
"
$env.FOO = 'foo'
",
)]);
let actual = nu!(
cwd: dirs.test(), pipeline(
"
source-env lol/lol/lol.nu;
$env.LOL
"
));
assert_eq!(actual.out, "foo lol lol");
})
}
#[test]
fn parse_file_relative_to_parsed_file_dont_use_cwd_1() {
Playground::setup("relative_files", |dirs, sandbox| {
sandbox
.mkdir("lol")
.with_files(vec![FileWithContentToBeTrimmed(
"lol/lol.nu",
"
source-env foo.nu
",
)])
.with_files(vec![FileWithContentToBeTrimmed(
"lol/foo.nu",
"
$env.FOO = 'good'
",
)])
.with_files(vec![FileWithContentToBeTrimmed(
"foo.nu",
"
$env.FOO = 'bad'
",
)]);
let actual = nu!(
cwd: dirs.test(), pipeline(
"
source-env lol/lol.nu;
$env.FOO
"
));
assert_eq!(actual.out, "good");
})
}
#[test]
fn parse_file_relative_to_parsed_file_dont_use_cwd_2() {
Playground::setup("relative_files", |dirs, sandbox| {
sandbox
.mkdir("lol")
.with_files(vec![FileWithContentToBeTrimmed(
"lol/lol.nu",
"
source-env foo.nu
",
)])
.with_files(vec![FileWithContentToBeTrimmed(
"foo.nu",
"
$env.FOO = 'bad'
",
)]);
let actual = nu!(
cwd: dirs.test(), pipeline(
"
source-env lol/lol.nu
"
));
assert!(actual.err.contains("File not found"));
})
}
#[test]
fn parse_export_env_in_module() {
let actual = nu!("
module spam { export-env { } }
");
assert!(actual.err.is_empty());
}
#[test]
fn parse_export_env_missing_block() {
let actual = nu!("
module spam { export-env }
");
assert!(actual.err.contains("missing block"));
}
#[test]
fn call_command_with_non_ascii_argument() {
let actual = nu!("
def nu-arg [--umlaut(-ö): int] {}
nu-arg -ö 42
");
assert_eq!(actual.err.len(), 0);
}
#[test]
fn parse_long_duration() {
let actual = nu!(r#"
"78.797877879789789sec" | into duration
"#);
assert_eq!(actual.out, "1min 18sec 797ms");
}
#[rstest]
#[case("def test [ --a: any = 32 ] {}")]
#[case("def test [ --a: number = 32 ] {}")]
#[case("def test [ --a: number = 32.0 ] {}")]
#[case("def test [ --a: list<any> = [ 1 2 3 ] ] {}")]
#[case("def test [ --a: record<a: int b: string> = { a: 32 b: 'qwe' c: 'wqe' } ] {}")]
#[case("def test [ --a: record<a: any b: any> = { a: 32 b: 'qwe'} ] {}")]
#[case("def test []: int -> int { 1 }")]
#[case("def test []: string -> string { 'qwe' }")]
#[case("def test []: nothing -> nothing { null }")]
#[case("def test []: list<string> -> list<string> { [] }")]
#[case("def test []: record<a: int b: int> -> record<c: int e: int> { {c: 1 e: 1} }")]
#[case("def test []: table<a: int b: int> -> table<c: int e: int> { [ {c: 1 e: 1} ] }")]
#[case("def test []: nothing -> record<c: int e: int> { {c: 1 e: 1} }")]
fn parse_function_signature(#[case] phrase: &str) {
let actual = nu!(phrase);
assert!(actual.err.is_empty());
}
#[rstest]
#[case("def test [ in ] {}")]
#[case("def test [ in: string ] {}")]
#[case("def test [ nu: int ] {}")]
#[case("def test [ env: record<> ] {}")]
#[case("def test [ --env ] {}")]
#[case("def test [ --nu: int ] {}")]
#[case("def test [ --in (-i): list<any> ] {}")]
#[case("def test [ a: string, b: int, in: table<a: int b: int> ] {}")]
#[case("def test [ env, in, nu ] {}")]
fn parse_function_signature_name_is_builtin_var(#[case] phrase: &str) {
let actual = nu!(phrase);
assert!(actual.err.contains("nu::parser::name_is_builtin_var"))
}
#[rstest]
#[case("let a: int = 1")]
#[case("let a: string = 'qwe'")]
#[case("let a: nothing = null")]
#[case("let a: list<string> = []")]
#[case("let a: record<a: int b: int> = {a: 1 b: 1}")]
#[case("let a: table<a: int b: int> = [[a b]; [1 2] [3 4]]")]
#[case("let a: record<a: record<name: string> b: int> = {a: {name: bob} b: 1}")]
fn parse_let_signature(#[case] phrase: &str) {
let actual = nu!(phrase);
assert!(actual.err.is_empty());
}
#[test]
fn parse_let_signature_missing_colon() {
let actual = nu!("let a int = 1");
assert!(actual.err.contains("nu::parser::extra_tokens"));
}
#[test]
fn parse_mut_signature_missing_colon() {
let actual = nu!("mut a record<a: int b: int> = {a: 1 b: 1}");
assert!(actual.err.contains("nu::parser::extra_tokens"));
}
#[test]
fn parse_const_signature_missing_colon() {
let actual = nu!("const a string = 'Hello World\n'");
assert!(actual.err.contains("nu::parser::extra_tokens"));
}