From f0b8b33dc171a670277a033b455f962bdfef335b Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 2 Oct 2022 18:11:40 -1000 Subject: [PATCH] test: add support for -ef, -nt & -ot --- src/uu/test/src/test.rs | 47 ++++++++++++++++++++++++++++++++--- tests/by-util/test_test.rs | 50 ++++++++++++++++++++++++++++++++------ 2 files changed, 86 insertions(+), 11 deletions(-) diff --git a/src/uu/test/src/test.rs b/src/uu/test/src/test.rs index e16ac85ea..c220ef7e9 100644 --- a/src/uu/test/src/test.rs +++ b/src/uu/test/src/test.rs @@ -13,6 +13,9 @@ mod parser; use clap::{crate_version, Command}; use parser::{parse, Operator, Symbol, UnaryOperator}; use std::ffi::{OsStr, OsString}; +use std::fs; +#[cfg(unix)] +use std::os::unix::fs::MetadataExt; use uucore::display::Quotable; use uucore::error::{UResult, USimpleError}; use uucore::format_usage; @@ -170,7 +173,11 @@ fn eval(stack: &mut Vec) -> Result { 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))) => { let s = match stack.pop() { Some(Symbol::Literal(s)) => s, @@ -230,9 +237,14 @@ fn eval(stack: &mut Vec) -> Result { } } +/// 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 { let format_err = |value: &OsStr| format!("invalid integer {}", value.quote()); + // Parse the two inputs let a: i64 = a .to_str() .and_then(|s| s.parse().ok()) @@ -243,6 +255,7 @@ fn integers(a: &OsStr, b: &OsStr, op: &OsStr) -> Result { .and_then(|s| s.parse().ok()) .ok_or_else(|| format_err(b))?; + // Do the maths Ok(match op.to_str() { Some("-eq") => a == b, Some("-ne") => a != b, @@ -254,6 +267,33 @@ fn integers(a: &OsStr, b: &OsStr, op: &OsStr) -> Result { }) } +/// 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 { + // 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 { fd.to_str() .and_then(|s| s.parse().ok()) @@ -292,8 +332,8 @@ enum PathCondition { #[cfg(not(windows))] fn path(path: &OsStr, condition: &PathCondition) -> bool { - use std::fs::{self, Metadata}; - use std::os::unix::fs::{FileTypeExt, MetadataExt}; + use std::fs::Metadata; + use std::os::unix::fs::FileTypeExt; const S_ISUID: u32 = 0o4000; const S_ISGID: u32 = 0o2000; @@ -386,6 +426,7 @@ fn path(path: &OsStr, condition: &PathCondition) -> bool { PathCondition::CharacterSpecial => false, PathCondition::Directory => stat.is_dir(), PathCondition::Exists => true, + PathCondition::ExistsModifiedLastRead => unimplemented!(), PathCondition::Regular => stat.is_file(), PathCondition::GroupIdFlag => false, PathCondition::GroupOwns => unimplemented!(), diff --git a/tests/by-util/test_test.rs b/tests/by-util/test_test.rs index 8ee4f8732..028babc4b 100644 --- a/tests/by-util/test_test.rs +++ b/tests/by-util/test_test.rs @@ -11,6 +11,7 @@ // spell-checker:ignore (words) egid euid pseudofloat use crate::common::util::*; +use std::thread::sleep; #[test] fn test_empty_test_equivalent_to_false() { @@ -332,7 +333,7 @@ fn test_invalid_utf8_integer_compare() { } #[test] -#[ignore = "fixme: parse/evaluation error (code 2); GNU returns 1"] +#[cfg(unix)] fn test_file_is_itself() { new_ucmd!() .args(&["regular_file", "-ef", "regular_file"]) @@ -340,7 +341,7 @@ fn test_file_is_itself() { } #[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() { // odd but matches GNU new_ucmd!() @@ -354,15 +355,47 @@ fn test_file_is_newer_than_and_older_than_itself() { } #[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() { 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") - .args(&["-m", "-d", "last Thursday", "regular_file"]) - .succeeds(); scenario .ucmd() @@ -370,7 +403,7 @@ fn test_newer_file() { .succeeds(); scenario .ucmd() - .args(&["regular_file", "-ot", "newer_file"]) + .args(&["newer_file", "-ot", "regular_file"]) .succeeds(); } @@ -885,6 +918,7 @@ fn test_bracket_syntax_version() { #[test] #[allow(non_snake_case)] +#[cfg(unix)] fn test_file_N() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures;