use uutests::at_and_ucmd;
use uutests::new_ucmd;
use uutests::unwrap_or_return;
use uutests::util::{TestScenario, expected_result};
use uutests::util_name;
use std::fs::{File, FileTimes, metadata};
use std::os::unix::fs::MetadataExt;
use std::time::{Duration, UNIX_EPOCH};
#[test]
fn test_invalid_arg() {
new_ucmd!().arg("--definitely-invalid").fails_with_code(1);
}
#[test]
fn test_invalid_option() {
new_ucmd!().arg("-w").arg("-q").arg("/").fails();
}
#[cfg(unix)]
const NORMAL_FORMAT_STR: &str =
"%a %A %b %B %d %D %f %F %g %G %h %i %m %n %o %s %u %U %x %X %y %Y %z %Z"; #[cfg(any(target_os = "linux", target_os = "android"))]
const DEV_FORMAT_STR: &str =
"%a %A %b %B %d %D %f %F %g %G %h %i %m %n %o %s (%t/%T) %u %U %w %W %x %X %y %Y %z %Z";
#[cfg(target_os = "linux")]
const FS_FORMAT_STR: &str = "%b %c %i %l %n %s %S %t %T";
#[test]
#[cfg(any(target_os = "linux", target_os = "android"))]
fn test_terse_fs_format() {
let args = ["-f", "-t", "/proc"];
let ts = TestScenario::new(util_name!());
let expected_stdout = unwrap_or_return!(expected_result(&ts, &args)).stdout_move_str();
ts.ucmd().args(&args).succeeds().stdout_is(expected_stdout);
}
#[test]
#[cfg(target_os = "linux")]
fn test_fs_format() {
let args = ["-f", "-c", FS_FORMAT_STR, "/dev/shm"];
let ts = TestScenario::new(util_name!());
let expected_stdout = unwrap_or_return!(expected_result(&ts, &args)).stdout_move_str();
ts.ucmd().args(&args).succeeds().stdout_is(expected_stdout);
}
#[test]
#[cfg(any(target_os = "linux", target_os = "android"))]
fn test_fs_default_format_block_size_label() {
new_ucmd!()
.args(&["-f", "/"])
.succeeds()
.stdout_contains("Block size:");
}
#[cfg(unix)]
#[test]
fn test_terse_normal_format() {
let args = ["-t", "/"];
let ts = TestScenario::new(util_name!());
let actual = ts.ucmd().args(&args).succeeds().stdout_move_str();
let expect = unwrap_or_return!(expected_result(&ts, &args)).stdout_move_str();
println!("actual: {actual:?}");
println!("expect: {expect:?}");
let v_actual: Vec<&str> = actual.trim().split(' ').collect();
let mut v_expect: Vec<&str> = expect.trim().split(' ').collect();
assert!(!v_expect.is_empty());
if v_actual.len() == v_expect.len() - 1 && v_expect[v_expect.len() - 1].contains(':') {
v_expect.pop();
}
assert!(
expect == "0"
|| expect == "0\n"
|| v_actual
.iter()
.zip(v_expect.iter())
.all(|(a, e)| a == e || *e == "0" || *e == "0\n")
);
}
#[cfg(unix)]
#[test]
fn test_format_created_time() {
let args = ["-c", "%w", "/bin"];
let ts = TestScenario::new(util_name!());
let actual = ts.ucmd().args(&args).succeeds().stdout_move_str();
let expect = unwrap_or_return!(expected_result(&ts, &args)).stdout_move_str();
println!("actual: {actual:?}");
println!("expect: {expect:?}");
let re = regex::Regex::new(r"\s").unwrap();
let v_actual: Vec<&str> = re.split(&actual).collect();
let v_expect: Vec<&str> = re.split(&expect).collect();
assert!(!v_expect.is_empty());
assert!(
expect == "-"
|| expect == "-\n"
|| v_actual
.iter()
.zip(v_expect.iter())
.all(|(a, e)| a == e || *e == "-" || *e == "-\n")
);
}
#[cfg(unix)]
#[test]
fn test_format_created_seconds() {
let args = ["-c", "%W", "/bin"];
let ts = TestScenario::new(util_name!());
let actual = ts.ucmd().args(&args).succeeds().stdout_move_str();
let expect = unwrap_or_return!(expected_result(&ts, &args)).stdout_move_str();
println!("actual: {actual:?}");
println!("expect: {expect:?}");
let re = regex::Regex::new(r"\s").unwrap();
let v_actual: Vec<&str> = re.split(&actual).collect();
let v_expect: Vec<&str> = re.split(&expect).collect();
assert!(!v_expect.is_empty());
assert!(
expect == "0"
|| expect == "0\n"
|| v_actual
.iter()
.zip(v_expect.iter())
.all(|(a, e)| a == e || *e == "0" || *e == "0\n")
);
}
#[cfg(unix)]
#[test]
fn test_normal_format() {
let args = ["-c", NORMAL_FORMAT_STR, "/bin"];
let ts = TestScenario::new(util_name!());
let expected_stdout = unwrap_or_return!(expected_result(&ts, &args)).stdout_move_str();
ts.ucmd().args(&args).succeeds().stdout_is(expected_stdout);
}
#[cfg(unix)]
#[cfg(not(target_os = "openbsd"))]
#[test]
fn test_symlinks() {
let ts = TestScenario::new(util_name!());
let at = &ts.fixtures;
let mut tested: bool = false;
for file in [
"/bin/sh",
"/data/data/com.termux/files/usr/bin/sh", "/bin/sudoedit",
"/usr/bin/ex",
"/etc/localtime",
"/etc/aliases",
] {
if at.file_exists(file) && at.is_symlink(file) {
tested = true;
let args = ["-c", NORMAL_FORMAT_STR, file];
let expected_stdout = unwrap_or_return!(expected_result(&ts, &args)).stdout_move_str();
ts.ucmd().args(&args).succeeds().stdout_is(expected_stdout);
let args = ["-L", "-c", NORMAL_FORMAT_STR, file];
let expected_stdout = unwrap_or_return!(expected_result(&ts, &args)).stdout_move_str();
ts.ucmd().args(&args).succeeds().stdout_is(expected_stdout);
}
}
assert!(tested, "No symlink found to test in this environment");
}
#[cfg(any(target_vendor = "apple", target_os = "linux", target_os = "android"))]
#[test]
fn test_char() {
let args = [
"-c",
#[cfg(any(target_os = "linux", target_os = "android"))]
DEV_FORMAT_STR,
#[cfg(target_os = "linux")]
"/dev/pts/ptmx",
#[cfg(target_vendor = "apple")]
"%a %A %b %B %d %D %f %F %g %G %h %i %m %n %o %s (/%T) %u %U %W %X %y %Y %z %Z",
#[cfg(any(target_vendor = "apple", target_os = "android"))]
"/dev/ptmx",
];
let ts = TestScenario::new(util_name!());
let expected_stdout = unwrap_or_return!(expected_result(&ts, &args)).stdout_move_str();
eprintln!("{expected_stdout}");
ts.ucmd().args(&args).succeeds().stdout_is(expected_stdout);
}
#[cfg(target_os = "linux")]
#[test]
fn test_printf_atime_ctime_mtime_precision() {
let args = ["-c", "%.0Y %.1Y %.2X %.2Y %.2Z", "/dev/pts/ptmx"];
let ts = TestScenario::new(util_name!());
let expected_stdout = unwrap_or_return!(expected_result(&ts, &args)).stdout_move_str();
eprintln!("{expected_stdout}");
ts.ucmd().args(&args).succeeds().stdout_is(expected_stdout);
}
#[cfg(feature = "touch")]
#[test]
fn test_timestamp_format() {
let ts = TestScenario::new(util_name!());
ts.ccmd("touch")
.args(&["-d", "1970-01-01 18:43:33.023456789", "k"])
.succeeds()
.no_stderr();
let test_cases = vec![
("%Y", "67413"),
("%.Y", "67413.023456789"),
("%.1Y", "67413.0"),
("%.3Y", "67413.023"),
("%.6Y", "67413.023456"),
("%.9Y", "67413.023456789"),
("%13.6Y", " 67413.023456"),
("%013.6Y", "067413.023456"),
("%-13.6Y", "67413.023456 "),
("%18.10Y", " 67413.0234567890"),
("%I18.10Y", " 67413.0234567890"),
("%018.10Y", "0067413.0234567890"),
("%-18.10Y", "67413.0234567890 "),
];
for (format_str, expected) in test_cases {
let result = ts
.ucmd()
.args(&["-c", format_str, "k"])
.succeeds()
.stdout_move_str();
assert_eq!(
result,
format!("{expected}\n"),
"Format '{format_str}' failed.\nExpected: '{expected}'\nGot: '{result}'",
);
}
}
#[test]
fn test_timestamp_format_preserves_nanoseconds() {
let ts = TestScenario::new(util_name!());
let path = ts.fixtures.plus("timestamp");
let file = File::create(&path).unwrap();
let timestamp = UNIX_EPOCH + Duration::new(1_755_300_000, 123_456_789);
file.set_times(
FileTimes::new()
.set_accessed(timestamp)
.set_modified(timestamp),
)
.unwrap();
let metadata = metadata(&path).unwrap();
let expected = format!(
"1755300000.123456789 1755300000.123456789 {}.{:09}\n",
metadata.ctime(),
metadata.ctime_nsec()
);
ts.ucmd()
.args(&["-c", "%.9X %.9Y %.9Z", "timestamp"])
.succeeds()
.stdout_is(expected);
}
#[test]
fn test_timestamp_format_before_epoch() {
let ts = TestScenario::new(util_name!());
let path = ts.fixtures.plus("timestamp");
let file = File::create(&path).unwrap();
let timestamp = UNIX_EPOCH - Duration::new(0, 876_543_211);
file.set_times(
FileTimes::new()
.set_accessed(timestamp)
.set_modified(timestamp),
)
.unwrap();
ts.ucmd()
.args(&["-c", "%.1X %.3X %.9X %.1Y %.3Y %.9Y", "timestamp"])
.succeeds()
.stdout_is("-0.9 -0.877 -0.876543211 -0.9 -0.877 -0.876543211\n");
}
#[cfg(any(target_vendor = "apple", target_os = "linux", target_os = "android"))]
#[test]
fn test_date() {
let args = [
"-c",
#[cfg(any(target_os = "linux", target_os = "android"))]
"%z",
#[cfg(target_os = "linux")]
"/bin/sh",
#[cfg(target_vendor = "apple")]
"%z",
#[cfg(any(target_vendor = "apple", target_os = "android"))]
"/bin/sh",
];
let ts = TestScenario::new(util_name!());
let expected_stdout = unwrap_or_return!(expected_result(&ts, &args)).stdout_move_str();
ts.ucmd().args(&args).succeeds().stdout_is(expected_stdout);
let args = [
"-c",
#[cfg(any(target_os = "linux", target_os = "android"))]
"%z",
#[cfg(target_os = "linux")]
"/dev/ptmx",
#[cfg(target_vendor = "apple")]
"%z",
#[cfg(any(target_vendor = "apple", target_os = "android"))]
"/dev/ptmx",
];
let ts = TestScenario::new(util_name!());
let expected_stdout = unwrap_or_return!(expected_result(&ts, &args)).stdout_move_str();
ts.ucmd().args(&args).succeeds().stdout_is(expected_stdout);
}
#[cfg(unix)]
#[test]
fn test_multi_files() {
let args = [
"-c",
NORMAL_FORMAT_STR,
"/dev",
"/usr/lib",
#[cfg(target_os = "linux")]
"/etc/fstab",
"/var",
];
let ts = TestScenario::new(util_name!());
let expected_stdout = unwrap_or_return!(expected_result(&ts, &args)).stdout_move_str();
ts.ucmd().args(&args).succeeds().stdout_is(expected_stdout);
}
#[cfg(unix)]
#[test]
fn test_printf() {
let args = [
"--printf=123%-# 15q\\r\\\"\\\\\\a\\b\\x1B\\f\\x0B%+020.23m\\x12\\167\\132\\112\\n",
"/",
];
let ts = TestScenario::new(util_name!());
let expected_stdout = unwrap_or_return!(expected_result(&ts, &args)).stdout_move_str();
ts.ucmd().args(&args).succeeds().stdout_is(expected_stdout);
}
#[test]
#[cfg(unix)]
fn test_pipe_fifo() {
let (at, mut ucmd) = at_and_ucmd!();
at.mkfifo("FIFO");
ucmd.arg("FIFO")
.succeeds()
.no_stderr()
.stdout_contains("fifo")
.stdout_contains("File: FIFO");
}
#[test]
#[cfg(all(
unix,
not(any(
target_vendor = "apple",
target_os = "android",
target_os = "freebsd",
target_os = "openbsd"
))
))]
fn test_stdin_pipe_fifo1() {
new_ucmd!()
.arg("-")
.set_stdin(std::process::Stdio::piped())
.succeeds()
.no_stderr()
.stdout_contains("fifo")
.stdout_contains("File: -");
new_ucmd!()
.args(&["-L", "-"])
.set_stdin(std::process::Stdio::piped())
.succeeds()
.no_stderr()
.stdout_contains("fifo")
.stdout_contains("File: -");
}
#[test]
#[cfg(all(unix, not(any(target_vendor = "apple", target_os = "android"))))]
fn test_stdin_pipe_fifo2() {
new_ucmd!()
.arg("-")
.set_stdin(std::process::Stdio::null())
.succeeds()
.no_stderr()
.stdout_contains("character special file")
.stdout_contains("File: -");
}
#[test]
#[cfg(all(unix, not(target_os = "android")))]
fn test_stdin_with_fs_option() {
new_ucmd!()
.arg("-f")
.arg("-")
.set_stdin(std::process::Stdio::null())
.fails_with_code(1)
.stderr_contains("using '-' to denote standard input does not work in file system mode");
}
#[test]
#[cfg(all(
unix,
not(any(
target_vendor = "apple",
target_os = "android",
target_os = "freebsd",
target_os = "openbsd"
))
))]
fn test_stdin_redirect() {
let ts = TestScenario::new(util_name!());
let at = &ts.fixtures;
at.touch("f");
ts.ucmd()
.arg("-")
.set_stdin(File::open(at.plus("f")).unwrap())
.succeeds()
.no_stderr()
.stdout_contains("regular empty file")
.stdout_contains("File: -");
}
#[test]
fn test_without_argument() {
new_ucmd!()
.fails()
.stderr_contains("the following required arguments were not provided"); }
#[test]
fn test_quoting_style_locale() {
let ts = TestScenario::new(util_name!());
let at = &ts.fixtures;
at.touch("'");
ts.ucmd()
.env("QUOTING_STYLE", "locale")
.args(&["-c", "%N", "'"])
.succeeds()
.stdout_only("'\\''\n");
ts.ucmd()
.args(&["-c", "%N", "'"])
.succeeds()
.stdout_only("\"'\"\n");
at.touch("\"");
ts.ucmd()
.args(&["-c", "%N", "\""])
.succeeds()
.stdout_only("\'\"\'\n");
}
#[test]
fn test_quoting_newline_in_filename() {
let ts = TestScenario::new(util_name!());
let at = &ts.fixtures;
at.touch("test\nnewline");
ts.ucmd()
.args(&["-c", r#"{"name":"%N"}"#, "test\nnewline"])
.succeeds()
.stdout_only("{\"name\":\"'test'$'\\n''newline'\"}\n");
at.touch("contiguous\n\nescape_characters");
ts.ucmd()
.args(&["-c", "%N", "contiguous\n\nescape_characters"])
.succeeds()
.stdout_only("'contiguous'$'\\n\\n''escape_characters'\n");
at.touch("multiple\nescape\ncharacters");
ts.ucmd()
.args(&["-c", "%N", "multiple\nescape\ncharacters"])
.succeeds()
.stdout_only("'multiple'$'\\n''escape'$'\\n''characters'\n");
at.touch("\t \n \r \x01");
ts.ucmd()
.args(&["-c", "%N", "\t \n \r \x01"])
.succeeds()
.stdout_only("''$'\\t'' '$'\\n'' '$'\\r'' '$'\\001'\n");
}
#[test]
fn test_quoting_style_invalid_env() {
let ts = TestScenario::new(util_name!());
let at = &ts.fixtures;
at.touch("baguette");
at.touch("Croissant");
at.touch("Escargot");
let needle = "ignoring invalid value of environment variable QUOTING_STYLE";
let res = ts
.ucmd()
.env("QUOTING_STYLE", "fromage")
.args(&["-c", "nom=[%N]", "baguette", "Croissant", "Escargot"])
.succeeds();
res.stdout_is("nom=['baguette']\nnom=['Croissant']\nnom=['Escargot']\n");
assert_eq!(res.stderr_str().matches(needle).count(), 1);
ts.ucmd()
.env("QUOTING_STYLE", "")
.args(&["-c", "%N", "baguette"])
.succeeds()
.stdout_is("'baguette'\n")
.stderr_is("stat: ignoring invalid value of environment variable QUOTING_STYLE: ''\n");
ts.ucmd()
.env("QUOTING_STYLE", "soufflé")
.args(&["-c", "%%%N", "baguette"])
.succeeds()
.stdout_is("%'baguette'\n")
.stderr_is(
"stat: ignoring invalid value of environment variable QUOTING_STYLE: 'soufflé'\n",
);
ts.ucmd()
.env("QUOTING_STYLE", "crème-brûlée")
.args(&["-c", "taille=%s genre:%F brut=%n", "baguette"])
.succeeds()
.no_stderr();
}
#[test]
fn test_printf_octal_1() {
let ts = TestScenario::new(util_name!());
let expected_stdout = vec![0x0A, 0xFF]; ts.ucmd()
.args(&["--printf=\\012\\377", "."])
.succeeds()
.stdout_is_bytes(expected_stdout);
}
#[test]
fn test_printf_octal_2() {
let ts = TestScenario::new(util_name!());
let expected_stdout = vec![b'.', 0x0A, b'a', 0xFF, b'b'];
ts.ucmd()
.args(&["--printf=.\\012a\\377b", "."])
.succeeds()
.stdout_is_bytes(expected_stdout);
}
#[test]
fn test_printf_incomplete_hex() {
let ts = TestScenario::new(util_name!());
ts.ucmd()
.args(&["--printf=\\x", "."])
.succeeds()
.stderr_contains("warning: incomplete hex escape");
}
#[test]
fn test_printf_bel_etc() {
let ts = TestScenario::new(util_name!());
let expected_stdout = vec![0x07, 0x08, 0x0C, 0x0A, 0x0D, 0x09]; ts.ucmd()
.args(&["--printf=\\a\\b\\f\\n\\r\\t", "."])
.succeeds()
.stdout_is_bytes(expected_stdout);
}
#[test]
fn test_printf_invalid_directive() {
let ts = TestScenario::new(util_name!());
ts.ucmd()
.args(&["--printf=%9", "."])
.fails_with_code(1)
.stderr_contains("'%9': invalid directive");
ts.ucmd()
.args(&["--printf=%9%", "."])
.fails_with_code(1)
.stderr_contains("'%9%': invalid directive");
}
#[test]
fn test_invalid_directive_after_multibyte_char() {
let ts = TestScenario::new(util_name!());
for (fmt, before, directive) in [("€%-", "€", "%-"), ("ä%0", "ä", "%0"), ("€%.", "€", "%.")]
{
ts.ucmd()
.args(&["-c", fmt, "."])
.fails_with_code(1)
.stdout_is(before)
.stderr_is(format!("stat: '{directive}': invalid directive\n"));
}
}
#[test]
fn test_precision_splits_multibyte_char_in_value() {
let ts = TestScenario::new(util_name!());
let at = &ts.fixtures;
at.touch("é");
ts.ucmd()
.args(&["-c", "%.1n", "é"])
.succeeds()
.stdout_only_bytes([0xc3, b'\n']);
}
#[test]
#[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))]
fn test_stat_selinux() {
let ts = TestScenario::new(util_name!());
let at = &ts.fixtures;
at.touch("f");
ts.ucmd()
.arg("--printf='%C'")
.arg("f")
.succeeds()
.no_stderr()
.stdout_contains("unconfined_u");
ts.ucmd()
.arg("--printf='%C'")
.arg("/bin/")
.succeeds()
.no_stderr()
.stdout_contains("system_u");
let result = ts.ucmd().arg("--printf='%C'").arg("/bin/").succeeds();
let s: Vec<_> = result.stdout_str().split(':').collect();
assert_eq!(s.len(), 4);
}
#[cfg(unix)]
#[test]
fn test_mount_point_basic() {
let ts = TestScenario::new(util_name!());
let result = ts.ucmd().args(&["-c", "%m", "/"]).succeeds();
let output = result.stdout_str().trim();
assert!(!output.is_empty(), "Mount point should not be empty");
assert_eq!(output, "/");
}
#[cfg(unix)]
#[test]
fn test_mount_point_width_and_alignment() {
let ts = TestScenario::new(util_name!());
let result = ts.ucmd().args(&["-c", "%15m", "/"]).succeeds();
let output = result.stdout_str();
assert!(
output.trim().len() <= 15 && output.len() >= 15,
"Output should be padded to width 15"
);
let result = ts.ucmd().args(&["-c", "%-15m", "/"]).succeeds();
let output = result.stdout_str();
assert!(
output.trim().len() <= 15 && output.len() >= 15,
"Output should be padded to width 15 (left-aligned)"
);
}
#[cfg(unix)]
#[test]
fn test_mount_point_combined_with_other_specifiers() {
let ts = TestScenario::new(util_name!());
let result = ts.ucmd().args(&["-c", "%m %n %s", "/bin/sh"]).succeeds();
let output = result.stdout_str();
let parts: Vec<&str> = output.split_whitespace().collect();
assert!(
parts.len() >= 3,
"Should print mount point, file name, and size"
);
}
#[cfg(unix)]
#[test]
fn test_percent_escaping() {
let ts = TestScenario::new(util_name!());
let result = ts
.ucmd()
.args(&["--printf", "%%%m%%m%m%%%", "/bin/sh"])
.succeeds();
assert_eq!(result.stdout_str(), "%/%m/%%");
}
#[cfg(unix)]
#[test]
fn test_correct_metadata() {
use uucore::fs::{major, minor};
let ts = TestScenario::new(util_name!());
let parse = |(i, str): (usize, &str)| {
let radix = if matches!(i, 2 | 10 | 14..) { 16 } else { 10 };
i128::from_str_radix(str, radix)
};
for device in ["/", "/dev/null"] {
let metadata = metadata(device).unwrap();
let test_str = "%u %g %f %b %s %h %i %d %Hd %Ld %D %r %Hr %Lr %R %t %T";
let expected = [
metadata.uid() as _,
metadata.gid() as _,
metadata.mode() as _,
metadata.blocks() as _,
metadata.size() as _,
metadata.nlink() as _,
metadata.ino() as _,
metadata.dev() as _,
major(metadata.dev() as _) as _,
minor(metadata.dev() as _) as _,
metadata.dev() as _,
metadata.rdev() as _,
major(metadata.rdev() as _) as _,
minor(metadata.rdev() as _) as _,
metadata.rdev() as _,
major(metadata.rdev() as _) as _,
minor(metadata.rdev() as _) as _,
];
let result = ts.ucmd().args(&["--printf", test_str, device]).succeeds();
let output = result
.stdout_str()
.split(' ')
.enumerate()
.map(parse)
.collect::<Result<Vec<i128>, _>>()
.unwrap();
assert_eq!(output, &expected);
}
}
#[test]
fn test_no_such_directory_message() {
let ts = TestScenario::new(util_name!());
ts.ucmd()
.arg("a")
.fails_with_code(1)
.stderr_is("stat: cannot statx 'a': No such file or directory\n");
}
#[cfg(all(feature = "feat_diagnostics", not(wasi_runner)))]
mod diagnostics {
use super::*;
#[cfg(unix)]
#[test]
fn test_snippet_points_at_the_failing_directive() {
let result = new_ucmd!()
.terminal_sim_stderr()
.args(&["-c", "%d%.3", "/dev/null"])
.fails_with_code(1);
assert_eq!(
result.stderr_as_displayed(),
"\
stat: '%.3': invalid directive
╭─[ stat:1:11 ]
│
1 │ stat -c %d%.3 /dev/null
│ ───
│
│ Help: a directive is %[FLAGS][WIDTH][.PRECISION]LETTER, as in %-10.2s; a literal % is written %%
───╯"
);
}
#[cfg(unix)]
#[test]
fn test_snippet_points_inside_a_printf_format() {
let result = new_ucmd!()
.terminal_sim_stderr()
.args(&["--printf=%12", "/dev/null"])
.fails_with_code(1);
let stderr = result.stderr_as_displayed();
assert!(stderr.contains("stat:1:15"), "{stderr}");
assert!(stderr.contains("'%12': invalid directive"), "{stderr}");
}
#[test]
fn test_plain_message_when_stderr_is_a_pipe() {
new_ucmd!()
.args(&["-c", "%d%.3", "/dev/null"])
.fails_with_code(1)
.stderr_is("stat: '%.3': invalid directive\n");
}
}