date: fix subfmt-up1, fill-1, pct-pct, and invalid-high-bit-set tests (#10940)

This commit is contained in:
Sylvestre Ledru
2026-02-15 09:14:29 -08:00
committed by GitHub
parent d13a2e0077
commit a7f55cbedc
6 changed files with 1182 additions and 28 deletions
Generated
+1
View File
@@ -3348,6 +3348,7 @@ dependencies = [
"jiff-icu",
"nix",
"parse_datetime",
"regex",
"tempfile",
"uucore",
"windows-sys 0.61.2",
+33
View File
@@ -21,6 +21,15 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "android_system_properties"
version = "0.1.5"
@@ -1398,11 +1407,34 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "regex"
version = "1.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c"
[[package]]
name = "rust-ini"
@@ -1754,6 +1786,7 @@ dependencies = [
"jiff-icu",
"nix",
"parse_datetime",
"regex",
"uucore",
"windows-sys 0.61.2",
]
+1
View File
@@ -40,6 +40,7 @@ jiff = { workspace = true, features = [
"tzdb-concatenated",
] }
parse_datetime = { workspace = true }
regex = { workspace = true }
uucore = { workspace = true, features = ["parser", "i18n-datetime"] }
[target.'cfg(unix)'.dependencies]
+171 -28
View File
@@ -5,6 +5,7 @@
// spell-checker:ignore strtime ; (format) DATEFILE MMDDhhmm ; (vars) datetime datetimes getres AWST ACST AEST foobarbaz
mod format_modifiers;
mod locale;
use clap::{Arg, ArgAction, Command};
@@ -14,7 +15,7 @@ use jiff::{Timestamp, Zoned};
use std::borrow::Cow;
use std::collections::HashMap;
use std::fs::File;
use std::io::{BufRead, BufReader, BufWriter, Write};
use std::io::{BufRead, BufReader, BufWriter, Read, Write};
use std::path::PathBuf;
use std::sync::OnceLock;
use uucore::display::Quotable;
@@ -57,6 +58,25 @@ struct Settings {
format: Format,
date_source: DateSource,
set_to: Option<Zoned>,
debug: bool,
}
/// Options for parsing dates
#[derive(Clone, Copy)]
struct DebugOptions {
/// Enable debug output
debug: bool,
/// Warn when midnight is used without explicit time specification
warn_midnight: bool,
}
impl DebugOptions {
fn new(debug: bool, warn_midnight: bool) -> Self {
Self {
debug,
warn_midnight,
}
}
}
/// Various ways of displaying the date
@@ -133,6 +153,40 @@ enum DayDelta {
Next,
}
/// Escape invalid UTF-8 bytes in GNU-compatible octal notation.
///
/// Converts bytes to a string with printable ASCII characters preserved
/// and non-printable/invalid UTF-8 bytes escaped as `\NNN` octal sequences.
///
/// This matches GNU date's behavior for invalid input.
///
/// # Arguments
/// * `bytes` - The byte sequence to escape
///
/// # Returns
/// A string with invalid bytes escaped in octal notation
///
/// # Example
/// ```ignore
/// let invalid = b"\xb0";
/// assert_eq!(escape_invalid_bytes(invalid), "\\260");
/// ```
fn escape_invalid_bytes(bytes: &[u8]) -> String {
let escaped = bytes
.iter()
.flat_map(|&b| {
// Preserve printable ASCII except backslash
if (0x20..0x7f).contains(&b) && b != b'\\' {
vec![b]
} else {
// Escape as octal: \NNN
format!("\\{b:03o}").into_bytes()
}
})
.collect::<Vec<u8>>();
String::from_utf8_lossy(&escaped).into_owned()
}
/// Strip parenthesized comments from a date string.
///
/// GNU date removes balanced parentheses and their content, treating them as comments.
@@ -270,6 +324,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
};
let utc = matches.get_flag(OPT_UNIVERSAL);
let debug_mode = matches.get_flag(OPT_DEBUG);
// Get the current time, either in the local time zone or UTC.
let now = if utc {
@@ -278,7 +333,13 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
Zoned::now()
};
let date_source = if let Some(date) = matches.get_one::<String>(OPT_DATE) {
let date_source = if let Some(date_os) = matches.get_one::<std::ffi::OsString>(OPT_DATE) {
// Convert OsString to String, handling invalid UTF-8 with GNU-compatible error
let date = date_os.to_str().ok_or_else(|| {
let bytes = date_os.as_encoded_bytes();
let escaped_str = escape_invalid_bytes(bytes);
USimpleError::new(1, format!("invalid date '{escaped_str}'"))
})?;
DateSource::Human(date.into())
} else if let Some(file) = matches.get_one::<String>(OPT_FILE) {
match file.as_ref() {
@@ -295,7 +356,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let set_to = match matches
.get_one::<String>(OPT_SET)
.map(|s| parse_date(s, &now))
.map(|s| parse_date(s, &now, DebugOptions::new(debug_mode, true)))
{
None => None,
Some(Err((input, _err))) => {
@@ -312,6 +373,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
format,
date_source,
set_to,
debug: debug_mode,
};
if let Some(date) = settings.set_to {
@@ -363,7 +425,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
} else {
format!("{date_part} 00:00 {offset}")
};
parse_date(composed, &now)
if settings.debug {
eprintln!("date: warning: using midnight as starting time: 00:00:00");
}
parse_date(composed, &now, DebugOptions::new(settings.debug, false))
} else if let Some((total_hours, day_delta)) = military_tz_with_offset {
// Military timezone with optional hour offset
// Convert to UTC time: midnight + military_tz_offset + additional_hours
@@ -383,7 +448,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
DayDelta::Previous => format_date_with_epoch_fallback(now.yesterday()),
};
let composed = format!("{date_part} {total_hours:02}:00:00 +00:00");
parse_date(composed, &now)
parse_date(composed, &now, DebugOptions::new(settings.debug, false))
} else if is_pure_digits {
// Derive HH and MM from the input
let (hh_opt, mm_opt) = if input.len() <= 2 {
@@ -409,23 +474,23 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
} else {
format!("{date_part} {hh:02}:{mm:02} {offset}")
};
parse_date(composed, &now)
parse_date(composed, &now, DebugOptions::new(settings.debug, false))
} else {
// Fallback on parse failure of digits
parse_date(input, &now)
parse_date(input, &now, DebugOptions::new(settings.debug, true))
}
} else {
parse_date(input, &now)
parse_date(input, &now, DebugOptions::new(settings.debug, true))
};
let iter = std::iter::once(date);
Box::new(iter)
}
DateSource::Stdin => {
let lines = BufReader::new(std::io::stdin()).lines();
let iter = lines.map_while(Result::ok).map(|s| parse_date(s, &now));
Box::new(iter)
}
DateSource::Stdin => parse_dates_from_reader(
std::io::stdin(),
&now,
DebugOptions::new(settings.debug, true),
),
DateSource::File(ref path) => {
if path.is_dir() {
return Err(USimpleError::new(
@@ -435,9 +500,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
}
let file =
File::open(path).map_err_context(|| path.as_os_str().maybe_quote().to_string())?;
let lines = BufReader::new(file).lines();
let iter = lines.map_while(Result::ok).map(|s| parse_date(s, &now));
Box::new(iter)
parse_dates_from_reader(file, &now, DebugOptions::new(settings.debug, true))
}
DateSource::FileMtime(ref path) => {
let metadata = std::fs::metadata(path)
@@ -528,6 +591,7 @@ pub fn uu_app() -> Command {
.value_name("STRING")
.allow_hyphen_values(true)
.overrides_with(OPT_DATE)
.value_parser(clap::value_parser!(std::ffi::OsString))
.help(translate!("date-help-date")),
)
.arg(
@@ -630,15 +694,25 @@ fn format_date_with_locale_aware_months(
format_string: &str,
config: &Config<PosixCustom>,
skip_localization: bool,
) -> Result<String, jiff::Error> {
let broken_down = BrokenDownTime::from(date);
if !should_use_icu_locale() || skip_localization {
return broken_down.to_string_with_config(config, format_string);
) -> Result<String, String> {
// First check if format string has GNU modifiers (width/flags) and format if present
// This optimization combines detection and formatting in a single pass
if let Some(result) =
format_modifiers::format_with_modifiers_if_present(date, format_string, config)
{
return result.map_err(|e| e.to_string());
}
let fmt = localize_format_string(format_string, date.date());
broken_down.to_string_with_config(config, &fmt)
let broken_down = BrokenDownTime::from(date);
let result = if !should_use_icu_locale() || skip_localization {
broken_down.to_string_with_config(config, format_string)
} else {
let fmt = localize_format_string(format_string, date.date());
broken_down.to_string_with_config(config, &fmt)
};
result.map_err(|e| e.to_string())
}
/// Return the appropriate format string for the given settings.
@@ -788,6 +862,23 @@ fn try_parse_with_abbreviation<S: AsRef<str>>(date_str: S) -> Option<Zoned> {
/// Parse a `String` into a `DateTime`.
/// If it fails, return a tuple of the `String` along with its `ParseError`.
/// Helper function to parse dates from a line-based reader (stdin or file)
///
/// Takes any `Read` source, reads it line by line, and parses each line as a date.
/// Returns a boxed iterator over the parse results.
fn parse_dates_from_reader<R: Read + 'static>(
reader: R,
now: &Zoned,
dbg_opts: DebugOptions,
) -> Box<dyn Iterator<Item = Result<Zoned, (String, parse_datetime::ParseDateTimeError)>> + '_> {
let lines = BufReader::new(reader).lines();
Box::new(
lines
.map_while(Result::ok)
.map(move |s| parse_date(s, now, dbg_opts)),
)
}
///
/// **Update for parse_datetime 0.13:**
/// - parse_datetime 0.11: returned `chrono::DateTime` → required conversion to `jiff::Zoned`
@@ -798,17 +889,64 @@ fn try_parse_with_abbreviation<S: AsRef<str>>(date_str: S) -> Option<Zoned> {
fn parse_date<S: AsRef<str> + Clone>(
s: S,
now: &Zoned,
dbg_opts: DebugOptions,
) -> Result<Zoned, (String, parse_datetime::ParseDateTimeError)> {
let input_str = s.as_ref();
if dbg_opts.debug {
eprintln!("date: input string: {input_str}");
}
// First, try to parse any timezone abbreviations
if let Some(zoned) = try_parse_with_abbreviation(s.as_ref()) {
if let Some(zoned) = try_parse_with_abbreviation(input_str) {
if dbg_opts.debug {
eprintln!(
"date: parsed date part: (Y-M-D) {}",
strtime::format("%Y-%m-%d", &zoned).unwrap_or_default()
);
eprintln!(
"date: parsed time part: {}",
strtime::format("%H:%M:%S", &zoned).unwrap_or_default()
);
let tz_display = zoned.time_zone().iana_name().unwrap_or("system default");
eprintln!("date: input timezone: {tz_display}");
}
return Ok(zoned);
}
match parse_datetime::parse_datetime_at_date(now.clone(), s.as_ref()) {
match parse_datetime::parse_datetime_at_date(now.clone(), input_str) {
// Convert to system timezone for display
// (parse_datetime 0.13 returns Zoned in the input's timezone)
Ok(date) => Ok(date.timestamp().to_zoned(now.time_zone().clone())),
Err(e) => Err((s.as_ref().into(), e)),
Ok(date) => {
let result = date.timestamp().to_zoned(now.time_zone().clone());
if dbg_opts.debug {
// Show final parsed date and time
eprintln!(
"date: parsed date part: (Y-M-D) {}",
strtime::format("%Y-%m-%d", &result).unwrap_or_default()
);
eprintln!(
"date: parsed time part: {}",
strtime::format("%H:%M:%S", &result).unwrap_or_default()
);
// Show timezone information
eprintln!("date: input timezone: system default");
// Check if time component was specified, if not warn about midnight usage
// Only warn for date-only inputs (no time specified), but not for epoch formats (@N)
// or inputs that explicitly specify a time (containing ':')
if dbg_opts.warn_midnight && !input_str.contains(':') && !input_str.contains('@') {
// Input likely didn't specify a time, so midnight was assumed
let time_str = strtime::format("%H:%M:%S", &result).unwrap_or_default();
if time_str == "00:00:00" {
eprintln!("date: warning: using midnight as starting time: 00:00:00");
}
}
}
Ok(result)
}
Err(e) => Err((input_str.into(), e)),
}
}
@@ -965,7 +1103,12 @@ mod tests {
fn test_utc_conversion_preserves_offset() {
let now = Zoned::now();
let date = parse_date("Sat 20 Mar 2021 14:53:01 AWST", &now).unwrap();
let date = parse_date(
"Sat 20 Mar 2021 14:53:01 AWST",
&now,
DebugOptions::new(false, false),
)
.unwrap();
let utc = convert_for_set(date, true);
assert_eq!((utc.hour(), utc.minute(), utc.second()), (6, 53, 1)); // AWST(+08:00) -> -8h
}
File diff suppressed because it is too large Load Diff
+407
View File
@@ -2194,3 +2194,410 @@ fn test_date_cross_tz_mishandled() {
.stdout_contains("21:00:00")
.stdout_contains("1969");
}
// Tests for GNU test invalid-high-bit-set: invalid UTF-8 in date string
#[test]
#[cfg(unix)]
fn test_date_invalid_high_bit_set() {
use std::os::unix::ffi::OsStrExt;
// GNU test invalid-high-bit-set: Invalid UTF-8 byte (0xb0) should produce
// GNU-compatible error message with octal escape sequence
let invalid_bytes = b"\xb0";
let invalid_arg = std::ffi::OsStr::from_bytes(invalid_bytes);
new_ucmd!()
.args(&[std::ffi::OsStr::new("-d"), invalid_arg])
.fails()
.code_is(1)
.stderr_contains("invalid date '\\260'");
}
// Tests for GNU format modifiers
#[test]
fn test_date_format_modifier_width() {
// Test width modifier: %10Y should pad year to 10 digits
new_ucmd!()
.env("TZ", "UTC")
.args(&["-d", "1999-06-01", "+%10Y"])
.succeeds()
.stdout_is("0000001999\n");
}
#[test]
fn test_date_format_modifier_underscore_padding() {
// Test underscore flag: %_10m should pad month with spaces
new_ucmd!()
.env("TZ", "UTC")
.args(&["-d", "1999-06-01", "+%_10m"])
.succeeds()
.stdout_is(" 6\n");
}
#[test]
fn test_date_format_modifier_no_pad() {
// Test no-pad flag: %-10Y suppresses all padding (width ignored)
new_ucmd!()
.env("TZ", "UTC")
.args(&["-d", "1999-06-01", "+%-10Y"])
.succeeds()
.stdout_is("1999\n");
// Test no-pad on day: %-d strips default zero padding
new_ucmd!()
.env("TZ", "UTC")
.args(&["-d", "1999-06-01", "+%-d"])
.succeeds()
.stdout_is("1\n");
}
#[test]
fn test_date_format_modifier_uppercase() {
// Test uppercase flag: %^B should uppercase month name
new_ucmd!()
.env("TZ", "UTC")
.env("LC_ALL", "C")
.args(&["-d", "1999-06-01", "+%^B"])
.succeeds()
.stdout_is("JUNE\n");
}
#[test]
fn test_date_format_modifier_force_sign() {
// Test force sign flag: %+6Y should show + sign for positive years
new_ucmd!()
.env("TZ", "UTC")
.args(&["-d", "1970-01-01", "+%+6Y"])
.succeeds()
.stdout_is("+01970\n");
}
#[test]
fn test_date_format_modifier_combined_flags() {
// Test combined flags: %-^10B should uppercase, no-pad suppresses all padding
new_ucmd!()
.env("TZ", "UTC")
.env("LC_ALL", "C")
.args(&["-d", "1999-06-01", "+%-^10B"])
.succeeds()
.stdout_is("JUNE\n");
}
#[test]
fn test_date_format_modifier_case_precedence() {
// Test that ^ (uppercase) takes precedence over # (swap case) regardless of order
new_ucmd!()
.env("TZ", "UTC")
.env("LC_ALL", "C")
.args(&["-d", "1999-06-01", "+%^#B"])
.succeeds()
.stdout_is("JUNE\n");
new_ucmd!()
.env("TZ", "UTC")
.env("LC_ALL", "C")
.args(&["-d", "1999-06-01", "+%#^B"])
.succeeds()
.stdout_is("JUNE\n");
}
#[test]
fn test_date_format_modifier_multiple() {
// Test multiple modifiers in one format string
// %-5d: no-pad suppresses all padding → "1"
new_ucmd!()
.env("TZ", "UTC")
.args(&["-d", "1999-06-01", "+%10Y-%_5m-%-5d"])
.succeeds()
.stdout_is("0000001999- 6-1\n");
}
#[test]
fn test_date_format_modifier_percent_escape() {
// Test that %% is preserved correctly with modifiers
new_ucmd!()
.env("TZ", "UTC")
.args(&["-d", "1999-06-01", "+%%Y=%10Y"])
.succeeds()
.stdout_is("%Y=0000001999\n");
}
// Tests for --debug flag
#[test]
fn test_date_debug_basic() {
// Test that --debug outputs to stderr, not stdout
let result = new_ucmd!()
.env("TZ", "UTC")
.args(&["--debug", "-d", "2005-01-01", "+%Y"])
.succeeds();
// Stdout should contain only the formatted date
assert_eq!(result.stdout_str().trim(), "2005");
// Stderr should contain debug information
let stderr = result.stderr_str();
assert!(stderr.contains("date: input string:"));
assert!(stderr.contains("date: parsed date part:"));
assert!(stderr.contains("date: parsed time part:"));
assert!(stderr.contains("date: input timezone:"));
}
#[test]
fn test_date_debug_various_formats() {
// Test debug mode with various date formats and expected output
let test_cases = [
// (input, format, expected_stdout_contains, expected_stderr_contains, stderr_not_contains, check_input_string)
(
"2005-01-01 +345 day",
"+%Y-%m-%d",
"2005-12-12",
"date: parsed date part: (Y-M-D) 2005-12-12",
"",
true,
),
(
"@0",
"+%Y-%m-%d",
"1970-01-01",
"date: parsed date part: (Y-M-D) 1970-01-01",
"warning: using midnight",
true,
),
(
"@-22",
"+%s",
"-22",
"date: parsed date part: (Y-M-D) 1969-12-31",
"",
true,
),
(
"2021-03-20 14:53:01 EST",
"+%Y-%m-%d",
"2021-03-20",
"date: parsed date part: (Y-M-D) 2021-03-20",
"",
true,
),
(
"m9",
"+%T",
"21:00:00",
"date: parsed time part:",
"",
false,
), // Military TZ is composed before parsing
(
" ",
"+%T",
"00:00:00",
"date: warning: using midnight",
"",
false,
), // Whitespace is composed
(
"1 day ago",
"+%Y-%m-%d",
"",
"date: parsed date part: (Y-M-D)",
"",
true,
),
];
for (
input,
format,
stdout_contains,
stderr_contains,
stderr_not_contains,
check_input_string,
) in test_cases
{
let result = new_ucmd!()
.env("TZ", "UTC")
.args(&["--debug", "-d", input, format])
.succeeds();
if !stdout_contains.is_empty() {
assert!(
result.stdout_str().contains(stdout_contains),
"For input '{input}': stdout should contain '{stdout_contains}', got: {}",
result.stdout_str()
);
}
let stderr = result.stderr_str();
assert!(
stderr.contains(stderr_contains),
"For input '{input}': stderr should contain '{stderr_contains}'"
);
if check_input_string {
assert!(
stderr.contains(&format!("date: input string: {input}")),
"For input '{input}': stderr should contain input string"
);
} else {
// Just check that there is some input string
assert!(
stderr.contains("date: input string:"),
"For input '{input}': stderr should contain some input string"
);
}
if !stderr_not_contains.is_empty() {
assert!(
!stderr.contains(stderr_not_contains),
"For input '{input}': stderr should not contain '{stderr_not_contains}'"
);
}
}
}
#[test]
fn test_date_debug_midnight_warnings() {
// Test midnight warning behavior with various inputs
let test_cases = [
// (input, format, should_warn)
("2005-01-01", "+%Y", true), // No time specified
("1997-01-19 08:17:48 +0", "+%Y-%m-%d", false), // Time specified
("@0", "+%Y-%m-%d", false), // Epoch format
(" ", "+%T", true), // Whitespace (defaults to midnight)
];
for (input, format, should_warn) in test_cases {
let result = new_ucmd!()
.env("TZ", "UTC")
.args(&["--debug", "-d", input, format])
.succeeds();
let stderr = result.stderr_str();
if should_warn {
assert!(
stderr.contains("date: warning: using midnight"),
"Input '{input}' should produce midnight warning"
);
} else {
assert!(
!stderr.contains("warning: using midnight"),
"Input '{input}' should not produce midnight warning"
);
}
}
}
#[test]
fn test_date_debug_without_flag() {
// Test that without --debug, no debug output appears
let result = new_ucmd!()
.env("TZ", "UTC")
.args(&["-d", "2005-01-01", "+%Y"])
.succeeds();
let stderr = result.stderr_str();
assert!(!stderr.contains("date: input string:"));
assert!(!stderr.contains("date: parsed date part:"));
}
#[test]
fn test_date_debug_with_multiple_inputs() {
// Test debug mode with file and stdin input (multiple dates)
let (at, mut ucmd) = at_and_ucmd!();
let file = "debug_test_file";
at.write(file, "2005-01-01\n2006-02-02\n");
let result = ucmd
.env("TZ", "UTC")
.args(&["--debug", "-f", file, "+%Y"])
.succeeds();
assert_eq!(result.stdout_str(), "2005\n2006\n");
let stderr = result.stderr_str();
// Should show debug output for both lines
assert!(stderr.contains("date: input string: 2005-01-01"));
assert!(stderr.contains("date: input string: 2006-02-02"));
assert!(stderr.contains("date: parsed date part: (Y-M-D) 2005-01-01"));
assert!(stderr.contains("date: parsed date part: (Y-M-D) 2006-02-02"));
// Test with stdin
let result = new_ucmd!()
.env("TZ", "UTC")
.args(&["--debug", "-f", "-", "+%Y"])
.pipe_in("2005-01-01\n2006-02-02\n")
.succeeds();
assert_eq!(result.stdout_str(), "2005\n2006\n");
let stderr = result.stderr_str();
assert!(stderr.contains("date: input string: 2005-01-01"));
assert!(stderr.contains("date: input string: 2006-02-02"));
}
#[test]
fn test_date_debug_with_flags() {
// Test debug mode combined with other flags and exit codes
let test_cases = [
// (args, should_succeed, stdout_contains, stderr_contains)
(
vec!["--debug", "-d", "2005-01-01", "+%Y"],
true,
"2005",
"date: input string:",
),
(
vec!["--debug", "-u", "-d", "2005-01-01", "+%Y-%m-%d %Z"],
true,
"UTC",
"date: parsed date part:",
),
(
vec!["--debug", "-R", "-d", "2005-01-01"],
true,
"Sat, 01 Jan 2005",
"date: input string:",
),
(
vec!["--debug", "-d", "invalid", "+%Y"],
false,
"",
"invalid date",
),
];
for (args, should_succeed, stdout_contains, stderr_contains) in test_cases {
let mut cmd = new_ucmd!();
cmd.env("TZ", "UTC").args(&args);
if should_succeed {
let result = cmd.succeeds();
assert!(
result.stdout_str().contains(stdout_contains),
"Args {args:?}: stdout should contain '{stdout_contains}'"
);
assert!(
result.stderr_str().contains(stderr_contains),
"Args {args:?}: stderr should contain '{stderr_contains}'"
);
} else {
let result = cmd.fails();
assert!(
result.stderr_str().contains(stderr_contains),
"Args {args:?}: stderr should contain '{stderr_contains}'"
);
}
}
}
#[test]
fn test_date_debug_current_time() {
// Test that debug mode without -d doesn't produce debug output (no parsing)
let result = new_ucmd!()
.env("TZ", "UTC")
.args(&["--debug", "+%Y"])
.succeeds();
let stderr = result.stderr_str();
// No parsing happens for "now", so no debug output
assert_eq!(stderr, "");
}