mirror of
https://github.com/RGBCube/uutils-coreutils
synced 2025-07-27 19:17:43 +00:00
Merge pull request #4002 from sylvestre/cmd-test
test: add a bunch of operations
This commit is contained in:
commit
25db814671
3 changed files with 157 additions and 16 deletions
|
@ -55,8 +55,8 @@ impl Symbol {
|
||||||
"-eq" | "-ge" | "-gt" | "-le" | "-lt" | "-ne" => Self::Op(Operator::Int(s)),
|
"-eq" | "-ge" | "-gt" | "-le" | "-lt" | "-ne" => Self::Op(Operator::Int(s)),
|
||||||
"-ef" | "-nt" | "-ot" => Self::Op(Operator::File(s)),
|
"-ef" | "-nt" | "-ot" => Self::Op(Operator::File(s)),
|
||||||
"-n" | "-z" => Self::UnaryOp(UnaryOperator::StrlenOp(s)),
|
"-n" | "-z" => Self::UnaryOp(UnaryOperator::StrlenOp(s)),
|
||||||
"-b" | "-c" | "-d" | "-e" | "-f" | "-g" | "-G" | "-h" | "-k" | "-L" | "-O"
|
"-b" | "-c" | "-d" | "-e" | "-f" | "-g" | "-G" | "-h" | "-k" | "-L" | "-N"
|
||||||
| "-p" | "-r" | "-s" | "-S" | "-t" | "-u" | "-w" | "-x" => {
|
| "-O" | "-p" | "-r" | "-s" | "-S" | "-t" | "-u" | "-w" | "-x" => {
|
||||||
Self::UnaryOp(UnaryOperator::FiletestOp(s))
|
Self::UnaryOp(UnaryOperator::FiletestOp(s))
|
||||||
}
|
}
|
||||||
_ => Self::Literal(s),
|
_ => Self::Literal(s),
|
||||||
|
@ -108,7 +108,7 @@ impl Symbol {
|
||||||
/// INTOP → -eq | -ge | -gt | -le | -lt | -ne
|
/// INTOP → -eq | -ge | -gt | -le | -lt | -ne
|
||||||
/// FILEOP → -ef | -nt | -ot
|
/// FILEOP → -ef | -nt | -ot
|
||||||
/// STRLEN → -n | -z
|
/// STRLEN → -n | -z
|
||||||
/// FILETEST → -b | -c | -d | -e | -f | -g | -G | -h | -k | -L | -O | -p |
|
/// FILETEST → -b | -c | -d | -e | -f | -g | -G | -h | -k | -L | -N | -O | -p |
|
||||||
/// -r | -s | -S | -t | -u | -w | -x
|
/// -r | -s | -S | -t | -u | -w | -x
|
||||||
/// BOOLOP → -a | -o
|
/// BOOLOP → -a | -o
|
||||||
///
|
///
|
||||||
|
|
|
@ -13,6 +13,9 @@ mod parser;
|
||||||
use clap::{crate_version, Command};
|
use clap::{crate_version, Command};
|
||||||
use parser::{parse, Operator, Symbol, UnaryOperator};
|
use parser::{parse, Operator, Symbol, UnaryOperator};
|
||||||
use std::ffi::{OsStr, OsString};
|
use std::ffi::{OsStr, OsString};
|
||||||
|
use std::fs;
|
||||||
|
#[cfg(unix)]
|
||||||
|
use std::os::unix::fs::MetadataExt;
|
||||||
use uucore::display::Quotable;
|
use uucore::display::Quotable;
|
||||||
use uucore::error::{UResult, USimpleError};
|
use uucore::error::{UResult, USimpleError};
|
||||||
use uucore::format_usage;
|
use uucore::format_usage;
|
||||||
|
@ -170,7 +173,11 @@ fn eval(stack: &mut Vec<Symbol>) -> Result<bool, String> {
|
||||||
|
|
||||||
Ok(integers(&a, &b, &op)?)
|
Ok(integers(&a, &b, &op)?)
|
||||||
}
|
}
|
||||||
Some(Symbol::Op(Operator::File(_op))) => unimplemented!(),
|
Some(Symbol::Op(Operator::File(op))) => {
|
||||||
|
let b = pop_literal!();
|
||||||
|
let a = pop_literal!();
|
||||||
|
Ok(files(&a, &b, &op)?)
|
||||||
|
}
|
||||||
Some(Symbol::UnaryOp(UnaryOperator::StrlenOp(op))) => {
|
Some(Symbol::UnaryOp(UnaryOperator::StrlenOp(op))) => {
|
||||||
let s = match stack.pop() {
|
let s = match stack.pop() {
|
||||||
Some(Symbol::Literal(s)) => s,
|
Some(Symbol::Literal(s)) => s,
|
||||||
|
@ -205,6 +212,7 @@ fn eval(stack: &mut Vec<Symbol>) -> Result<bool, String> {
|
||||||
"-h" => path(&f, &PathCondition::SymLink),
|
"-h" => path(&f, &PathCondition::SymLink),
|
||||||
"-k" => path(&f, &PathCondition::Sticky),
|
"-k" => path(&f, &PathCondition::Sticky),
|
||||||
"-L" => path(&f, &PathCondition::SymLink),
|
"-L" => path(&f, &PathCondition::SymLink),
|
||||||
|
"-N" => path(&f, &PathCondition::ExistsModifiedLastRead),
|
||||||
"-O" => path(&f, &PathCondition::UserOwns),
|
"-O" => path(&f, &PathCondition::UserOwns),
|
||||||
"-p" => path(&f, &PathCondition::Fifo),
|
"-p" => path(&f, &PathCondition::Fifo),
|
||||||
"-r" => path(&f, &PathCondition::Readable),
|
"-r" => path(&f, &PathCondition::Readable),
|
||||||
|
@ -229,19 +237,25 @@ fn eval(stack: &mut Vec<Symbol>) -> Result<bool, String> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Operations to compare integers
|
||||||
|
/// `a` is the left hand side
|
||||||
|
/// `b` is the left hand side
|
||||||
|
/// `op` the operation (ex: -eq, -lt, etc)
|
||||||
fn integers(a: &OsStr, b: &OsStr, op: &OsStr) -> Result<bool, String> {
|
fn integers(a: &OsStr, b: &OsStr, op: &OsStr) -> Result<bool, String> {
|
||||||
let format_err = |value: &OsStr| format!("invalid integer {}", value.quote());
|
let format_err = |value: &OsStr| format!("invalid integer {}", value.quote());
|
||||||
|
|
||||||
let a: i64 = a
|
// Parse the two inputs
|
||||||
|
let a: i128 = a
|
||||||
.to_str()
|
.to_str()
|
||||||
.and_then(|s| s.parse().ok())
|
.and_then(|s| s.parse().ok())
|
||||||
.ok_or_else(|| format_err(a))?;
|
.ok_or_else(|| format_err(a))?;
|
||||||
|
|
||||||
let b: i64 = b
|
let b: i128 = b
|
||||||
.to_str()
|
.to_str()
|
||||||
.and_then(|s| s.parse().ok())
|
.and_then(|s| s.parse().ok())
|
||||||
.ok_or_else(|| format_err(b))?;
|
.ok_or_else(|| format_err(b))?;
|
||||||
|
|
||||||
|
// Do the maths
|
||||||
Ok(match op.to_str() {
|
Ok(match op.to_str() {
|
||||||
Some("-eq") => a == b,
|
Some("-eq") => a == b,
|
||||||
Some("-ne") => a != b,
|
Some("-ne") => a != b,
|
||||||
|
@ -253,6 +267,33 @@ fn integers(a: &OsStr, b: &OsStr, op: &OsStr) -> Result<bool, String> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Operations to compare files metadata
|
||||||
|
/// `a` is the left hand side
|
||||||
|
/// `b` is the left hand side
|
||||||
|
/// `op` the operation (ex: -ef, -nt, etc)
|
||||||
|
fn files(a: &OsStr, b: &OsStr, op: &OsStr) -> Result<bool, String> {
|
||||||
|
// Don't manage the error. GNU doesn't show error when doing
|
||||||
|
// test foo -nt bar
|
||||||
|
let f_a = match fs::metadata(a) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(_) => return Ok(false),
|
||||||
|
};
|
||||||
|
let f_b = match fs::metadata(b) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(_) => return Ok(false),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(match op.to_str() {
|
||||||
|
#[cfg(unix)]
|
||||||
|
Some("-ef") => f_a.ino() == f_b.ino() && f_a.dev() == f_b.dev(),
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
Some("-ef") => unimplemented!(),
|
||||||
|
Some("-nt") => f_a.modified().unwrap() > f_b.modified().unwrap(),
|
||||||
|
Some("-ot") => f_a.created().unwrap() > f_b.created().unwrap(),
|
||||||
|
_ => return Err(format!("unknown operator {}", op.quote())),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn isatty(fd: &OsStr) -> Result<bool, String> {
|
fn isatty(fd: &OsStr) -> Result<bool, String> {
|
||||||
fd.to_str()
|
fd.to_str()
|
||||||
.and_then(|s| s.parse().ok())
|
.and_then(|s| s.parse().ok())
|
||||||
|
@ -273,6 +314,7 @@ enum PathCondition {
|
||||||
CharacterSpecial,
|
CharacterSpecial,
|
||||||
Directory,
|
Directory,
|
||||||
Exists,
|
Exists,
|
||||||
|
ExistsModifiedLastRead,
|
||||||
Regular,
|
Regular,
|
||||||
GroupIdFlag,
|
GroupIdFlag,
|
||||||
GroupOwns,
|
GroupOwns,
|
||||||
|
@ -290,8 +332,8 @@ enum PathCondition {
|
||||||
|
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
fn path(path: &OsStr, condition: &PathCondition) -> bool {
|
fn path(path: &OsStr, condition: &PathCondition) -> bool {
|
||||||
use std::fs::{self, Metadata};
|
use std::fs::Metadata;
|
||||||
use std::os::unix::fs::{FileTypeExt, MetadataExt};
|
use std::os::unix::fs::FileTypeExt;
|
||||||
|
|
||||||
const S_ISUID: u32 = 0o4000;
|
const S_ISUID: u32 = 0o4000;
|
||||||
const S_ISGID: u32 = 0o2000;
|
const S_ISGID: u32 = 0o2000;
|
||||||
|
@ -351,6 +393,9 @@ fn path(path: &OsStr, condition: &PathCondition) -> bool {
|
||||||
PathCondition::CharacterSpecial => file_type.is_char_device(),
|
PathCondition::CharacterSpecial => file_type.is_char_device(),
|
||||||
PathCondition::Directory => file_type.is_dir(),
|
PathCondition::Directory => file_type.is_dir(),
|
||||||
PathCondition::Exists => true,
|
PathCondition::Exists => true,
|
||||||
|
PathCondition::ExistsModifiedLastRead => {
|
||||||
|
metadata.accessed().unwrap() < metadata.modified().unwrap()
|
||||||
|
}
|
||||||
PathCondition::Regular => file_type.is_file(),
|
PathCondition::Regular => file_type.is_file(),
|
||||||
PathCondition::GroupIdFlag => metadata.mode() & S_ISGID != 0,
|
PathCondition::GroupIdFlag => metadata.mode() & S_ISGID != 0,
|
||||||
PathCondition::GroupOwns => metadata.gid() == getegid(),
|
PathCondition::GroupOwns => metadata.gid() == getegid(),
|
||||||
|
@ -381,6 +426,7 @@ fn path(path: &OsStr, condition: &PathCondition) -> bool {
|
||||||
PathCondition::CharacterSpecial => false,
|
PathCondition::CharacterSpecial => false,
|
||||||
PathCondition::Directory => stat.is_dir(),
|
PathCondition::Directory => stat.is_dir(),
|
||||||
PathCondition::Exists => true,
|
PathCondition::Exists => true,
|
||||||
|
PathCondition::ExistsModifiedLastRead => unimplemented!(),
|
||||||
PathCondition::Regular => stat.is_file(),
|
PathCondition::Regular => stat.is_file(),
|
||||||
PathCondition::GroupIdFlag => false,
|
PathCondition::GroupIdFlag => false,
|
||||||
PathCondition::GroupOwns => unimplemented!(),
|
PathCondition::GroupOwns => unimplemented!(),
|
||||||
|
@ -396,3 +442,28 @@ fn path(path: &OsStr, condition: &PathCondition) -> bool {
|
||||||
PathCondition::Executable => false, // TODO
|
PathCondition::Executable => false, // TODO
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::integers;
|
||||||
|
use std::ffi::OsStr;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_integer_op() {
|
||||||
|
let a = OsStr::new("18446744073709551616");
|
||||||
|
let b = OsStr::new("0");
|
||||||
|
assert_eq!(integers(a, b, OsStr::new("-lt")).unwrap(), false);
|
||||||
|
let a = OsStr::new("18446744073709551616");
|
||||||
|
let b = OsStr::new("0");
|
||||||
|
assert_eq!(integers(a, b, OsStr::new("-gt")).unwrap(), true);
|
||||||
|
let a = OsStr::new("-1");
|
||||||
|
let b = OsStr::new("0");
|
||||||
|
assert_eq!(integers(a, b, OsStr::new("-lt")).unwrap(), true);
|
||||||
|
let a = OsStr::new("42");
|
||||||
|
let b = OsStr::new("42");
|
||||||
|
assert_eq!(integers(a, b, OsStr::new("-eq")).unwrap(), true);
|
||||||
|
let a = OsStr::new("42");
|
||||||
|
let b = OsStr::new("42");
|
||||||
|
assert_eq!(integers(a, b, OsStr::new("-ne")).unwrap(), false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
// spell-checker:ignore (words) egid euid pseudofloat
|
// spell-checker:ignore (words) egid euid pseudofloat
|
||||||
|
|
||||||
use crate::common::util::*;
|
use crate::common::util::*;
|
||||||
|
use std::thread::sleep;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_empty_test_equivalent_to_false() {
|
fn test_empty_test_equivalent_to_false() {
|
||||||
|
@ -332,7 +333,7 @@ fn test_invalid_utf8_integer_compare() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore = "fixme: parse/evaluation error (code 2); GNU returns 1"]
|
#[cfg(unix)]
|
||||||
fn test_file_is_itself() {
|
fn test_file_is_itself() {
|
||||||
new_ucmd!()
|
new_ucmd!()
|
||||||
.args(&["regular_file", "-ef", "regular_file"])
|
.args(&["regular_file", "-ef", "regular_file"])
|
||||||
|
@ -340,7 +341,7 @@ fn test_file_is_itself() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore = "fixme: parse/evaluation error (code 2); GNU returns 1"]
|
#[cfg(not(target_env = "musl"))]
|
||||||
fn test_file_is_newer_than_and_older_than_itself() {
|
fn test_file_is_newer_than_and_older_than_itself() {
|
||||||
// odd but matches GNU
|
// odd but matches GNU
|
||||||
new_ucmd!()
|
new_ucmd!()
|
||||||
|
@ -354,15 +355,47 @@ fn test_file_is_newer_than_and_older_than_itself() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore = "todo: implement these"]
|
fn test_non_existing_files() {
|
||||||
|
let scenario = TestScenario::new(util_name!());
|
||||||
|
|
||||||
|
let result = scenario
|
||||||
|
.ucmd()
|
||||||
|
.args(&["newer_file", "-nt", "regular_file"])
|
||||||
|
.fails();
|
||||||
|
assert!(result.stderr().is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn test_same_device_inode() {
|
||||||
|
let scenario = TestScenario::new(util_name!());
|
||||||
|
let at = &scenario.fixtures;
|
||||||
|
|
||||||
|
scenario.cmd("touch").arg("regular_file").succeeds();
|
||||||
|
scenario.cmd("touch").arg("regular_file_second").succeeds();
|
||||||
|
|
||||||
|
at.symlink_file("regular_file", "symlink");
|
||||||
|
|
||||||
|
scenario
|
||||||
|
.ucmd()
|
||||||
|
.args(&["regular_file", "-ef", "regular_file_second"])
|
||||||
|
.fails();
|
||||||
|
|
||||||
|
scenario
|
||||||
|
.ucmd()
|
||||||
|
.args(&["regular_file", "-ef", "symlink"])
|
||||||
|
.succeeds();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(not(target_env = "musl"))]
|
||||||
|
// musl: creation time is not available on this platform currently
|
||||||
fn test_newer_file() {
|
fn test_newer_file() {
|
||||||
let scenario = TestScenario::new(util_name!());
|
let scenario = TestScenario::new(util_name!());
|
||||||
|
|
||||||
|
scenario.cmd("touch").arg("regular_file").succeeds();
|
||||||
|
sleep(std::time::Duration::from_millis(1000));
|
||||||
scenario.cmd("touch").arg("newer_file").succeeds();
|
scenario.cmd("touch").arg("newer_file").succeeds();
|
||||||
scenario
|
|
||||||
.cmd("touch")
|
|
||||||
.args(&["-m", "-d", "last Thursday", "regular_file"])
|
|
||||||
.succeeds();
|
|
||||||
|
|
||||||
scenario
|
scenario
|
||||||
.ucmd()
|
.ucmd()
|
||||||
|
@ -370,7 +403,7 @@ fn test_newer_file() {
|
||||||
.succeeds();
|
.succeeds();
|
||||||
scenario
|
scenario
|
||||||
.ucmd()
|
.ucmd()
|
||||||
.args(&["regular_file", "-ot", "newer_file"])
|
.args(&["newer_file", "-ot", "regular_file"])
|
||||||
.succeeds();
|
.succeeds();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -882,3 +915,40 @@ fn test_bracket_syntax_version() {
|
||||||
.succeeds()
|
.succeeds()
|
||||||
.stdout_matches(&r"\[ \d+\.\d+\.\d+".parse().unwrap());
|
.stdout_matches(&r"\[ \d+\.\d+\.\d+".parse().unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn test_file_N() {
|
||||||
|
let scene = TestScenario::new(util_name!());
|
||||||
|
let at = &scene.fixtures;
|
||||||
|
scene.ucmd().args(&["-N", "regular_file"]).fails();
|
||||||
|
// The file will have different create/modified data
|
||||||
|
// so, test -N will return 0
|
||||||
|
sleep(std::time::Duration::from_millis(1000));
|
||||||
|
at.touch("regular_file");
|
||||||
|
scene.ucmd().args(&["-N", "regular_file"]).succeeds();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_long_integer() {
|
||||||
|
let scene = TestScenario::new(util_name!());
|
||||||
|
scene
|
||||||
|
.ucmd()
|
||||||
|
.args(&["18446744073709551616", "-eq", "0"])
|
||||||
|
.fails();
|
||||||
|
scene
|
||||||
|
.ucmd()
|
||||||
|
.args(&["-9223372036854775809", "-ge", "18446744073709551616"])
|
||||||
|
.fails();
|
||||||
|
scene
|
||||||
|
.ucmd()
|
||||||
|
.args(&[
|
||||||
|
"'('",
|
||||||
|
"-9223372036854775809",
|
||||||
|
"-ge",
|
||||||
|
"18446744073709551616",
|
||||||
|
"')'",
|
||||||
|
])
|
||||||
|
.fails();
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue