1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 11:37:44 +00:00

test: add support for -ef, -nt & -ot

This commit is contained in:
Sylvestre Ledru 2022-10-02 18:11:40 -10:00
parent 63203a0a68
commit f0b8b33dc1
2 changed files with 86 additions and 11 deletions

View file

@ -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,
@ -230,9 +237,14 @@ 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());
// Parse the two inputs
let a: i64 = a let a: i64 = a
.to_str() .to_str()
.and_then(|s| s.parse().ok()) .and_then(|s| s.parse().ok())
@ -243,6 +255,7 @@ fn integers(a: &OsStr, b: &OsStr, op: &OsStr) -> Result<bool, String> {
.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,
@ -254,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())
@ -292,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;
@ -386,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!(),

View file

@ -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();
} }
@ -885,6 +918,7 @@ fn test_bracket_syntax_version() {
#[test] #[test]
#[allow(non_snake_case)] #[allow(non_snake_case)]
#[cfg(unix)]
fn test_file_N() { fn test_file_N() {
let scene = TestScenario::new(util_name!()); let scene = TestScenario::new(util_name!());
let at = &scene.fixtures; let at = &scene.fixtures;