Skip to content

Commit f1c95e1

Browse files
committed
test(date): use parse_date for test helper timestamp
Replace manual jiff::civil::date construction with parse_date in expand_format_with_test_date helper to ensure proper timestamp initialization for strtime formatting. Fixes uutils#9654
1 parent 502f3b1 commit f1c95e1

File tree

3 files changed

+109
-34
lines changed

3 files changed

+109
-34
lines changed

.vscode/cspell.dictionaries/jargon.wordlist.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ shortcodes
136136
siginfo
137137
sigusr
138138
strcasecmp
139+
strtime
139140
subcommand
140141
subexpression
141142
submodule

src/uu/comm/src/comm.rs

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,12 @@ struct OrderChecker {
5454
has_error: bool,
5555
}
5656

57-
enum Input {
58-
Stdin(StdinLock<'static>),
57+
enum Input<'a> {
58+
Stdin(StdinLock<'a>),
5959
FileIn(BufReader<File>),
6060
}
6161

62-
impl Input {
62+
impl Input<'_> {
6363
fn stdin() -> Self {
6464
Self::Stdin(stdin().lock())
6565
}
@@ -69,13 +69,13 @@ impl Input {
6969
}
7070
}
7171

72-
struct LineReader {
72+
struct LineReader<'a> {
7373
line_ending: LineEnding,
74-
input: Input,
74+
input: Input<'a>,
7575
}
7676

77-
impl LineReader {
78-
fn new(input: Input, line_ending: LineEnding) -> Self {
77+
impl<'a> LineReader<'a> {
78+
fn new(input: Input<'a>, line_ending: LineEnding) -> Self {
7979
Self { line_ending, input }
8080
}
8181

@@ -178,7 +178,12 @@ pub fn are_files_identical(path1: &Path, path2: &Path) -> io::Result<bool> {
178178
}
179179
}
180180

181-
fn comm(a: &mut LineReader, b: &mut LineReader, delim: &str, opts: &ArgMatches) -> UResult<()> {
181+
fn comm(
182+
a: &mut LineReader<'_>,
183+
b: &mut LineReader<'_>,
184+
delim: &str,
185+
opts: &ArgMatches,
186+
) -> UResult<()> {
182187
let width_col_1 = usize::from(!opts.get_flag(options::COLUMN_1));
183188
let width_col_2 = usize::from(!opts.get_flag(options::COLUMN_2));
184189

@@ -291,7 +296,7 @@ fn comm(a: &mut LineReader, b: &mut LineReader, delim: &str, opts: &ArgMatches)
291296
}
292297
}
293298

294-
fn open_file(name: &OsString, line_ending: LineEnding) -> io::Result<LineReader> {
299+
fn open_file(name: &OsString, line_ending: LineEnding) -> io::Result<LineReader<'static>> {
295300
if name == "-" {
296301
Ok(LineReader::new(Input::stdin(), line_ending))
297302
} else {

src/uu/date/src/locale.rs

Lines changed: 94 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,37 @@ cfg_langinfo! {
4444
DEFAULT_FORMAT_CACHE.get_or_init(|| {
4545
// Try to get locale format string
4646
if let Some(format) = get_locale_format_string() {
47-
let format_with_tz = ensure_timezone_in_format(&format);
48-
return Box::leak(format_with_tz.into_boxed_str());
47+
// Validate that the format is complete (contains date and time components)
48+
// On some platforms (e.g., macOS ARM64), nl_langinfo may return minimal formats
49+
if is_format_complete(&format) {
50+
let format_with_tz = ensure_timezone_in_format(&format);
51+
return Box::leak(format_with_tz.into_boxed_str());
52+
}
4953
}
5054

5155
// Fallback: use 24-hour format as safe default
5256
"%a %b %e %X %Z %Y"
5357
})
5458
}
5559

60+
/// Checks if a format string contains both date and time components.
61+
/// Returns false for minimal formats that would produce incomplete output.
62+
fn is_format_complete(format: &str) -> bool {
63+
// Check for date components (at least one of: day, month, or year)
64+
let has_date = format.contains("%d") || format.contains("%e")
65+
|| format.contains("%b") || format.contains("%B")
66+
|| format.contains("%m")
67+
|| format.contains("%Y") || format.contains("%y");
68+
69+
// Check for time components (at least one of: hour, minute, or time format)
70+
let has_time = format.contains("%H") || format.contains("%I")
71+
|| format.contains("%M")
72+
|| format.contains("%T") || format.contains("%X")
73+
|| format.contains("%R");
74+
75+
has_date && has_time
76+
}
77+
5678
/// Retrieves the date/time format string from the system locale
5779
fn get_locale_format_string() -> Option<String> {
5880
unsafe {
@@ -112,6 +134,23 @@ mod tests {
112134
cfg_langinfo! {
113135
use super::*;
114136

137+
/// Helper function to expand a format string with a known test date
138+
///
139+
/// Uses a fixed test date: Monday, January 15, 2024, 14:30:45 UTC
140+
/// This allows us to validate format strings by checking their expanded output
141+
/// rather than looking for literal format codes.
142+
fn expand_format_with_test_date(format: &str) -> String {
143+
use jiff::fmt::strtime;
144+
145+
// Create test timestamp: Monday, January 15, 2024, 14:30:45 UTC
146+
// Use parse_date to get a proper Zoned timestamp (same as production code)
147+
let test_date = crate::parse_date("2024-01-15 14:30:45 UTC")
148+
.expect("Test date parse should never fail");
149+
150+
// Expand the format string with the test date
151+
strtime::format(format, &test_date).unwrap_or_default()
152+
}
153+
115154
#[test]
116155
fn test_locale_detection() {
117156
// Just verify the function doesn't panic
@@ -121,10 +160,31 @@ mod tests {
121160
#[test]
122161
fn test_default_format_contains_valid_codes() {
123162
let format = get_locale_default_format();
124-
assert!(format.contains("%a")); // abbreviated weekday
125-
assert!(format.contains("%b")); // abbreviated month
126-
assert!(format.contains("%Y") || format.contains("%y")); // year (4-digit or 2-digit)
127-
assert!(format.contains("%Z")); // timezone
163+
164+
let expanded = expand_format_with_test_date(format);
165+
166+
// Verify expanded output contains expected components
167+
// Test date: Monday, January 15, 2024, 14:30:45
168+
assert!(
169+
expanded.contains("Mon") || expanded.contains("Monday"),
170+
"Expanded format should contain weekday name, got: {expanded}"
171+
);
172+
173+
assert!(
174+
expanded.contains("Jan") || expanded.contains("January"),
175+
"Expanded format should contain month name, got: {expanded}"
176+
);
177+
178+
assert!(
179+
expanded.contains("2024") || expanded.contains("24"),
180+
"Expanded format should contain year, got: {expanded}"
181+
);
182+
183+
// Keep literal %Z check - this is enforced by ensure_timezone_in_format()
184+
assert!(
185+
format.contains("%Z"),
186+
"Format string must contain %Z timezone (enforced by ensure_timezone_in_format)"
187+
);
128188
}
129189

130190
#[test]
@@ -135,25 +195,34 @@ mod tests {
135195
// The format should not be empty
136196
assert!(!format.is_empty(), "Locale format should not be empty");
137197

138-
// Should contain date/time components
139-
let has_date_component = format.contains("%a")
140-
|| format.contains("%A")
141-
|| format.contains("%b")
142-
|| format.contains("%B")
143-
|| format.contains("%d")
144-
|| format.contains("%e");
145-
assert!(has_date_component, "Format should contain date components");
146-
147-
// Should contain time component (hour)
148-
let has_time_component = format.contains("%H")
149-
|| format.contains("%I")
150-
|| format.contains("%k")
151-
|| format.contains("%l")
152-
|| format.contains("%r")
153-
|| format.contains("%R")
154-
|| format.contains("%T")
155-
|| format.contains("%X");
156-
assert!(has_time_component, "Format should contain time components");
198+
let expanded = expand_format_with_test_date(format);
199+
200+
// Verify expanded output contains date components
201+
// Test date: Monday, January 15, 2024
202+
let has_date_component = expanded.contains("15") // day
203+
|| expanded.contains("Jan") // month name
204+
|| expanded.contains("January") // full month
205+
|| expanded.contains("Mon") // weekday
206+
|| expanded.contains("Monday"); // full weekday
207+
208+
assert!(
209+
has_date_component,
210+
"Expanded format should contain date components, got: {expanded}"
211+
);
212+
213+
// Verify expanded output contains time components
214+
// Test time: 14:30:45
215+
let has_time_component = expanded.contains("14") // 24-hour
216+
|| expanded.contains("02") // 12-hour
217+
|| expanded.contains("30") // minutes
218+
|| expanded.contains(':') // time separator
219+
|| expanded.contains("PM") // AM/PM indicator
220+
|| expanded.contains("pm");
221+
222+
assert!(
223+
has_time_component,
224+
"Expanded format should contain time components, got: {expanded}"
225+
);
157226
}
158227

159228
#[test]

0 commit comments

Comments
 (0)