coreutils/tests/by-util/test_test.rs
Daniel Rocco d8d71cc477 test: handle additional parentheses edge cases
Handle additional edge cases arising from test(1)’s lack of reserved words.
For example, left parenthesis followed by an operator could indicate
either

* string comparison of a literal left parenthesis, e.g. `( = foo`
* parenthesized expression using an operator as a literal, e.g. `( = != foo )`
2021-07-24 23:02:02 -04:00

862 lines
20 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// This file is part of the uutils coreutils package.
//
// (c) mahkoh (ju.orth [at] gmail [dot] com)
// (c) Daniel Rocco <drocco@gmail.com>
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
//
// spell-checker:ignore (words) egid euid pseudofloat
use crate::common::util::*;
#[test]
fn test_empty_test_equivalent_to_false() {
new_ucmd!().run().status_code(1);
}
#[test]
fn test_empty_string_is_false() {
new_ucmd!().arg("").run().status_code(1);
}
#[test]
fn test_solo_not() {
new_ucmd!().arg("!").succeeds();
}
#[test]
fn test_solo_and_or_or_is_a_literal() {
// /bin/test '' -a '' => 1; so test(1) must interpret `-a` by itself as
// a literal string
new_ucmd!().arg("-a").succeeds();
new_ucmd!().arg("-o").succeeds();
}
#[test]
fn test_some_literals() {
let scenario = TestScenario::new(util_name!());
let tests = [
"a string",
"(",
")",
"-",
"--",
"-0",
"-f",
"--help",
"--version",
"-eq",
"-lt",
"-ef",
"[",
];
for test in &tests {
scenario.ucmd().arg(test).succeeds();
}
// run the inverse of all these tests
for test in &tests {
scenario.ucmd().arg("!").arg(test).run().status_code(1);
}
}
#[test]
fn test_double_not_is_false() {
new_ucmd!().args(&["!", "!"]).run().status_code(1);
}
#[test]
fn test_and_not_is_false() {
new_ucmd!().args(&["-a", "!"]).run().status_code(1);
}
#[test]
fn test_not_and_is_false() {
// `-a` is a literal here & has nonzero length
new_ucmd!().args(&["!", "-a"]).run().status_code(1);
}
#[test]
fn test_not_and_not_succeeds() {
new_ucmd!().args(&["!", "-a", "!"]).succeeds();
}
#[test]
fn test_simple_or() {
new_ucmd!().args(&["foo", "-o", ""]).succeeds();
}
#[test]
fn test_negated_or() {
new_ucmd!()
.args(&["!", "foo", "-o", "bar"])
.run()
.status_code(1);
new_ucmd!().args(&["foo", "-o", "!", "bar"]).succeeds();
new_ucmd!()
.args(&["!", "foo", "-o", "!", "bar"])
.run()
.status_code(1);
}
#[test]
fn test_string_length_of_nothing() {
// odd but matches GNU, which must interpret -n as a literal here
new_ucmd!().arg("-n").succeeds();
}
#[test]
fn test_string_length_of_empty() {
new_ucmd!().args(&["-n", ""]).run().status_code(1);
// STRING equivalent to -n STRING
new_ucmd!().arg("").run().status_code(1);
}
#[test]
fn test_nothing_is_empty() {
// -z is a literal here and has nonzero length
new_ucmd!().arg("-z").succeeds();
}
#[test]
fn test_zero_len_of_empty() {
new_ucmd!().args(&["-z", ""]).succeeds();
}
#[test]
fn test_zero_len_equals_zero_len() {
new_ucmd!().args(&["", "=", ""]).succeeds();
}
#[test]
fn test_zero_len_not_equals_zero_len_is_false() {
new_ucmd!().args(&["", "!=", ""]).run().status_code(1);
}
#[test]
fn test_double_equal_is_string_comparison_op() {
// undocumented but part of the GNU test suite
new_ucmd!().args(&["t", "==", "t"]).succeeds();
new_ucmd!().args(&["t", "==", "f"]).run().status_code(1);
}
#[test]
fn test_string_comparison() {
let scenario = TestScenario::new(util_name!());
let tests = [
["foo", "!=", "bar"],
["contained\nnewline", "=", "contained\nnewline"],
["(", "=", "("],
["(", "!=", ")"],
["(", "!=", "="],
["!", "=", "!"],
["=", "=", "="],
];
for test in &tests {
scenario.ucmd().args(&test[..]).succeeds();
}
// run the inverse of all these tests
for test in &tests {
scenario
.ucmd()
.arg("!")
.args(&test[..])
.run()
.status_code(1);
}
}
#[test]
#[ignore = "fixme: error reporting"]
fn test_dangling_string_comparison_is_error() {
new_ucmd!()
.args(&["missing_something", "="])
.run()
.status_code(2)
.stderr_is("test: missing argument after '='");
}
#[test]
fn test_string_operator_is_literal_after_bang() {
let scenario = TestScenario::new(util_name!());
let tests = [
["!", "="],
["!", "!="],
["!", "-eq"],
["!", "-ne"],
["!", "-lt"],
["!", "-le"],
["!", "-gt"],
["!", "-ge"],
["!", "-ef"],
["!", "-nt"],
["!", "-ot"],
];
for test in &tests {
scenario.ucmd().args(&test[..]).run().status_code(1);
}
}
#[test]
fn test_a_bunch_of_not() {
new_ucmd!()
.args(&["!", "", "!=", "", "-a", "!", "", "!=", ""])
.succeeds();
}
#[test]
fn test_pseudofloat_equal() {
// string comparison; test(1) doesn't support comparison of actual floats
new_ucmd!().args(&["123.45", "=", "123.45"]).succeeds();
}
#[test]
fn test_pseudofloat_not_equal() {
// string comparison; test(1) doesn't support comparison of actual floats
new_ucmd!().args(&["123.45", "!=", "123.450"]).succeeds();
}
#[test]
fn test_negative_arg_is_a_string() {
new_ucmd!().arg("-12345").succeeds();
new_ucmd!().arg("--qwert").succeeds(); // spell-checker:disable-line
}
#[test]
fn test_some_int_compares() {
let scenario = TestScenario::new(util_name!());
let tests = [
["0", "-eq", "0"],
["0", "-ne", "1"],
["421", "-lt", "3720"],
["0", "-le", "0"],
["11", "-gt", "10"],
["1024", "-ge", "512"],
["9223372036854775806", "-le", "9223372036854775807"],
];
for test in &tests {
scenario.ucmd().args(&test[..]).succeeds();
}
// run the inverse of all these tests
for test in &tests {
scenario
.ucmd()
.arg("!")
.args(&test[..])
.run()
.status_code(1);
}
}
#[test]
#[ignore = "fixme: evaluation error (code 1); GNU returns 0"]
fn test_values_greater_than_i64_allowed() {
new_ucmd!()
.args(&["9223372036854775808", "-gt", "0"])
.succeeds();
}
#[test]
fn test_negative_int_compare() {
let scenario = TestScenario::new(util_name!());
let tests = [
["-1", "-eq", "-1"],
["-1", "-ne", "-2"],
["-3720", "-lt", "-421"],
["-10", "-le", "-10"],
["-21", "-gt", "-22"],
["-128", "-ge", "-256"],
["-9223372036854775808", "-le", "-9223372036854775807"],
];
for test in &tests {
scenario.ucmd().args(&test[..]).succeeds();
}
// run the inverse of all these tests
for test in &tests {
scenario
.ucmd()
.arg("!")
.args(&test[..])
.run()
.status_code(1);
}
}
#[test]
fn test_float_inequality_is_error() {
new_ucmd!()
.args(&["123.45", "-ge", "6"])
.run()
.status_code(2)
.stderr_is("test: invalid integer '123.45'");
}
#[test]
#[cfg(not(windows))]
fn test_invalid_utf8_integer_compare() {
use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt;
let source = [0x66, 0x6f, 0x80, 0x6f];
let arg = OsStr::from_bytes(&source[..]);
let mut cmd = new_ucmd!();
cmd.arg("123").arg("-ne");
cmd.raw.arg(arg);
cmd.run()
.status_code(2)
.stderr_is("test: invalid integer 'fo<66>o'");
let mut cmd = new_ucmd!();
cmd.raw.arg(arg);
cmd.arg("-eq").arg("456");
cmd.run()
.status_code(2)
.stderr_is("test: invalid integer 'fo<66>o'");
}
#[test]
#[ignore = "fixme: parse/evaluation error (code 2); GNU returns 1"]
fn test_file_is_itself() {
new_ucmd!()
.args(&["regular_file", "-ef", "regular_file"])
.succeeds();
}
#[test]
#[ignore = "fixme: parse/evaluation error (code 2); GNU returns 1"]
fn test_file_is_newer_than_and_older_than_itself() {
// odd but matches GNU
new_ucmd!()
.args(&["regular_file", "-nt", "regular_file"])
.run()
.status_code(1);
new_ucmd!()
.args(&["regular_file", "-ot", "regular_file"])
.run()
.status_code(1);
}
#[test]
#[ignore = "todo: implement these"]
fn test_newer_file() {
let scenario = TestScenario::new(util_name!());
scenario.cmd("touch").arg("newer_file").succeeds();
scenario
.cmd("touch")
.args(&["-m", "-d", "last Thursday", "regular_file"])
.succeeds();
scenario
.ucmd()
.args(&["newer_file", "-nt", "regular_file"])
.succeeds();
scenario
.ucmd()
.args(&["regular_file", "-ot", "newer_file"])
.succeeds();
}
#[test]
fn test_file_exists() {
new_ucmd!().args(&["-e", "regular_file"]).succeeds();
}
#[test]
fn test_nonexistent_file_does_not_exist() {
new_ucmd!()
.args(&["-e", "nonexistent_file"])
.run()
.status_code(1);
}
#[test]
fn test_nonexistent_file_is_not_regular() {
new_ucmd!()
.args(&["-f", "nonexistent_file"])
.run()
.status_code(1);
}
#[test]
fn test_file_exists_and_is_regular() {
new_ucmd!().args(&["-f", "regular_file"]).succeeds();
}
#[test]
#[cfg(not(windows))] // FIXME: implement on Windows
fn test_file_is_readable() {
new_ucmd!().args(&["-r", "regular_file"]).succeeds();
}
#[test]
#[cfg(not(windows))] // FIXME: implement on Windows
fn test_file_is_not_readable() {
let scenario = TestScenario::new(util_name!());
let mut ucmd = scenario.ucmd();
let mut chmod = scenario.cmd("chmod");
scenario.fixtures.touch("crypto_file");
chmod.args(&["u-r", "crypto_file"]).succeeds();
ucmd.args(&["!", "-r", "crypto_file"]).succeeds();
}
#[test]
#[cfg(not(windows))] // FIXME: implement on Windows
fn test_file_is_writable() {
new_ucmd!().args(&["-w", "regular_file"]).succeeds();
}
#[test]
#[cfg(not(windows))] // FIXME: implement on Windows
fn test_file_is_not_writable() {
let scenario = TestScenario::new(util_name!());
let mut ucmd = scenario.ucmd();
let mut chmod = scenario.cmd("chmod");
scenario.fixtures.touch("immutable_file");
chmod.args(&["u-w", "immutable_file"]).succeeds();
ucmd.args(&["!", "-w", "immutable_file"]).succeeds();
}
#[test]
fn test_file_is_not_executable() {
new_ucmd!().args(&["!", "-x", "regular_file"]).succeeds();
}
#[test]
#[cfg(not(windows))] // FIXME: implement on Windows
fn test_file_is_executable() {
let scenario = TestScenario::new(util_name!());
let mut chmod = scenario.cmd("chmod");
chmod.args(&["u+x", "regular_file"]).succeeds();
scenario.ucmd().args(&["-x", "regular_file"]).succeeds();
}
#[test]
fn test_is_not_empty() {
new_ucmd!().args(&["-s", "non_empty_file"]).succeeds();
}
#[test]
fn test_nonexistent_file_size_test_is_false() {
new_ucmd!()
.args(&["-s", "nonexistent_file"])
.run()
.status_code(1);
}
#[test]
fn test_not_is_not_empty() {
new_ucmd!().args(&["!", "-s", "regular_file"]).succeeds();
}
#[test]
#[cfg(not(windows))]
fn test_symlink_is_symlink() {
let scenario = TestScenario::new(util_name!());
let at = &scenario.fixtures;
at.symlink_file("regular_file", "symlink");
// FIXME: implement on Windows
scenario.ucmd().args(&["-h", "symlink"]).succeeds();
scenario.ucmd().args(&["-L", "symlink"]).succeeds();
}
#[test]
fn test_file_is_not_symlink() {
let scenario = TestScenario::new(util_name!());
scenario
.ucmd()
.args(&["!", "-h", "regular_file"])
.succeeds();
scenario
.ucmd()
.args(&["!", "-L", "regular_file"])
.succeeds();
}
#[test]
fn test_nonexistent_file_is_not_symlink() {
let scenario = TestScenario::new(util_name!());
scenario
.ucmd()
.args(&["!", "-h", "nonexistent_file"])
.succeeds();
scenario
.ucmd()
.args(&["!", "-L", "nonexistent_file"])
.succeeds();
}
#[test]
#[cfg(not(windows))] // Windows has no concept of sticky bit
fn test_file_is_sticky() {
let scenario = TestScenario::new(util_name!());
let mut ucmd = scenario.ucmd();
let mut chmod = scenario.cmd("chmod");
scenario.fixtures.touch("sticky_file");
chmod.args(&["+t", "sticky_file"]).succeeds();
ucmd.args(&["-k", "sticky_file"]).succeeds();
}
#[test]
fn test_file_is_not_sticky() {
new_ucmd!()
.args(&["-k", "regular_file"])
.run()
.status_code(1);
}
#[test]
fn test_solo_empty_parenthetical_is_error() {
new_ucmd!().args(&["(", ")"]).run().status_code(2);
}
#[test]
fn test_parenthesized_literal() {
let scenario = TestScenario::new(util_name!());
let tests = [
"a string",
"(",
")",
"-",
"--",
"-0",
"-f",
"--help",
"--version",
"-e",
"-t",
"!",
"-n",
"-z",
"[",
"-a",
"-o",
];
for test in &tests {
scenario.ucmd().arg("(").arg(test).arg(")").succeeds();
}
// run the inverse of all these tests
for test in &tests {
scenario
.ucmd()
.arg("!")
.arg("(")
.arg(test)
.arg(")")
.run()
.status_code(1);
}
}
#[test]
fn test_parenthesized_op_compares_literal_parenthesis() {
// ensure we arent treating this case as “string length of literal equal
// sign”
new_ucmd!().args(&["(", "=", ")"]).run().status_code(1);
}
#[test]
fn test_parenthesized_string_comparison() {
let scenario = TestScenario::new(util_name!());
let tests = [
["(", "foo", "!=", "bar", ")"],
["(", "contained\nnewline", "=", "contained\nnewline", ")"],
["(", "(", "=", "(", ")"],
["(", "(", "!=", ")", ")"],
["(", "!", "=", "!", ")"],
["(", "=", "=", "=", ")"],
];
for test in &tests {
scenario.ucmd().args(&test[..]).succeeds();
}
// run the inverse of all these tests
for test in &tests {
scenario
.ucmd()
.arg("!")
.args(&test[..])
.run()
.status_code(1);
}
}
#[test]
fn test_parenthesized_right_parenthesis_as_literal() {
new_ucmd!()
.args(&["(", "-f", ")", ")"])
.run()
.status_code(1);
}
#[test]
#[cfg(not(windows))]
fn test_file_owned_by_euid() {
new_ucmd!().args(&["-O", "regular_file"]).succeeds();
}
#[test]
#[cfg(not(windows))]
fn test_nonexistent_file_not_owned_by_euid() {
new_ucmd!()
.args(&["-O", "nonexistent_file"])
.run()
.status_code(1);
}
#[test]
#[cfg(all(not(windows), not(target_os = "freebsd")))]
fn test_file_not_owned_by_euid() {
new_ucmd!()
.args(&["-f", "/bin/sh", "-a", "!", "-O", "/bin/sh"])
.succeeds();
}
#[test]
#[cfg(not(windows))]
fn test_file_owned_by_egid() {
new_ucmd!().args(&["-G", "regular_file"]).succeeds();
}
#[test]
#[cfg(not(windows))]
fn test_nonexistent_file_not_owned_by_egid() {
new_ucmd!()
.args(&["-G", "nonexistent_file"])
.run()
.status_code(1);
}
#[test]
#[cfg(all(not(windows), not(target_os = "freebsd")))]
fn test_file_not_owned_by_egid() {
new_ucmd!()
.args(&["-f", "/bin/sh", "-a", "!", "-G", "/bin/sh"])
.succeeds();
}
#[test]
fn test_op_precedence_and_or_1() {
new_ucmd!().args(&[" ", "-o", "", "-a", ""]).succeeds();
}
#[test]
fn test_op_precedence_and_or_1_overridden_by_parentheses() {
new_ucmd!()
.args(&["(", " ", "-o", "", ")", "-a", ""])
.run()
.status_code(1);
}
#[test]
fn test_op_precedence_and_or_2() {
new_ucmd!()
.args(&["", "-a", "", "-o", " ", "-a", " "])
.succeeds();
}
#[test]
fn test_op_precedence_and_or_2_overridden_by_parentheses() {
new_ucmd!()
.args(&["", "-a", "(", "", "-o", " ", ")", "-a", " "])
.run()
.status_code(1);
}
#[test]
fn test_negated_boolean_precedence() {
let scenario = TestScenario::new(util_name!());
let tests = [
vec!["!", "(", "foo", ")", "-o", "bar"],
vec!["!", "", "-o", "", "-a", ""],
vec!["!", "(", "", "-a", "", ")", "-o", ""],
];
for test in &tests {
scenario.ucmd().args(&test[..]).succeeds();
}
let negative_tests = [
vec!["!", "-n", "", "-a", ""],
vec!["", "-a", "", "-o", ""],
vec!["!", "", "-a", "", "-o", ""],
vec!["!", "(", "", "-a", "", ")", "-a", ""],
];
for test in &negative_tests {
scenario.ucmd().args(&test[..]).run().status_code(1);
}
}
#[test]
fn test_bang_bool_op_precedence() {
// For a Boolean combination of two literals, bang inverts the entire expression
new_ucmd!().args(&["!", "", "-a", ""]).succeeds();
new_ucmd!().args(&["!", "", "-o", ""]).succeeds();
new_ucmd!()
.args(&["!", "a value", "-o", "another value"])
.run()
.status_code(1);
// Introducing a UOP — even one that is equivalent to a bare string — causes
// bang to invert only the first term
new_ucmd!()
.args(&["!", "-n", "", "-a", ""])
.run()
.status_code(1);
new_ucmd!()
.args(&["!", "", "-a", "-n", ""])
.run()
.status_code(1);
// for compound Boolean expressions, bang inverts the _next_ expression
// only, not the entire compound expression
new_ucmd!()
.args(&["!", "", "-a", "", "-a", ""])
.run()
.status_code(1);
// parentheses can override this
new_ucmd!()
.args(&["!", "(", "", "-a", "", "-a", "", ")"])
.succeeds();
}
#[test]
fn test_inverted_parenthetical_bool_op_precedence() {
// For a Boolean combination of two literals, bang inverts the entire expression
new_ucmd!()
.args(&["!", "a value", "-o", "another value"])
.run()
.status_code(1);
// only the parenthetical is inverted, not the entire expression
new_ucmd!()
.args(&["!", "(", "a value", ")", "-o", "another value"])
.succeeds();
}
#[test]
#[ignore = "fixme: error reporting"]
fn test_dangling_parenthesis() {
new_ucmd!()
.args(&["(", "(", "a", "!=", "b", ")", "-o", "-n", "c"])
.run()
.status_code(2);
new_ucmd!()
.args(&["(", "(", "a", "!=", "b", ")", "-o", "-n", "c", ")"])
.succeeds();
}
#[test]
fn test_complicated_parenthesized_expression() {
new_ucmd!()
.args(&[
"(", "(", "!", "(", "a", "=", "b", ")", "-o", "c", "=", "d", ")", "-a", "(", "q", "!=",
"r", ")", ")",
])
.succeeds();
}
#[test]
fn test_erroneous_parenthesized_expression() {
new_ucmd!()
.args(&["a", "!=", "(", "b", "-a", "b", ")", "!=", "c"])
.run()
.status_code(2)
.stderr_is("test: extra argument 'b'");
}
#[test]
fn test_or_as_filename() {
new_ucmd!()
.args(&["x", "-a", "-z", "-o"])
.run()
.status_code(1);
}
#[test]
#[ignore = "GNU considers this an error"]
fn test_string_length_and_nothing() {
new_ucmd!().args(&["-n", "a", "-a"]).run().status_code(2);
}
#[test]
fn test_bracket_syntax_success() {
let scenario = TestScenario::new("[");
let mut ucmd = scenario.ucmd();
ucmd.args(&["1", "-eq", "1", "]"]).succeeds();
}
#[test]
fn test_bracket_syntax_failure() {
let scenario = TestScenario::new("[");
let mut ucmd = scenario.ucmd();
ucmd.args(&["1", "-eq", "2", "]"]).run().status_code(1);
}
#[test]
fn test_bracket_syntax_missing_right_bracket() {
let scenario = TestScenario::new("[");
let mut ucmd = scenario.ucmd();
// Missing closing bracket takes precedence over other possible errors.
ucmd.args(&["1", "-eq"])
.run()
.status_code(2)
.stderr_is("[: missing ']'");
}
#[test]
fn test_bracket_syntax_help() {
let scenario = TestScenario::new("[");
let mut ucmd = scenario.ucmd();
ucmd.arg("--help").succeeds().stdout_contains("USAGE:");
}
#[test]
fn test_bracket_syntax_version() {
let scenario = TestScenario::new("[");
let mut ucmd = scenario.ucmd();
ucmd.arg("--version")
.succeeds()
.stdout_matches(&r"\[ \d+\.\d+\.\d+".parse().unwrap());
}