nushell/tests/eval/mod.rs
Ian Manske 4d3283e235
Change append operator to concatenation operator (#14344)
# Description

The "append" operator currently serves as both the append operator and
the concatenation operator. This dual role creates ambiguity when
operating on nested lists.

```nu
[1 2] ++ 3     # appends a value to a list [1 2 3]
[1 2] ++ [3 4] # concatenates two lists    [1 2 3 4]

[[1 2] [3 4]] ++ [5 6]
# does this give [[1 2] [3 4] [5 6]]
# or             [[1 2] [3 4] 5 6]  
```

Another problem is that `++=` can change the type of a variable:
```nu
mut str = 'hello '
$str ++= ['world']
($str | describe) == list<string>
```

Note that appending is only relevant for lists, but concatenation is
relevant for lists, strings, and binary values. Additionally, appending
can be expressed in terms of concatenation (see example below). So, this
PR changes the `++` operator to only perform concatenation.

# User-Facing Changes

Using the `++` operator with a list and a non-list value will now be a
compile time or runtime error.
```nu
mut list = []
$list ++= 1 # error
```
Instead, concatenate a list with one element:
```nu
$list ++= [1]
```
Or use `append`:
```nu
$list = $list | append 1
```

# After Submitting

Update book and docs.

---------

Co-authored-by: Douglas <32344964+NotTheDr01ds@users.noreply.github.com>
2024-11-24 10:59:54 -08:00

451 lines
8.9 KiB
Rust

use nu_test_support::{nu, playground::Playground};
use regex::Regex;
#[test]
fn record_with_redefined_key() {
let actual = nu!("{x: 1, x: 2}");
assert!(actual.err.contains("redefined"));
}
#[test]
fn run_file_parse_error() {
let actual = nu!(
cwd: "tests/fixtures/eval",
"nu script.nu"
);
assert!(actual.err.contains("unknown type"));
}
enum ExpectedOut<'a> {
/// Equals a string exactly
Eq(&'a str),
/// Matches a regex
Matches(&'a str),
/// Produces an error (match regex)
Error(&'a str),
/// Drops a file that contains these contents
FileEq(&'a str, &'a str),
}
use self::ExpectedOut::*;
fn test_eval(source: &str, expected_out: ExpectedOut) {
Playground::setup("test_eval", |dirs, _playground| {
let actual = nu!(
cwd: dirs.test(),
source,
);
match expected_out {
Eq(eq) => {
assert_eq!(actual.out, eq);
assert!(actual.status.success());
}
Matches(regex) => {
let compiled_regex = Regex::new(regex).expect("regex failed to compile");
assert!(
compiled_regex.is_match(&actual.out),
"eval out does not match: {}\n{}",
regex,
actual.out,
);
assert!(actual.status.success());
}
Error(regex) => {
let compiled_regex = Regex::new(regex).expect("regex failed to compile");
assert!(
compiled_regex.is_match(&actual.err),
"eval err does not match: {}",
regex
);
assert!(!actual.status.success());
}
FileEq(path, contents) => {
let read_contents =
std::fs::read_to_string(dirs.test().join(path)).expect("failed to read file");
assert_eq!(read_contents.trim(), contents);
assert!(actual.status.success());
}
}
});
}
#[test]
fn literal_bool() {
test_eval("true", Eq("true"))
}
#[test]
fn literal_int() {
test_eval("1", Eq("1"))
}
#[test]
fn literal_float() {
test_eval("1.5", Eq("1.5"))
}
#[test]
fn literal_filesize() {
test_eval("30MiB", Eq("30.0 MiB"))
}
#[test]
fn literal_duration() {
test_eval("30ms", Eq("30ms"))
}
#[test]
fn literal_binary() {
test_eval("0x[1f 2f f0]", Matches("Length.*1f.*2f.*f0"))
}
#[test]
fn literal_closure() {
test_eval("{||}", Matches("<Closure"))
}
#[test]
fn literal_range() {
test_eval("0..2..10", Matches("10"))
}
#[test]
fn literal_list() {
test_eval("[foo bar baz]", Matches("foo.*bar.*baz"))
}
#[test]
fn literal_record() {
test_eval("{foo: bar, baz: quux}", Matches("foo.*bar.*baz.*quux"))
}
#[test]
fn literal_table() {
test_eval("[[a b]; [1 2] [3 4]]", Matches("a.*b.*1.*2.*3.*4"))
}
#[test]
fn literal_string() {
test_eval(r#""foobar""#, Eq("foobar"))
}
#[test]
fn literal_raw_string() {
test_eval(r#"r#'bazquux'#"#, Eq("bazquux"))
}
#[test]
fn literal_date() {
test_eval("2020-01-01T00:00:00Z", Matches("2020"))
}
#[test]
fn literal_nothing() {
test_eval("null", Eq(""))
}
#[test]
fn list_spread() {
test_eval("[foo bar ...[baz quux]] | length", Eq("4"))
}
#[test]
fn record_spread() {
test_eval("{foo: bar ...{baz: quux}} | columns | length", Eq("2"))
}
#[test]
fn binary_op_example() {
test_eval(
"(([1 2] ++ [3 4]) == [1 2 3 4]) and (([1] ++ [2 3 4]) == [1 2 3 4])",
Eq("true"),
)
}
#[test]
fn range_from_expressions() {
test_eval("(1 + 1)..(2 + 2)", Matches("2.*3.*4"))
}
#[test]
fn list_from_expressions() {
test_eval(
"[('foo' | str upcase) ('BAR' | str downcase)]",
Matches("FOO.*bar"),
)
}
#[test]
fn record_from_expressions() {
test_eval("{('foo' | str upcase): 42}", Matches("FOO.*42"))
}
#[test]
fn call_spread() {
test_eval(
"echo foo bar ...[baz quux nushell]",
Matches("foo.*bar.*baz.*quux.*nushell"),
)
}
#[test]
fn call_flag() {
test_eval("print -e message", Eq("")) // should not be visible on stdout
}
#[test]
fn call_named() {
test_eval("10.123 | into string --decimals 1", Eq("10.1"))
}
#[test]
fn external_call() {
test_eval("nu --testbin cococo foo=bar baz", Eq("foo=bar baz"))
}
#[test]
fn external_call_redirect_pipe() {
test_eval(
"nu --testbin cococo foo=bar baz | str upcase",
Eq("FOO=BAR BAZ"),
)
}
#[test]
fn external_call_redirect_capture() {
test_eval(
"echo (nu --testbin cococo foo=bar baz) | str upcase",
Eq("FOO=BAR BAZ"),
)
}
#[test]
fn external_call_redirect_file() {
test_eval(
"nu --testbin cococo hello out> hello.txt",
FileEq("hello.txt", "hello"),
)
}
#[test]
fn let_variable() {
test_eval("let foo = 'test'; print $foo", Eq("test"))
}
#[test]
fn let_variable_mutate_error() {
test_eval(
"let foo = 'test'; $foo = 'bar'; print $foo",
Error("immutable"),
)
}
#[test]
fn constant() {
test_eval("const foo = 1 + 2; print $foo", Eq("3"))
}
#[test]
fn constant_assign_error() {
test_eval(
"const foo = 1 + 2; $foo = 4; print $foo",
Error("immutable"),
)
}
#[test]
fn mut_variable() {
test_eval("mut foo = 'test'; $foo = 'bar'; print $foo", Eq("bar"))
}
#[test]
fn mut_variable_append_assign() {
test_eval(
"mut foo = 'test'; $foo ++= 'bar'; print $foo",
Eq("testbar"),
)
}
#[test]
fn bind_in_variable_to_input() {
test_eval("3 | (4 + $in)", Eq("7"))
}
#[test]
fn if_true() {
test_eval("if true { 'foo' }", Eq("foo"))
}
#[test]
fn if_false() {
test_eval("if false { 'foo' } | describe", Eq("nothing"))
}
#[test]
fn if_else_true() {
test_eval("if 5 > 3 { 'foo' } else { 'bar' }", Eq("foo"))
}
#[test]
fn if_else_false() {
test_eval("if 5 < 3 { 'foo' } else { 'bar' }", Eq("bar"))
}
#[test]
fn match_empty_fallthrough() {
test_eval("match 42 { }; 'pass'", Eq("pass"))
}
#[test]
fn match_value() {
test_eval("match 1 { 1 => 'pass', 2 => 'fail' }", Eq("pass"))
}
#[test]
fn match_value_default() {
test_eval(
"match 3 { 1 => 'fail1', 2 => 'fail2', _ => 'pass' }",
Eq("pass"),
)
}
#[test]
fn match_value_fallthrough() {
test_eval("match 3 { 1 => 'fail1', 2 => 'fail2' }", Eq(""))
}
#[test]
fn match_variable() {
test_eval(
"match 'pass' { $s => { print $s }, _ => { print 'fail' } }",
Eq("pass"),
)
}
#[test]
fn match_variable_in_list() {
test_eval("match [fail pass] { [$f, $p] => { print $p } }", Eq("pass"))
}
#[test]
fn match_passthrough_input() {
test_eval(
"'yes' | match [pass fail] { [$p, ..] => (collect { |y| $y ++ $p }) }",
Eq("yespass"),
)
}
#[test]
fn while_mutate_var() {
test_eval("mut x = 2; while $x > 0 { print $x; $x -= 1 }", Eq("21"))
}
#[test]
fn for_list() {
test_eval("for v in [1 2 3] { print ($v * 2) }", Eq(r"246"))
}
#[test]
fn for_seq() {
test_eval("for v in (seq 1 4) { print ($v * 2) }", Eq("2468"))
}
#[test]
fn early_return() {
test_eval("do { return 'foo'; 'bar' }", Eq("foo"))
}
#[test]
fn early_return_from_if() {
test_eval("do { if true { return 'pass' }; 'fail' }", Eq("pass"))
}
#[test]
fn early_return_from_loop() {
test_eval("do { loop { return 'pass' } }", Eq("pass"))
}
#[test]
fn early_return_from_while() {
test_eval(
"do { let x = true; while $x { return 'pass' } }",
Eq("pass"),
)
}
#[test]
fn early_return_from_for() {
test_eval("do { for x in [pass fail] { return $x } }", Eq("pass"))
}
#[test]
fn try_no_catch() {
test_eval("try { error make { msg: foo } }; 'pass'", Eq("pass"))
}
#[test]
fn try_catch_no_var() {
test_eval(
"try { error make { msg: foo } } catch { 'pass' }",
Eq("pass"),
)
}
#[test]
fn try_catch_var() {
test_eval(
"try { error make { msg: foo } } catch { |err| $err.msg }",
Eq("foo"),
)
}
#[test]
fn try_catch_with_non_literal_closure_no_var() {
test_eval(
r#"
let error_handler = { || "pass" }
try { error make { msg: foobar } } catch $error_handler
"#,
Eq("pass"),
)
}
#[test]
fn try_catch_with_non_literal_closure() {
test_eval(
r#"
let error_handler = { |err| $err.msg }
try { error make { msg: foobar } } catch $error_handler
"#,
Eq("foobar"),
)
}
#[test]
fn try_catch_external() {
test_eval(
r#"try { nu -c 'exit 1' } catch { $env.LAST_EXIT_CODE }"#,
Eq("1"),
)
}
#[test]
fn row_condition() {
test_eval(
"[[a b]; [1 2] [3 4]] | where a < 3 | to nuon",
Eq("[[a, b]; [1, 2]]"),
)
}
#[test]
fn custom_command() {
test_eval(
r#"
def cmd [a: int, b: string = 'fail', ...c: string, --x: int] { $"($a)($b)($c)($x)" }
cmd 42 pass foo --x 30
"#,
Eq("42pass[foo]30"),
)
}