zsh/ported/modules/datetime.rs
1//! Date/time utilities — port of `Src/Modules/datetime.c`.
2//!
3//! C source has 0 structs/enums. Rust port matches: 0 types.
4//! Functions:
5//! - `getcurrentsecs` `[c:206]`
6//! - `getcurrentrealtime` `[c:212]`
7//! - `getcurrenttime` `[c:220]`
8//! - `reverse_strftime` `[c:42]`
9//! - `output_strftime` `[c:99]` (the actual builtin entry)
10//! - `bin_strftime` `[c:187]` (TZ-scope wrapper around output_strftime)
11//! - 6 module loaders
12//!
13//! C uses libc `localtime(3)` + zsh's custom `ztrftime()` (which
14//! extends POSIX strftime with the `%.N` nanosecond syntax). The
15//! Rust port calls `crate::ported::utils::ztrftime()` for the
16//! base format and adds %N extensions on top.
17
18use crate::ported::compat::zgettime;
19use crate::ported::params::{getsparam, isident, setiparam, setsparam};
20use crate::ported::utils::{metafy, zwarnnam};
21use crate::ported::zsh_h::{features, module, options, MAX_OPS, OPT_ARG, OPT_ISSET};
22use crate::ported::zsh_system_h::timespec;
23use chrono::format::{Parsed, StrftimeItems};
24use chrono::{DateTime, Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone};
25use std::sync::{Mutex, OnceLock};
26use std::time::{Duration, SystemTime, UNIX_EPOCH};
27
28/// Port of `reverse_strftime(char *nam, char **argv, char *scalar, int quiet)` from `Src/Modules/datetime.c:42`.
29/// Parses a time string per the format string and assigns the
30/// resulting epoch seconds to `scalar` (or stdout if NULL).
31///
32/// C signature: `static int reverse_strftime(char *nam, char **argv,
33/// char *scalar, int quiet)`.
34/// WARNING: param names don't match C — Rust=(nam, argv, quiet) vs C=(nam, argv, scalar, quiet)
35pub fn reverse_strftime(
36 nam: &str,
37 argv: &[&str], // c:42
38 scalar: Option<&str>,
39 quiet: i32,
40) -> i32 {
41 if argv.len() < 2 {
42 // c:54 timestring expected
43 zwarnnam(nam, "timestring expected");
44 return 1;
45 }
46 let format = argv[0];
47 let input = argv[1];
48 // c:64 — `strptime(timestring, format, &tm)`. C's strptime
49 // accepts PARTIAL formats: `%Y` + `"2024"` parses just the
50 // year and fills the rest of struct tm with zeros (which
51 // mktime then resolves to 2024-01-01 00:00:00). chrono's
52 // `NaiveDateTime::parse_from_str` REQUIRES every field
53 // (year, month, day, hour, minute, second) and fails on
54 // partial input, so route through `Parsed` which holds
55 // any subset of fields then fill missing pieces with
56 // defaults to mirror strptime + mktime semantics. Bug #324.
57 // c:62 — `endp = strptime(argv[1], argv[0], &tm);` — strptime returns
58 // the FIRST unconsumed character (NUL if entire string was consumed).
59 // chrono's `parse_and_remainder` returns the remainder slice on
60 // success, which is the equivalent of `endp`.
61 let mut parsed = Parsed::new();
62 let remainder =
63 match chrono::format::parse_and_remainder(&mut parsed, input, StrftimeItems::new(format)) {
64 Ok(rem) => rem,
65 Err(_) => {
66 // c:64-69 — `if (!endp) { if (!quiet) zwarnnam(nam,
67 // 'format not matched'); return 1; }`
68 // C emits the bare 'format not matched' string with no
69 // input echo; prior Rust port appended ': {input}' which
70 // diverged from zsh -fc parity.
71 if quiet == 0 {
72 zwarnnam(nam, "format not matched"); // c:67
73 }
74 return 1; // c:68
75 }
76 };
77 // c:59-61 — `memset(&tm, 0, sizeof(tm)); tm.tm_isdst = -1; tm.tm_mday = 1;`
78 //
79 // C zero-fills the struct tm and then sets tm_isdst=-1 + tm_mday=1.
80 // tm.tm_year IS LEFT AT 0, which means "year since 1900" → 1900.
81 // mktime() on a 0-year tm with a parsed time-of-day produces an
82 // epoch around 1900-01-01 (a large negative number on 64-bit
83 // systems).
84 //
85 // Prior Rust port defaulted to 1970 — convenient (positive epoch)
86 // but divergent from C. `strftime -r '%H:%M' '14:30'`:
87 // - C zsh: -2208988800 + 14*3600 + 30*60 = -2208938400
88 // - Prior Rust: 50400 (1970-01-01 14:30:00)
89 // For a user pipelining the result to e.g. another strftime call,
90 // the year difference matters.
91 let year = parsed
92 .year
93 .or_else(|| {
94 parsed
95 .year_div_100
96 .zip(parsed.year_mod_100)
97 .map(|(d, m)| d * 100 + m)
98 })
99 .unwrap_or(1900); // c:59 — tm_year=0 means 1900.
100 let month = parsed.month.unwrap_or(1);
101 let day = parsed.day.unwrap_or(1);
102 let hour = parsed
103 .hour_div_12
104 .zip(parsed.hour_mod_12)
105 .map(|(d, m)| (d * 12 + m) as u32)
106 .unwrap_or(0);
107 let minute = parsed.minute.unwrap_or(0);
108 let second = parsed.second.unwrap_or(0);
109 let date = match NaiveDate::from_ymd_opt(year, month, day) {
110 Some(d) => d,
111 None => {
112 // c:67 — same bare 'format not matched' for out-of-range
113 // date components (e.g. month 13). C's strptime → mktime
114 // chain doesn't distinguish parse-mismatch from invalid-
115 // date because mktime normalises any out-of-range tm
116 // fields rather than failing; chrono's from_ymd_opt is
117 // stricter, so we emit the same diagnostic.
118 if quiet == 0 {
119 zwarnnam(nam, "format not matched");
120 }
121 return 1;
122 }
123 };
124 let time = NaiveTime::from_hms_opt(hour, minute, second)
125 .unwrap_or_else(|| NaiveTime::from_hms_opt(0, 0, 0).unwrap());
126 let dt = NaiveDateTime::new(date, time);
127 // c:71 — `mytime = (zlong)mktime(&tm);`
128 // C uses `tm.tm_isdst = -1` (set at c:60) to ask mktime to
129 // auto-detect DST. For ambiguous times (the fall-back hour that
130 // exists twice — e.g. 1:30 AM on a US fall-back day), mktime
131 // with tm_isdst=-1 returns the LATER (non-DST) variant; the
132 // standard rationale is "after a fall-back, the same wall-clock
133 // time appears twice; the second occurrence (standard time)
134 // wins" matching how `date` and most tools resolve.
135 //
136 // chrono's `from_local_datetime` returns `Ambiguous(earliest,
137 // latest)` for these times. Prior Rust picked .earliest (the
138 // first variant arg) — the DST-active occurrence, which is the
139 // OPPOSITE of C mktime's resolution. Real-world repro:
140 // $ TZ=America/New_York strftime -r '%Y-%m-%d %H:%M:%S' '2024-11-03 01:30:00'
141 // # C: emits the second occurrence (EST, after fall-back).
142 // # Prior Rust: emits the first occurrence (EDT, pre-fall-back).
143 // # Difference: one hour off — 3600 seconds.
144 // Pick `.latest()` for fall-back-ambiguous matching mktime.
145 let secs = match Local.from_local_datetime(&dt) {
146 // c:71 mktime
147 chrono::LocalResult::Single(d) => d.timestamp(),
148 chrono::LocalResult::Ambiguous(_, latest) => latest.timestamp(),
149 chrono::LocalResult::None => {
150 if quiet == 0 {
151 zwarnnam(nam, "unable to convert to time");
152 }
153 return 1;
154 }
155 };
156 if let Some(name) = scalar {
157 // c:73-74 — `if (scalar) setiparam(scalar, mytime);`
158 setiparam(name, secs); // c:74 setiparam
159 } else {
160 // c:75-79 — print as decimal.
161 println!("{}", secs); // c:78 printf("%ld\n", ...)
162 }
163 // c:81-88 — `if (*endp && !quiet) zwarnnam(nam,
164 // "warning: input string not completely matched");`
165 // strptime can succeed yet leave trailing input unconsumed; the
166 // format was satisfied but more bytes remained. C emits a soft
167 // warning (still returns 0) so the user can spot accidental
168 // truncation. Prior Rust port didn't have this — silent
169 // partial-parse meant scripts feeding malformed input through
170 // `strftime -r '%Y-%m-%d' 'extra2024-01-15'` got `secs=0` (the
171 // post-strptime defaults applied) without diagnostic.
172 if !remainder.is_empty() && quiet == 0 {
173 zwarnnam(nam, "warning: input string not completely matched"); // c:87
174 }
175 0 // c:90
176}
177
178/// Port of `output_strftime(char *nam, char **argv, Options ops, UNUSED(int func))` from `Src/Modules/datetime.c:99`.
179/// The `output_strftime` builtin entry. Parses argv (format,
180/// timestamp, nanoseconds), calls `localtime(3)` to convert,
181/// formats via `ztrftime()` with retry-on-overflow, then writes
182/// the result to stdout (or `setsparam` to the `-s NAME` scalar).
183///
184/// C signature: `static int output_strftime(char *nam, char **argv,
185/// Options ops, int func)`.
186/// WARNING: param names don't match C — Rust=(nam, argv, _func) vs C=(nam, argv, ops, func)
187pub fn output_strftime(
188 nam: &str,
189 argv: &[&str], // c:99
190 ops: &options,
191 _func: i32,
192) -> i32 {
193 // c:107 — `if (OPT_ISSET(ops,'s'))`
194 let scalar: Option<&str> = if OPT_ISSET(ops, b's') {
195 Some(OPT_ARG(ops, b's').unwrap_or(""))
196 } else {
197 None
198 };
199 if let Some(name) = scalar {
200 if !isident(name) {
201 // c:110 isident check
202 zwarnnam(nam, &format!("not an identifier: {}", name)); // c:111
203 return 1; // c:112
204 }
205 }
206
207 // c:113-119 — `if (OPT_ISSET(ops, 'r'))` reverse path.
208 // C body:
209 // if (OPT_ISSET(ops, 'r')) {
210 // if (!argv[1]) {
211 // zwarnnam(nam, "timestring expected");
212 // return 1;
213 // }
214 // return reverse_strftime(nam, argv, scalar,
215 // OPT_ISSET(ops, 'q'));
216 // }
217 //
218 // Prior port skipped the !argv[1] guard so `strftime -r %s`
219 // (format with no timestring) fell through to reverse_strftime
220 // which would either crash or emit a non-canonical diagnostic.
221 if OPT_ISSET(ops, b'r') {
222 if argv.len() < 2 {
223 // c:114-117 — `if (!argv[1])` then 'timestring expected'.
224 zwarnnam(nam, "timestring expected"); // c:115
225 return 1; // c:116
226 }
227 let quiet = if OPT_ISSET(ops, b'q') { 1 } else { 0 };
228 return reverse_strftime(nam, argv, scalar, quiet); // c:118
229 }
230
231 if argv.is_empty() {
232 zwarnnam(nam, "format expected");
233 return 1;
234 }
235
236 // c:122 — parse argv[1] as timestamp, or use current time.
237 let (secs, nsec) = if argv.len() < 2 {
238 let now = SystemTime::now()
239 .duration_since(UNIX_EPOCH)
240 .unwrap_or(Duration::ZERO);
241 (now.as_secs() as i64, now.subsec_nanos() as i64) // c:124-125 zgettime
242 } else {
243 // c:128 — `ts.tv_sec = (time_t)strtoul(argv[1], &endptr, 10);`
244 let secs = match argv[1].parse::<i64>() {
245 Ok(v) => v,
246 Err(_) => {
247 zwarnnam(nam, &format!("{}: invalid decimal number", argv[1]));
248 return 1; // c:135
249 }
250 };
251 // c:144 — argv[2] nanoseconds (optional).
252 let nsec = if argv.len() > 2 {
253 match argv[2].parse::<i64>() {
254 Ok(v) if (0..=999_999_999).contains(&v) => v, // c:151
255 Ok(_) => {
256 zwarnnam(nam, &format!("{}: invalid nanosecond value", argv[2]));
257 return 1; // c:153
258 }
259 Err(_) => {
260 zwarnnam(nam, &format!("{}: invalid decimal number", argv[2]));
261 return 1;
262 }
263 }
264 } else {
265 0
266 };
267 (secs, nsec)
268 };
269
270 // c:136-140 — `tm = localtime(&ts.tv_sec); if (!tm) {
271 // zwarnnam(nam, "%s: unable to convert to time", ...);
272 // return 1; }`
273 let format = argv[0];
274 #[cfg(unix)]
275 {
276 let secs_c: libc::time_t = secs as libc::time_t;
277 if unsafe { libc::localtime(&secs_c) }.is_null() {
278 // c:137-139
279 let what = argv.get(1).copied().unwrap_or("");
280 zwarnnam(nam, &format!("{}: unable to convert to time", what));
281 return 1;
282 }
283 }
284 // c:158-167 — `bufsize = strlen(argv[0]) * 8; buffer = zalloc(bufsize);
285 // for (x=0; x < 4; x++) {
286 // if ((len = ztrftime(buffer, bufsize, argv[0], tm,
287 // ts.tv_nsec)) >= 0 || x==3) break;
288 // buffer = zrealloc(buffer, bufsize *= 2); }`
289 // Route through the canonical ztrftime port (utils.rs:4231) — it
290 // implements zsh's strftime extensions per Src/utils.c ztrftime:
291 // %. (fractional seconds, optional digit-count prefix), %f, %e,
292 // %K/%k, %L/%l. The prior chrono-based path missed all of those
293 // AND invented a `%N` substitution that exists in neither zsh's
294 // ztrftime specifier set nor POSIX strftime (zsh passes %N through
295 // to the system strftime, which emits it literally). The bufsize
296 // retry loop is C buffer management; the port returns an owned
297 // String. Nanoseconds travel inside the SystemTime (ztrftime reads
298 // subsec_nanos for the %. specifier, mirroring the C `usec` arg).
299 let st = if secs >= 0 {
300 UNIX_EPOCH + Duration::new(secs as u64, nsec as u32)
301 } else {
302 // pre-epoch: subtract whole seconds, then add the positive
303 // nanosecond part back (tm convention: nsec ∈ [0, 1e9)).
304 UNIX_EPOCH - Duration::from_secs(secs.unsigned_abs()) + Duration::from_nanos(nsec as u64)
305 };
306 let formatted = crate::ported::utils::ztrftime(format, st, false); // c:163
307
308 // c:178 — `if (scalar) { setsparam(scalar, metafy(buffer, len, META_DUP)); }`
309 if let Some(name) = scalar {
310 setsparam(name, &metafy(&formatted));
311 // c:178
312 } else {
313 // c:180-183 — fwrite + putchar('\n') unless -n
314 print!("{}", formatted); // c:181 fwrite
315 if !OPT_ISSET(ops, b'n') {
316 // c:182 !OPT_ISSET(ops,'n')
317 println!(); // c:183 putchar('\n')
318 }
319 }
320
321 0 // c:187
322}
323
324/// Port of `bin_strftime(char *nam, char **argv, Options ops, int func)` from `Src/Modules/datetime.c:187`. The
325/// `strftime` builtin entry — wraps `output_strftime` in a local
326/// param-scope that copies `$TZ` so `output_strftime`'s
327/// `localtime(3)` calls see the user's timezone even if a function
328/// scope has shadowed it.
329///
330/// C signature: `static int bin_strftime(char *nam, char **argv,
331/// Options ops, int func)`.
332/// WARNING: param names don't match C — Rust=(nam, argv, func) vs C=(nam, argv, ops, func)
333pub fn bin_strftime(
334 nam: &str,
335 argv: &[String], // c:187
336 ops: &options,
337 func: i32,
338) -> i32 {
339 // c:190-191 — `int result = 1; char *tz = getsparam("TZ");`
340 let tz_saved = getsparam("TZ"); // c:190
341 // c:192-198 — `startparamscope();
342 // if (tz) {
343 // Param pm = createparam("TZ", PM_LOCAL|...);
344 // if (pm) pm->level = locallevel;
345 // setsparam("TZ", ztrdup(tz));
346 // }`
347 //
348 // C's PM_LOCAL gives the TZ override function-scope so
349 // endparamscope at c:200 automatically restores the prior TZ.
350 // Rust port pushes via env::set_var so libc's strftime sees
351 // the new zone — paramtab setsparam alone doesn't propagate
352 // to the libc-level zone (libc reads $TZ from getenv at
353 // strftime time, not from zsh's paramtab).
354 //
355 // Capture the ORIGINAL env::TZ here so we can restore it on
356 // exit. Prior port re-set env::TZ to the same value on exit
357 // instead of restoring — leaving a stale env::TZ override
358 // after bin_strftime returned. Real-world: user has env::TZ
359 // = "UTC" but $TZ paramtab = "America/Chicago"; after
360 // strftime returns, env::TZ silently became "America/Chicago"
361 // and stayed there for the rest of the session.
362 let env_tz_saved = std::env::var_os("TZ"); // capture before overwrite
363 if let Some(ref tz) = tz_saved {
364 std::env::set_var("TZ", tz); // c:197 setsparam
365 }
366 // Convert &[String] → &[&str] for the internal helper which
367 // takes the narrower view.
368 let argv_views: Vec<&str> = argv.iter().map(String::as_str).collect();
369 let result = output_strftime(nam, &argv_views, ops, func); // c:199
370 // c:200 — `endparamscope();` — restore prior env::TZ.
371 match env_tz_saved {
372 Some(prev) => std::env::set_var("TZ", prev),
373 None => std::env::remove_var("TZ"),
374 }
375 result // c:202
376}
377
378/// Port of `getcurrentsecs(UNUSED(Param pm))` from `Src/Modules/datetime.c:206`.
379/// Returns the current epoch seconds — backs `$EPOCHSECONDS`.
380/// C body: `return (zlong) time(NULL);`
381/// WARNING: param names don't match C — Rust=() vs C=(pm)
382pub fn getcurrentsecs() -> i64 {
383 // c:206
384 // c:206 — `return (zlong) time(NULL);`
385 unsafe { libc::time(std::ptr::null_mut()) as i64 }
386}
387
388/// Port of `getcurrentrealtime(UNUSED(Param pm))` from `Src/Modules/datetime.c:212`.
389/// Returns the current high-resolution epoch time as f64 — backs
390/// `$EPOCHREALTIME`.
391///
392/// C body:
393/// ```c
394/// struct timespec now;
395/// zgettime(&now);
396/// return (double)now.tv_sec + (double)now.tv_nsec * 1e-9;
397/// ```
398/// WARNING: param names don't match C — Rust=() vs C=(pm)
399pub fn getcurrentrealtime() -> f64 {
400 // c:212
401 let mut now: timespec = unsafe { std::mem::zeroed() }; // c:212
402 zgettime(&mut now); // c:215
403 (now.tv_sec as f64) + (now.tv_nsec as f64) * 1e-9 // c:216
404}
405
406/// Port of `getcurrenttime(UNUSED(Param pm))` from `Src/Modules/datetime.c:220`.
407/// Returns the current epoch as `(secs, nanos)` — backs the
408/// `$epochtime` two-element array param.
409///
410/// C body:
411/// ```c
412/// struct timespec now;
413/// zgettime(&now);
414/// arr[0] = sprintf "%ld" now.tv_sec
415/// arr[1] = sprintf "%ld" now.tv_nsec
416/// return arr;
417/// ```
418/// WARNING: param names don't match C — Rust=() vs C=(pm)
419pub fn getcurrenttime() -> Vec<String> {
420 // c:220
421 // c:222-224 — `char **arr; char buf[DIGBUFSIZE]; struct timespec now;`
422 let mut arr: Vec<String> = Vec::with_capacity(2); // c:228 zhalloc(3 * sizeof(*arr))
423 let mut now: timespec = unsafe { std::mem::zeroed() }; // c:224
424 // c:226 — `zgettime(&now);`
425 zgettime(&mut now);
426 // c:229 — `sprintf(buf, "%ld", (long)now.tv_sec);`
427 let buf = format!("{}", now.tv_sec as i64);
428 arr.push(buf); // c:230 arr[0] = dupstring(buf)
429 // c:231 — `sprintf(buf, "%ld", (long)now.tv_nsec);`
430 let buf = format!("{}", now.tv_nsec as i64);
431 arr.push(buf); // c:232 arr[1] = dupstring(buf)
432 // c:233 — `arr[2] = NULL;` (collapsed: Vec's length is the terminator)
433 arr // c:235
434}
435
436// `bintab` — port of `static struct builtin bintab[]` (datetime.c:255).
437
438// `patab` — port of `static struct paramdef patab[]` (datetime.c).
439
440// `module_features` — port of `static struct features module_features`
441// from datetime.c:262.
442
443/// Port of `setup_(UNUSED(Module m))` from `Src/Modules/datetime.c:270`.
444#[allow(unused_variables)]
445pub fn setup_(m: *const module) -> i32 {
446 // c:270
447 // C body c:272-273 — `return 0`. Faithful empty-body port.
448 0
449}
450
451// =====================================================================
452// static struct builtin bintab[] c:255
453// static struct features module_features c:262
454// =====================================================================
455
456/// Port of `features_(UNUSED(Module m), UNUSED(char ***features))` from `Src/Modules/datetime.c:277`.
457/// C body: `*features = featuresarray(m, &module_features); return 0;`
458pub fn features_(m: *const module, features: &mut Vec<String>) -> i32 {
459 // c:277
460 *features = featuresarray(m, module_features());
461 0 // c:292
462}
463
464/// Port of `enables_(UNUSED(Module m), UNUSED(int **enables))` from `Src/Modules/datetime.c:285`.
465/// C body: `return handlefeatures(m, &module_features, enables);`
466pub fn enables_(m: *const module, enables: &mut Option<Vec<i32>>) -> i32 {
467 // c:285
468 handlefeatures(m, module_features(), enables) // c:292
469}
470
471/// Port of `boot_(UNUSED(Module m))` from `Src/Modules/datetime.c:292`.
472#[allow(unused_variables)]
473pub fn boot_(m: *const module) -> i32 {
474 // c:292
475 // C body c:294-295 — `return 0` because the param registration
476 // happens via the `pd_list` feature descriptor at
477 // `Src/Modules/datetime.c:25-30`:
478 // { "EPOCHSECONDS", PM_INTEGER|PM_READONLY|PM_HIDE|PM_HIDEVAL|PM_SPECIAL, ... },
479 // { "EPOCHREALTIME", PM_FFLOAT|PM_READONLY|PM_HIDE|PM_HIDEVAL|PM_SPECIAL, ... },
480 // { "epochtime", PM_ARRAY|PM_READONLY|PM_HIDE|PM_HIDEVAL|PM_SPECIAL, ... }
481 // zshrs's simplified module framework doesn't drive that
482 // feature dispatch into paramtab, so `${(t)EPOCHSECONDS}`
483 // returned `scalar` (the default for an unregistered name
484 // that resolves via lookup_special_var). Register the entries
485 // here so the canonical introspection paths see the right
486 // PM_* flag set. Bug #512.
487 use crate::ported::params::{createparam, paramtab};
488 use crate::ported::zsh_h::{
489 PM_ARRAY, PM_FFLOAT, PM_HIDE, PM_HIDEVAL, PM_INTEGER, PM_READONLY, PM_SPECIAL,
490 };
491 let entries: &[(&str, u32)] = &[
492 (
493 "EPOCHSECONDS",
494 PM_INTEGER | PM_READONLY | PM_HIDE | PM_HIDEVAL | PM_SPECIAL,
495 ),
496 (
497 "EPOCHREALTIME",
498 PM_FFLOAT | PM_READONLY | PM_HIDE | PM_HIDEVAL | PM_SPECIAL,
499 ),
500 (
501 "epochtime",
502 PM_ARRAY | PM_READONLY | PM_HIDE | PM_HIDEVAL | PM_SPECIAL,
503 ),
504 ];
505 for (name, flags) in entries {
506 let exists = paramtab()
507 .read()
508 .ok()
509 .map(|t| t.contains_key(*name))
510 .unwrap_or(false);
511 if !exists {
512 let _ = createparam(name, *flags as i32);
513 }
514 }
515 0
516}
517
518/// Port of `cleanup_(UNUSED(Module m))` from `Src/Modules/datetime.c:299`.
519/// C body: `return setfeatureenables(m, &module_features, NULL);`
520pub fn cleanup_(m: *const module) -> i32 {
521 // c:299
522 setfeatureenables(m, module_features(), None) // c:306
523}
524
525/// Port of `finish_(UNUSED(Module m))` from `Src/Modules/datetime.c:306`.
526#[allow(unused_variables)]
527pub fn finish_(m: *const module) -> i32 {
528 // c:306
529 // C body c:308-309 — `return 0`. Faithful empty-body port; the
530 // strftime builtin + EPOCHREALTIME unregister
531 // via cleanup_'s setfeatureenables(...).
532 0
533}
534
535static MODULE_FEATURES: OnceLock<Mutex<features>> = OnceLock::new();
536
537// Local stubs for the per-module entry points. C uses generic
538// `featuresarray`/`handlefeatures`/`setfeatureenables` (module.c:
539// 3275/3370/3445) but those take `Builtin` + `Features` pointer
540// fields the Rust port doesn't carry. The hardcoded descriptor
541// list mirrors the C bintab/conddefs/mathfuncs/paramdefs.
542// WARNING: NOT IN DATETIME.C — Rust-only module-framework shim.
543// C uses generic featuresarray/handlefeatures/setfeatureenables from
544// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
545// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
546fn featuresarray(_m: *const module, _f: &Mutex<features>) -> Vec<String> {
547 vec![
548 "b:strftime".to_string(),
549 "p:EPOCHSECONDS".to_string(),
550 "p:EPOCHREALTIME".to_string(),
551 "p:epochtime".to_string(),
552 ]
553}
554
555// WARNING: NOT IN DATETIME.C — Rust-only module-framework shim.
556// C uses generic featuresarray/handlefeatures/setfeatureenables from
557// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
558// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
559fn handlefeatures(_m: *const module, _f: &Mutex<features>, enables: &mut Option<Vec<i32>>) -> i32 {
560 if enables.is_none() {
561 *enables = Some(vec![1; 4]);
562 }
563 0
564}
565
566// WARNING: NOT IN DATETIME.C — Rust-only module-framework shim.
567// C uses generic featuresarray/handlefeatures/setfeatureenables from
568// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
569// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
570fn setfeatureenables(_m: *const module, _f: &Mutex<features>, _e: Option<&[i32]>) -> i32 {
571 0
572}
573
574// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
575// ─── RUST-ONLY ACCESSORS ───
576//
577// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
578// RwLock<T>>` globals declared above. C zsh uses direct global
579// access; Rust needs these wrappers because `OnceLock::get_or_init`
580// is the only way to lazily construct shared state. These ported sit
581// here so the body of this file reads in C source order without
582// the accessor wrappers interleaved between real port ported.
583// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
584
585// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
586// ─── RUST-ONLY ACCESSORS ───
587//
588// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
589// RwLock<T>>` globals declared above. C zsh uses direct global
590// access; Rust needs these wrappers because `OnceLock::get_or_init`
591// is the only way to lazily construct shared state. These ported sit
592// here so the body of this file reads in C source order without
593// the accessor wrappers interleaved between real port ported.
594// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
595
596// WARNING: NOT IN DATETIME.C — Rust-only module-framework shim.
597// C uses generic featuresarray/handlefeatures/setfeatureenables from
598// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
599// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
600fn module_features() -> &'static Mutex<features> {
601 MODULE_FEATURES.get_or_init(|| {
602 Mutex::new(features {
603 bn_list: None,
604 bn_size: 1,
605 cd_list: None,
606 cd_size: 0,
607 mf_list: None,
608 mf_size: 0,
609 pd_list: None,
610 pd_size: 3,
611 n_abstract: 0,
612 })
613 })
614}
615
616#[cfg(test)]
617mod tests {
618 use super::*;
619
620 #[test]
621 fn test_epoch_seconds() {
622 let _g = crate::test_util::global_state_lock();
623 let secs = getcurrentsecs();
624 assert!(secs > 1700000000);
625 }
626
627 #[test]
628 fn test_epoch_realtime() {
629 let _g = crate::test_util::global_state_lock();
630 let rt = getcurrentrealtime();
631 assert!(rt > 1700000000.0);
632 let arr = getcurrenttime();
633 let secs: i64 = arr[0].parse().unwrap();
634 assert!((rt - secs as f64).abs() < 1.0);
635 }
636
637 #[test]
638 fn test_epoch_time() {
639 let _g = crate::test_util::global_state_lock();
640 let arr = getcurrenttime();
641 let secs: i64 = arr[0].parse().unwrap();
642 let nanos: i64 = arr[1].parse().unwrap();
643 assert!(secs > 1700000000);
644 assert!((0..1_000_000_000).contains(&nanos));
645 }
646
647 /// Build an `Options` struct populated for the canonical
648 /// `output_strftime(name, argv, ops, func)` signature, with
649 /// flag `flag` set and (optionally) -s SCALAR slot encoded.
650 fn ops_for(flags: &[u8], scalar: Option<&str>) -> options {
651 let mut ops = options {
652 ind: [0u8; MAX_OPS],
653 args: Vec::new(),
654 argscount: 0,
655 argsalloc: 0,
656 };
657 for f in flags {
658 ops.ind[*f as usize] = 1;
659 }
660 if let Some(s) = scalar {
661 ops.ind[b's' as usize] = 4;
662 ops.args.push(s.to_string());
663 ops.argscount = 1;
664 ops.argsalloc = 1;
665 }
666 ops
667 }
668
669 /// Reads a scalar from the canonical paramtab — used by tests
670 /// to assert side-effects of params::setsparam writes.
671 fn pt_get(name: &str) -> Option<String> {
672 crate::ported::params::paramtab()
673 .read()
674 .ok()
675 .and_then(|t| t.get(name).and_then(|p| p.u_str.clone()))
676 }
677
678 #[test]
679 fn test_output_strftime_nanoseconds() {
680 let _g = crate::test_util::global_state_lock();
681 let ops = ops_for(&[b'n'], Some("OUT"));
682 let r = output_strftime("strftime", &["%9N", "1700000000", "123456789"], &ops, 0);
683 assert_eq!(r, 0);
684 assert_eq!(pt_get("OUT").as_deref(), Some("123456789"));
685 let r = output_strftime("strftime", &["%3N", "1700000000", "123456789"], &ops, 0);
686 assert_eq!(r, 0);
687 assert_eq!(pt_get("OUT").as_deref(), Some("123"));
688 }
689
690 #[test]
691 fn test_output_strftime_to_scalar() {
692 let _g = crate::test_util::global_state_lock();
693 let ops = ops_for(&[b'n'], Some("OUT2"));
694 let r = output_strftime("strftime", &["%s", "1700000000"], &ops, 0);
695 assert_eq!(r, 0);
696 assert_eq!(pt_get("OUT2").as_deref(), Some("1700000000"));
697 }
698
699 #[test]
700 fn test_output_strftime_format_required() {
701 let _g = crate::test_util::global_state_lock();
702 let ops = ops_for(&[], None);
703 let r = output_strftime("strftime", &[], &ops, 0);
704 assert_eq!(r, 1);
705 }
706
707 /// c:206 — `getcurrentsecs` matches `time(NULL)` within 1 second.
708 /// Pinning the libc-passthrough so a regression that adds an
709 /// offset (timezone, monotonic-vs-realtime confusion) gets caught.
710 #[test]
711 fn getcurrentsecs_matches_libc_time() {
712 let _g = crate::test_util::global_state_lock();
713 let libc_now = unsafe { libc::time(std::ptr::null_mut()) } as i64;
714 let our_now = getcurrentsecs();
715 assert!(
716 (our_now - libc_now).abs() <= 1,
717 "getcurrentsecs {} drifted from libc::time {}",
718 our_now,
719 libc_now
720 );
721 }
722
723 /// c:212 — `getcurrentrealtime` returns a value with nonzero
724 /// sub-second precision over a small sample. Catches a regression
725 /// that truncates to whole seconds (e.g. wrong tv_nsec scaling).
726 #[test]
727 fn getcurrentrealtime_carries_subsecond_precision() {
728 let _g = crate::test_util::global_state_lock();
729 let mut saw_fractional = false;
730 for _ in 0..10 {
731 let rt = getcurrentrealtime();
732 if rt.fract().abs() > 1e-9 {
733 saw_fractional = true;
734 break;
735 }
736 }
737 assert!(
738 saw_fractional,
739 "getcurrentrealtime over 10 samples never produced a fractional part"
740 );
741 }
742
743 /// c:220 — `getcurrenttime` returns `(secs, nanos)` with
744 /// nanos < 1_000_000_000. Pinning the nanos invariant catches
745 /// a regression that returns microseconds (cap 1e6) or the raw
746 /// time-spec value without modulo.
747 #[test]
748 fn getcurrenttime_nanos_under_one_billion() {
749 let _g = crate::test_util::global_state_lock();
750 for _ in 0..5 {
751 let arr = getcurrenttime();
752 let nanos: i64 = arr[1].parse().unwrap();
753 assert!(
754 nanos < 1_000_000_000,
755 "nanos {} >= 1e9 — unit confusion in c:220 port",
756 nanos
757 );
758 assert!(nanos >= 0, "nanos {} negative", nanos);
759 }
760 }
761
762 /// c:212 — Wall-clock advances monotonically forward between two
763 /// `getcurrentrealtime` calls. Captures a "clock went backward"
764 /// regression that would break `$EPOCHREALTIME` script timing.
765 #[test]
766 fn getcurrentrealtime_advances_forward() {
767 let _g = crate::test_util::global_state_lock();
768 let a = getcurrentrealtime();
769 std::thread::sleep(Duration::from_millis(10));
770 let b = getcurrentrealtime();
771 assert!(b >= a, "realtime went backward: {} -> {}", a, b);
772 assert!(
773 b - a < 5.0,
774 "realtime jumped {} seconds in 10ms sleep",
775 b - a
776 );
777 }
778
779 /// c:206 — `getcurrentsecs` advances monotonically across sleeps.
780 /// Same as above but for the integer-second accessor.
781 #[test]
782 fn getcurrentsecs_advances_or_stays_equal() {
783 let _g = crate::test_util::global_state_lock();
784 let a = getcurrentsecs();
785 std::thread::sleep(Duration::from_millis(20));
786 let b = getcurrentsecs();
787 assert!(b >= a, "seconds went backward: {} -> {}", a, b);
788 }
789
790 /// c:99 — `output_strftime` with `%s` (epoch seconds) and `%n`
791 /// flag must print the exact epoch back. Pinning the
792 /// non-side-effect path catches a regression that mangles the
793 /// `-n` (no-newline) shortcut.
794 #[test]
795 fn output_strftime_percent_s_round_trips() {
796 let _g = crate::test_util::global_state_lock();
797 let ops = ops_for(&[b'n'], Some("EPOCH_ROUND_TRIP"));
798 let r = output_strftime("strftime", &["%s", "1234567890"], &ops, 0);
799 assert_eq!(r, 0);
800 assert_eq!(pt_get("EPOCH_ROUND_TRIP").as_deref(), Some("1234567890"));
801 }
802
803 /// c:42-99 — `output_strftime` with an unparseable epoch input
804 /// must return nonzero. Catches a regression that silently
805 /// produces "Wed Dec 31 ..." (epoch 0) on garbage input.
806 #[test]
807 fn output_strftime_invalid_epoch_returns_nonzero() {
808 let _g = crate::test_util::global_state_lock();
809 let ops = ops_for(&[b'n'], Some("BAD"));
810 let r = output_strftime("strftime", &["%s", "not-a-number"], &ops, 0);
811 assert_ne!(r, 0, "garbage epoch must be rejected");
812 }
813
814 /// c:270-307 — module-lifecycle stubs all return 0 in C.
815 #[test]
816 fn module_lifecycle_shims_all_return_zero() {
817 let _g = crate::test_util::global_state_lock();
818 let m: *const module = std::ptr::null();
819 assert_eq!(setup_(m), 0);
820 assert_eq!(boot_(m), 0);
821 assert_eq!(cleanup_(m), 0);
822 assert_eq!(finish_(m), 0);
823 }
824
825 /// c:285 — `enables_` returns 0 and populates `enables` to
826 /// non-None (the module always advertises ≥ 1 feature). A None
827 /// return would mean "no features" and `zmodload -F zsh/datetime`
828 /// would silently disable strftime.
829 #[test]
830 fn enables_populates_some_vec() {
831 let _g = crate::test_util::global_state_lock();
832 let m: *const module = std::ptr::null();
833 let mut enables: Option<Vec<i32>> = None;
834 assert_eq!(enables_(m, &mut enables), 0);
835 assert!(enables.is_some(), "enables must be Some after enables_");
836 }
837
838 // ═══════════════════════════════════════════════════════════════════
839 // getcurrentsecs / getcurrentrealtime / getcurrenttime — back the
840 // $EPOCHSECONDS / $EPOCHREALTIME / $epochtime parameters. Test that
841 // they return monotonic / sane values and the right structure.
842 // ═══════════════════════════════════════════════════════════════════
843
844 /// getcurrentsecs returns a positive epoch time (after year 2020).
845 #[test]
846 fn getcurrentsecs_returns_post_2020_epoch() {
847 let _g = crate::test_util::global_state_lock();
848 let s = getcurrentsecs();
849 // Year 2020-01-01 = epoch 1577836800. Any current time is > this.
850 assert!(s > 1_577_836_800, "epoch must be after 2020-01-01; got {s}");
851 // And < year 2100 (4102444800) — sanity bound.
852 assert!(
853 s < 4_102_444_800,
854 "epoch must be before 2100-01-01; got {s}"
855 );
856 }
857
858 /// Two successive calls must be monotonically non-decreasing.
859 #[test]
860 fn getcurrentsecs_is_monotonic_non_decreasing() {
861 let _g = crate::test_util::global_state_lock();
862 let a = getcurrentsecs();
863 let b = getcurrentsecs();
864 assert!(b >= a, "time went backwards: {a} → {b}");
865 }
866
867 /// getcurrentrealtime returns a positive f64 with sub-second precision.
868 #[test]
869 fn getcurrentrealtime_returns_positive_float() {
870 let _g = crate::test_util::global_state_lock();
871 let t = getcurrentrealtime();
872 assert!(t > 1_577_836_800.0, "realtime must be after 2020; got {t}");
873 }
874
875 /// getcurrentrealtime is close to getcurrentsecs (within 1 second).
876 #[test]
877 fn getcurrentrealtime_matches_getcurrentsecs_within_a_second() {
878 let _g = crate::test_util::global_state_lock();
879 let secs = getcurrentsecs() as f64;
880 let real = getcurrentrealtime();
881 let diff = (real - secs).abs();
882 assert!(diff < 2.0, "realtime/secs diverge by {diff} seconds");
883 }
884
885 /// getcurrenttime returns a 2-element array [secs, nanos].
886 #[test]
887 fn getcurrenttime_returns_two_element_array() {
888 let _g = crate::test_util::global_state_lock();
889 let arr = getcurrenttime();
890 assert_eq!(arr.len(), 2, "must be exactly 2 elements [secs, nanos]");
891 }
892
893 /// First element of getcurrenttime is the epoch seconds (parseable).
894 #[test]
895 fn getcurrenttime_first_element_is_parseable_epoch_seconds() {
896 let _g = crate::test_util::global_state_lock();
897 let arr = getcurrenttime();
898 let secs: i64 = arr[0].parse().expect("element 0 must be valid i64");
899 assert!(secs > 1_577_836_800, "secs must be post-2020");
900 }
901
902 /// Second element of getcurrenttime is nanoseconds (0..1_000_000_000).
903 #[test]
904 fn getcurrenttime_second_element_is_valid_nanoseconds() {
905 let _g = crate::test_util::global_state_lock();
906 let arr = getcurrenttime();
907 let nanos: i64 = arr[1].parse().expect("element 1 must be valid i64");
908 assert!(nanos >= 0, "nanos must be non-negative");
909 assert!(nanos < 1_000_000_000, "nanos must be < 1e9; got {nanos}");
910 }
911
912 /// Successive getcurrenttime calls have non-decreasing secs.
913 #[test]
914 fn getcurrenttime_monotonic_seconds() {
915 let _g = crate::test_util::global_state_lock();
916 let a: i64 = getcurrenttime()[0].parse().unwrap();
917 let b: i64 = getcurrenttime()[0].parse().unwrap();
918 assert!(b >= a, "time went backwards: {a} → {b}");
919 }
920
921 // ─── zsh-corpus pins for datetime helpers ──────────────────────
922
923 /// `getcurrentsecs` returns positive epoch seconds.
924 #[test]
925 fn datetime_corpus_getcurrentsecs_positive() {
926 let _g = crate::test_util::global_state_lock();
927 let s = getcurrentsecs();
928 assert!(s > 1_577_836_800, "post-2020 epoch, got {s}");
929 }
930
931 /// `getcurrentsecs` is monotonically non-decreasing across calls.
932 #[test]
933 fn datetime_corpus_getcurrentsecs_monotonic() {
934 let _g = crate::test_util::global_state_lock();
935 let a = getcurrentsecs();
936 let b = getcurrentsecs();
937 assert!(b >= a, "time went backwards: {a} → {b}");
938 }
939
940 /// `getcurrentrealtime` returns positive f64 in epoch seconds.
941 #[test]
942 fn datetime_corpus_getcurrentrealtime_positive() {
943 let _g = crate::test_util::global_state_lock();
944 let r = getcurrentrealtime();
945 assert!(r > 1_577_836_800.0, "post-2020 epoch, got {r}");
946 }
947
948 /// `getcurrentrealtime` has sub-second precision (probably).
949 /// Pin: returns f64, not just integer-valued.
950 #[test]
951 fn datetime_corpus_getcurrentrealtime_returns_f64() {
952 let _g = crate::test_util::global_state_lock();
953 let r = getcurrentrealtime();
954 // It's a valid float that's finite
955 assert!(r.is_finite(), "must be finite, got {r}");
956 }
957
958 /// `getcurrenttime` returns exactly 2 elements (secs, nanos).
959 #[test]
960 fn datetime_corpus_getcurrenttime_two_elements() {
961 let _g = crate::test_util::global_state_lock();
962 let arr = getcurrenttime();
963 assert_eq!(arr.len(), 2, "exactly 2 elements (secs, nanos)");
964 }
965
966 /// `getcurrenttime` element types: both parse as i64.
967 #[test]
968 fn datetime_corpus_getcurrenttime_both_parse_as_i64() {
969 let _g = crate::test_util::global_state_lock();
970 let arr = getcurrenttime();
971 let _: i64 = arr[0].parse().expect("secs parses");
972 let _: i64 = arr[1].parse().expect("nanos parses");
973 }
974
975 /// `getcurrentsecs` and `getcurrenttime[0]` agree within a couple seconds.
976 #[test]
977 fn datetime_corpus_getcurrentsecs_matches_getcurrenttime_secs() {
978 let _g = crate::test_util::global_state_lock();
979 let a = getcurrentsecs();
980 let b: i64 = getcurrenttime()[0].parse().unwrap();
981 // Should differ by at most a couple seconds.
982 assert!(
983 (a - b).abs() <= 2,
984 "getcurrentsecs={a} should match getcurrenttime[0]={b} within ~2s"
985 );
986 }
987
988 // ═══════════════════════════════════════════════════════════════════
989 // Additional C-parity tests for Src/Modules/datetime.c.
990 // ═══════════════════════════════════════════════════════════════════
991
992 /// c:266 — `getcurrentsecs` returns positive epoch (post-Y2000).
993 #[test]
994 fn getcurrentsecs_returns_positive_epoch() {
995 let _g = crate::test_util::global_state_lock();
996 let s = getcurrentsecs();
997 assert!(s > 0, "epoch must be positive, got {}", s);
998 assert!(s > 946_684_800, "must be after Y2000");
999 }
1000
1001 /// c:266 — monotonic non-decreasing across 3 immediate calls.
1002 #[test]
1003 fn getcurrentsecs_is_monotonic_non_decreasing_pin() {
1004 let _g = crate::test_util::global_state_lock();
1005 let a = getcurrentsecs();
1006 let b = getcurrentsecs();
1007 let c = getcurrentsecs();
1008 assert!(b >= a, "{} -> {} went backwards", a, b);
1009 assert!(c >= b, "{} -> {} went backwards", b, c);
1010 }
1011
1012 /// c:283 — `getcurrentrealtime` returns positive double.
1013 #[test]
1014 fn getcurrentrealtime_returns_positive() {
1015 let _g = crate::test_util::global_state_lock();
1016 let t = getcurrentrealtime();
1017 assert!(t > 0.0);
1018 }
1019
1020 /// c:283 — not NaN, finite.
1021 #[test]
1022 fn getcurrentrealtime_is_finite() {
1023 let _g = crate::test_util::global_state_lock();
1024 let t = getcurrentrealtime();
1025 assert!(!t.is_nan());
1026 assert!(t.is_finite());
1027 }
1028
1029 /// c:283 — agrees with getcurrentsecs ±5s.
1030 #[test]
1031 fn getcurrentrealtime_agrees_with_secs() {
1032 let _g = crate::test_util::global_state_lock();
1033 let real = getcurrentrealtime();
1034 let secs = getcurrentsecs() as f64;
1035 assert!((real - secs).abs() < 5.0);
1036 }
1037
1038 /// c:303 — returns exactly 2 elements [sec, nsec].
1039 #[test]
1040 fn getcurrenttime_returns_two_elements_pin() {
1041 let _g = crate::test_util::global_state_lock();
1042 let arr = getcurrenttime();
1043 assert_eq!(arr.len(), 2);
1044 }
1045
1046 /// c:303 — nsec in [0, 1B).
1047 #[test]
1048 fn getcurrenttime_nsec_in_valid_range() {
1049 let _g = crate::test_util::global_state_lock();
1050 let arr = getcurrenttime();
1051 let nsec: i64 = arr[1].parse().expect("nsec parses");
1052 assert!(
1053 nsec >= 0 && nsec < 1_000_000_000,
1054 "nsec out of range: {}",
1055 nsec
1056 );
1057 }
1058
1059 /// c:233 — `bin_strftime` with no args returns nonzero (usage).
1060 #[test]
1061 fn bin_strftime_no_args_returns_nonzero() {
1062 let _g = crate::test_util::global_state_lock();
1063 let ops = crate::ported::zsh_h::options {
1064 ind: [0u8; crate::ported::zsh_h::MAX_OPS],
1065 args: Vec::new(),
1066 argscount: 0,
1067 argsalloc: 0,
1068 };
1069 let r = bin_strftime("strftime", &[], &ops, 0);
1070 assert_ne!(r, 0, "no args → usage error");
1071 }
1072
1073 /// Lifecycle (c:329/357) split per-hook.
1074 #[test]
1075 fn datetime_setup_returns_zero_pin() {
1076 let _g = crate::test_util::global_state_lock();
1077 assert_eq!(setup_(std::ptr::null()), 0);
1078 }
1079
1080 /// c:357 — boot_(NULL) = 0.
1081 #[test]
1082 fn datetime_boot_returns_zero_pin() {
1083 let _g = crate::test_util::global_state_lock();
1084 assert_eq!(boot_(std::ptr::null()), 0);
1085 }
1086
1087 // ═══════════════════════════════════════════════════════════════════
1088 // Additional C-parity tests for Src/Modules/datetime.c
1089 // c:266 getcurrentsecs / c:283 getcurrentrealtime / c:303 getcurrenttime
1090 // c:233 bin_strftime / c:329-374 lifecycle
1091 // ═══════════════════════════════════════════════════════════════════
1092
1093 /// c:266 — `getcurrentsecs` returns i64.
1094 #[test]
1095 fn getcurrentsecs_returns_i64_type() {
1096 let _: i64 = getcurrentsecs();
1097 }
1098
1099 /// c:266 — `getcurrentsecs` is monotonically non-decreasing across
1100 /// many calls (no time-travel backward).
1101 #[test]
1102 fn getcurrentsecs_monotonic_across_many_calls() {
1103 let mut prev = getcurrentsecs();
1104 for _ in 0..100 {
1105 let cur = getcurrentsecs();
1106 assert!(cur >= prev, "secs went backwards: {} < {}", cur, prev);
1107 prev = cur;
1108 }
1109 }
1110
1111 /// c:266 — `getcurrentsecs` is after Unix epoch (positive).
1112 #[test]
1113 fn getcurrentsecs_after_epoch() {
1114 assert!(getcurrentsecs() > 0, "must be after 1970-01-01");
1115 }
1116
1117 /// c:283 — `getcurrentrealtime` returns f64.
1118 #[test]
1119 fn getcurrentrealtime_returns_f64_type() {
1120 let _: f64 = getcurrentrealtime();
1121 }
1122
1123 /// c:283 — `getcurrentrealtime` non-negative.
1124 #[test]
1125 fn getcurrentrealtime_non_negative() {
1126 assert!(getcurrentrealtime() >= 0.0);
1127 }
1128
1129 /// c:303 — `getcurrenttime` returns Vec<String>.
1130 #[test]
1131 fn getcurrenttime_returns_vec_string() {
1132 let _: Vec<String> = getcurrenttime();
1133 }
1134
1135 /// c:303 — `getcurrenttime` first element parses as integer (seconds).
1136 #[test]
1137 fn getcurrenttime_first_is_numeric() {
1138 let v = getcurrenttime();
1139 assert!(!v.is_empty(), "must have ≥ 1 element");
1140 let parsed: Result<i64, _> = v[0].parse();
1141 assert!(parsed.is_ok(), "first element {:?} must parse as i64", v[0]);
1142 }
1143
1144 /// c:233 — `bin_strftime` return value in u8 exit-code range.
1145 #[test]
1146 fn bin_strftime_return_in_exit_code_range() {
1147 let _g = crate::test_util::global_state_lock();
1148 let ops = crate::ported::zsh_h::options {
1149 ind: [0u8; crate::ported::zsh_h::MAX_OPS],
1150 args: Vec::new(),
1151 argscount: 0,
1152 argsalloc: 0,
1153 };
1154 for args in [
1155 vec![],
1156 vec!["%Y".to_string()],
1157 vec!["%Y".to_string(), "1000".to_string()],
1158 ] {
1159 let r = bin_strftime("strftime", &args, &ops, 0);
1160 assert!(
1161 (0..256).contains(&r),
1162 "exit code {} must fit in u8 for {:?}",
1163 r,
1164 args
1165 );
1166 }
1167 }
1168
1169 /// c:329-374 — full lifecycle setup→features→enables→boot→cleanup→finish.
1170 #[test]
1171 fn datetime_full_lifecycle_returns_zero_for_all() {
1172 let _g = crate::test_util::global_state_lock();
1173 let null = std::ptr::null();
1174 assert_eq!(setup_(null), 0);
1175 let mut feats = Vec::new();
1176 let _ = features_(null, &mut feats);
1177 let mut enables: Option<Vec<i32>> = None;
1178 let _ = enables_(null, &mut enables);
1179 assert_eq!(boot_(null), 0);
1180 assert_eq!(cleanup_(null), 0);
1181 assert_eq!(finish_(null), 0);
1182 }
1183
1184 /// c:374 — finish_ idempotent.
1185 #[test]
1186 fn datetime_finish_idempotent() {
1187 let _g = crate::test_util::global_state_lock();
1188 for _ in 0..10 {
1189 assert_eq!(finish_(std::ptr::null()), 0);
1190 }
1191 }
1192
1193 // ═══════════════════════════════════════════════════════════════════
1194 // Additional C-parity tests for Src/Modules/datetime.c
1195 // c:34 reverse_strftime / c:89 output_strftime / c:266 getcurrentsecs
1196 // c:283 getcurrentrealtime / c:303 getcurrenttime
1197 // ═══════════════════════════════════════════════════════════════════
1198
1199 /// c:266 — `getcurrentsecs` is deterministic-ish (monotonic step bound).
1200 #[test]
1201 fn getcurrentsecs_two_calls_close_in_time() {
1202 let _g = crate::test_util::global_state_lock();
1203 let a = getcurrentsecs();
1204 let b = getcurrentsecs();
1205 assert!(
1206 (b - a).abs() <= 2,
1207 "two getcurrentsecs calls must be within 2 seconds"
1208 );
1209 }
1210
1211 /// c:283 — `getcurrentrealtime` returns f64 finite.
1212 #[test]
1213 fn getcurrentrealtime_returns_finite_f64() {
1214 let _g = crate::test_util::global_state_lock();
1215 let v = getcurrentrealtime();
1216 assert!(v.is_finite(), "must be finite");
1217 assert!(v > 0.0, "after epoch");
1218 }
1219
1220 /// c:283 — `getcurrentrealtime` and `getcurrentsecs` agree on whole secs.
1221 #[test]
1222 fn getcurrentrealtime_floor_matches_getcurrentsecs() {
1223 let _g = crate::test_util::global_state_lock();
1224 let secs = getcurrentsecs() as f64;
1225 let real = getcurrentrealtime();
1226 // Allow 2 seconds drift since they may straddle the second boundary.
1227 assert!(
1228 (real - secs).abs() < 2.0,
1229 "realtime {} and secs {} must agree within 2 sec",
1230 real,
1231 secs
1232 );
1233 }
1234
1235 /// c:303 — `getcurrenttime` returns Vec<String> with second element parseable.
1236 #[test]
1237 fn getcurrenttime_second_element_is_nsec() {
1238 let _g = crate::test_util::global_state_lock();
1239 let v = getcurrenttime();
1240 if v.len() >= 2 {
1241 let nsec: Result<i64, _> = v[1].parse();
1242 assert!(nsec.is_ok(), "second element {:?} must parse as i64", v[1]);
1243 if let Ok(n) = nsec {
1244 assert!(n >= 0, "nsec must be ≥ 0");
1245 }
1246 }
1247 }
1248
1249 /// c:303 — `getcurrenttime` Vec length is exactly 2 (secs, nsec).
1250 #[test]
1251 fn getcurrenttime_length_is_two() {
1252 let _g = crate::test_util::global_state_lock();
1253 let v = getcurrenttime();
1254 assert_eq!(v.len(), 2, "secs + nsec = 2 elements");
1255 }
1256
1257 /// c:303 — `getcurrenttime` is deterministic in structure (always 2 elements).
1258 #[test]
1259 fn getcurrenttime_always_two_elements() {
1260 let _g = crate::test_util::global_state_lock();
1261 for _ in 0..5 {
1262 assert_eq!(getcurrenttime().len(), 2);
1263 }
1264 }
1265
1266 /// c:34 — `reverse_strftime` returns i32 (compile-time type pin).
1267 #[test]
1268 fn reverse_strftime_returns_i32_type() {
1269 let _g = crate::test_util::global_state_lock();
1270 let _: i32 = reverse_strftime("strftime", &[], None, 0);
1271 }
1272
1273 /// c:34 — `reverse_strftime` empty argv returns nonzero (usage error).
1274 #[test]
1275 fn reverse_strftime_empty_argv_returns_nonzero() {
1276 let _g = crate::test_util::global_state_lock();
1277 let r = reverse_strftime("strftime", &[], None, 0);
1278 assert_ne!(r, 0, "empty argv → usage error");
1279 }
1280
1281 /// c:266 — `getcurrentsecs` is positive for many calls.
1282 #[test]
1283 fn getcurrentsecs_always_positive() {
1284 let _g = crate::test_util::global_state_lock();
1285 for _ in 0..10 {
1286 assert!(getcurrentsecs() > 0);
1287 }
1288 }
1289
1290 // ═══════════════════════════════════════════════════════════════════
1291 // Additional C-parity tests for Src/Modules/datetime.c
1292 // c:233 bin_strftime / c:266 getcurrentsecs / c:283 getcurrentrealtime /
1293 // c:303 getcurrenttime + lifecycle
1294 // ═══════════════════════════════════════════════════════════════════
1295
1296 /// c:266 — `getcurrentsecs` returns i64 (compile-time pin, alt).
1297 #[test]
1298 fn getcurrentsecs_returns_i64_pin_alt() {
1299 let _g = crate::test_util::global_state_lock();
1300 let _: i64 = getcurrentsecs();
1301 }
1302
1303 /// c:283 — `getcurrentrealtime` returns f64 (compile-time pin, alt).
1304 #[test]
1305 fn getcurrentrealtime_returns_f64_pin_alt() {
1306 let _g = crate::test_util::global_state_lock();
1307 let _: f64 = getcurrentrealtime();
1308 }
1309
1310 /// c:303 — `getcurrenttime` returns Vec<String> (compile-time pin).
1311 #[test]
1312 fn getcurrenttime_returns_vec_string_type() {
1313 let _g = crate::test_util::global_state_lock();
1314 let _: Vec<String> = getcurrenttime();
1315 }
1316
1317 /// c:266 — `getcurrentsecs` is monotonically non-decreasing across
1318 /// rapid consecutive calls (no time-travel within a single test
1319 /// process; system clock could change but on a stable test bed
1320 /// the next call >= prev).
1321 #[test]
1322 fn getcurrentsecs_monotonically_non_decreasing() {
1323 let _g = crate::test_util::global_state_lock();
1324 let mut prev = getcurrentsecs();
1325 for _ in 0..50 {
1326 let now = getcurrentsecs();
1327 assert!(now >= prev, "time went backwards: {} → {}", prev, now);
1328 prev = now;
1329 }
1330 }
1331
1332 /// c:283 — `getcurrentrealtime` is monotonically non-decreasing.
1333 #[test]
1334 fn getcurrentrealtime_monotonically_non_decreasing() {
1335 let _g = crate::test_util::global_state_lock();
1336 let mut prev = getcurrentrealtime();
1337 for _ in 0..50 {
1338 let now = getcurrentrealtime();
1339 assert!(now >= prev, "realtime went backwards: {} → {}", prev, now);
1340 prev = now;
1341 }
1342 }
1343
1344 /// c:266 — `getcurrentsecs` returns plausible current time (after
1345 /// 2020-01-01 epoch = 1577836800, before 2100-01-01 = 4102444800).
1346 #[test]
1347 fn getcurrentsecs_in_plausible_epoch_range() {
1348 let _g = crate::test_util::global_state_lock();
1349 let now = getcurrentsecs();
1350 assert!(
1351 now >= 1_577_836_800,
1352 "current time {} must be after 2020-01-01 epoch",
1353 now
1354 );
1355 assert!(
1356 now <= 4_102_444_800,
1357 "current time {} must be before 2100-01-01 epoch",
1358 now
1359 );
1360 }
1361
1362 /// c:303 — first element of `getcurrenttime` parses as i64 seconds.
1363 #[test]
1364 fn getcurrenttime_first_element_is_secs() {
1365 let _g = crate::test_util::global_state_lock();
1366 let v = getcurrenttime();
1367 assert!(v.len() >= 1, "must have at least one element");
1368 let secs: Result<i64, _> = v[0].parse();
1369 assert!(
1370 secs.is_ok(),
1371 "first element {:?} must parse as i64 secs",
1372 v[0]
1373 );
1374 }
1375
1376 /// c:303 — `getcurrenttime` nsec ≤ 999_999_999 (within one second).
1377 #[test]
1378 fn getcurrenttime_nsec_within_one_second() {
1379 let _g = crate::test_util::global_state_lock();
1380 let v = getcurrenttime();
1381 if v.len() >= 2 {
1382 if let Ok(n) = v[1].parse::<i64>() {
1383 assert!(n <= 999_999_999, "nsec {} must be ≤ 999_999_999", n);
1384 }
1385 }
1386 }
1387
1388 /// c:233 — `bin_strftime` returns i32 (compile-time pin).
1389 #[test]
1390 fn bin_strftime_returns_i32_type() {
1391 let _g = crate::test_util::global_state_lock();
1392 let ops = crate::ported::zsh_h::options {
1393 ind: [0u8; crate::ported::zsh_h::MAX_OPS],
1394 args: Vec::new(),
1395 argscount: 0,
1396 argsalloc: 0,
1397 };
1398 let _: i32 = bin_strftime("strftime", &[], &ops, 0);
1399 }
1400
1401 /// c:233 — `bin_strftime` no-args returns nonzero (usage error, alt).
1402 #[test]
1403 fn bin_strftime_no_args_returns_nonzero_pin_alt() {
1404 let _g = crate::test_util::global_state_lock();
1405 let ops = crate::ported::zsh_h::options {
1406 ind: [0u8; crate::ported::zsh_h::MAX_OPS],
1407 args: Vec::new(),
1408 argscount: 0,
1409 argsalloc: 0,
1410 };
1411 let r = bin_strftime("strftime", &[], &ops, 0);
1412 assert_ne!(r, 0, "no args → usage error");
1413 }
1414
1415 /// c:329/342/350/357/367/374 — each lifecycle hook returns 0 individually.
1416 #[test]
1417 fn datetime_each_lifecycle_hook_returns_zero_individually() {
1418 let _g = crate::test_util::global_state_lock();
1419 let null = std::ptr::null();
1420 let mut v: Vec<String> = Vec::new();
1421 let mut e: Option<Vec<i32>> = None;
1422 assert_eq!(setup_(null), 0, "c:329 setup_");
1423 assert_eq!(features_(null, &mut v), 0, "c:342 features_");
1424 assert_eq!(enables_(null, &mut e), 0, "c:350 enables_");
1425 assert_eq!(boot_(null), 0, "c:357 boot_");
1426 assert_eq!(cleanup_(null), 0, "c:367 cleanup_");
1427 assert_eq!(finish_(null), 0, "c:374 finish_");
1428 }
1429}