mirror of
https://github.com/uutils/coreutils.git
synced 2026-06-10 15:48:22 -07:00
date: fix subfmt-up1, fill-1, pct-pct, and invalid-high-bit-set tests (#10940)
This commit is contained in:
Generated
+1
@@ -3348,6 +3348,7 @@ dependencies = [
|
||||
"jiff-icu",
|
||||
"nix",
|
||||
"parse_datetime",
|
||||
"regex",
|
||||
"tempfile",
|
||||
"uucore",
|
||||
"windows-sys 0.61.2",
|
||||
|
||||
Generated
+33
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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
@@ -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
@@ -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, "");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user