Skip to main content

ironwork_rt/
cics.rs

1//! The CICS region a harness run stands in for: one task, its files, its temporary-storage and
2//! transient-data queues, and the time services. The interpreter runs the commands; this holds
3//! what they act on.
4
5use crate::files::{Dd, Format, KeySpan, Keying};
6use crate::calendar::{civil, EPOCH_1900_TO_1970_MILLIS, EPOCH_1900_TO_1970_SECONDS, SECONDS_PER_DAY};
7pub use crate::terminal::Terminal;
8use std::collections::{BTreeMap, HashMap};
9use std::fs::OpenOptions;
10use std::io::Write;
11use std::path::PathBuf;
12
13/// The EXEC interface block's length: EIBRLDBK, its last field, is at X'54'.
14pub const EIB_LEN: usize = 85;
15
16macro_rules! conditions {
17    ($($name:ident)*) => {
18        /// A CICS exception condition, one variant per name DFHRESP knows, spelt as IBM spells it.
19        #[allow(clippy::upper_case_acronyms)]
20        #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
21        pub enum Condition {
22            $($name,)*
23        }
24
25        impl Condition {
26            pub const ALL: &[Self] = &[$(Self::$name,)*];
27
28            pub fn name(self) -> &'static str {
29                match self {
30                    $(Self::$name => stringify!($name),)*
31                }
32            }
33        }
34    };
35}
36
37conditions! {
38    NORMAL ERROR RDATT WRBRK EOF EODS EOC INBFMH ENDINPT NONVAL NOSTART TERMIDERR FILENOTFOUND
39    NOTFND DUPREC DUPKEY INVREQ IOERR NOSPACE NOTOPEN ENDFILE ILLOGIC LENGERR QZERO SIGNAL QBUSY
40    ITEMERR PGMIDERR TRANSIDERR ENDDATA INVTSREQ EXPIRED RETPAGE RTEFAIL RTESOME TSIOERR MAPFAIL
41    INVERRTERM INVMPSZ IGREQID OVERFLOW INVLDC NOSTG JIDERR QIDERR NOJBUFSP DSSTAT SELNERR FUNCERR
42    UNEXPIN NOPASSBKRD NOPASSBKWR SEGIDERR SYSIDERR ISCINVREQ ENQBUSY ENVDEFERR IGREQCD SESSIONERR
43    SYSBUSY SESSBUSY NOTALLOC CBIDERR INVEXITREQ INVPARTNSET INVPARTN PARTNFAIL USERIDERR NOTAUTH
44    VOLIDERR SUPPRESSED RESIDERR NOSPOOL TERMERR ROLLEDBACK END DISABLED ALLOCERR STRELERR OPENERR
45    SPOLBUSY SPOLERR NODEIDERR TASKIDERR TCIDERR DSNNOTFOUND LOADING MODELIDERR OUTDESCRERR
46    PARTNERIDERR PROFILEIDERR NETNAMEIDERR LOCKED RECORDBUSY UOWNOTFOUND UOWLNOTFOUND LINKABEND
47    CHANGED PROCESSBUSY ACTIVITYBUSY PROCESSERR ACTIVITYERR CONTAINERERR EVENTERR TOKENERR
48    NOTFINISHED POOLERR TIMERERR SYMBOLERR TEMPLATERR NOTSUPERUSER CSDERR DUPRES RESUNAVAIL
49    CHANNELERR CCSIDERR TIMEDOUT CODEPAGEERR INCOMPLETE APPNOTFOUND BUSY
50}
51
52/// Names a condition also goes by: DSIDERR is FILENOTFOUND's older name.
53const ALIASES: &[(&str, Condition)] = &[("DSIDERR", Condition::FILENOTFOUND)];
54
55/// The transaction abend CICS issues when a condition is raised and nothing handles it; a
56/// condition not listed abends AEIP.
57const DEFAULT_ABENDS: &[(Condition, &str)] = &[
58    (Condition::NOTFND, "AEIM"),
59    (Condition::DUPREC, "AEIN"),
60    (Condition::DUPKEY, "AEIO"),
61    (Condition::IOERR, "AEIQ"),
62    (Condition::NOSPACE, "AEIR"),
63    (Condition::NOTOPEN, "AEIS"),
64    (Condition::ENDFILE, "AEIT"),
65    (Condition::ILLOGIC, "AEIU"),
66    (Condition::LENGERR, "AEIV"),
67    (Condition::QZERO, "AEIW"),
68    (Condition::ITEMERR, "AEIZ"),
69    (Condition::PGMIDERR, "AEI0"),
70    (Condition::TRANSIDERR, "AEI1"),
71    (Condition::ENDDATA, "AEI2"),
72    (Condition::INVTSREQ, "AEI3"),
73    (Condition::EXPIRED, "AEI4"),
74    (Condition::TSIOERR, "AEI8"),
75    (Condition::MAPFAIL, "AEI9"),
76    (Condition::ERROR, "AEIA"),
77    (Condition::EOF, "AEID"),
78    (Condition::EODS, "AEIE"),
79    (Condition::INBFMH, "AEIG"),
80    (Condition::ENDINPT, "AEIH"),
81    (Condition::NONVAL, "AEII"),
82    (Condition::NOSTART, "AEIJ"),
83    (Condition::TERMIDERR, "AEIK"),
84    (Condition::FILENOTFOUND, "AEIL"),
85    (Condition::DISABLED, "AEXL"),
86    (Condition::ROLLEDBACK, "AEXJ"),
87    (Condition::LOCKED, "AEX8"),
88    (Condition::RECORDBUSY, "AEX9"),
89    (Condition::QIDERR, "AEYH"),
90    (Condition::SYSIDERR, "AEYQ"),
91    (Condition::NOTAUTH, "AEY7"),
92    (Condition::USERIDERR, "AEYX"),
93    (Condition::CONTAINERERR, "AEZJ"),
94    (Condition::CHANNELERR, "AEZV"),
95];
96
97impl Condition {
98    /// The condition DFHRESP(name) names, by either of its names, in any case.
99    pub fn from_name(name: &str) -> Option<Self> {
100        let alias = ALIASES.iter().find(|(a, _)| a.eq_ignore_ascii_case(name)).map(|&(_, c)| c);
101        alias.or_else(|| Self::ALL.iter().copied().find(|c| c.name().eq_ignore_ascii_case(name)))
102    }
103
104    /// EIBRESP's value for the condition, from IBM's DFHRESP table.
105    pub fn resp(self) -> i32 {
106        crate::cics_tables::resp(self.name()).unwrap_or_else(|| panic!("DFHRESP has no {}", self.name()))
107    }
108
109    pub fn default_abend(self) -> &'static str {
110        DEFAULT_ABENDS.iter().find(|&&(c, _)| c == self).map_or("AEIP", |&(_, abend)| abend)
111    }
112}
113
114/// What kind of VSAM data set a CICS file is.
115#[derive(Clone, Debug, PartialEq, Eq)]
116pub enum DataSet {
117    Ksds { key: KeySpan },
118    Rrds,
119}
120
121/// A file-control table entry: a CICS FILE name's data set, given with --file.
122#[derive(Clone, Debug, PartialEq, Eq)]
123pub struct FileDef {
124    pub dd: Dd,
125    pub data_set: DataSet,
126    pub record_len: usize,
127}
128
129impl FileDef {
130    /// How files::open_keyed keys the data set.
131    pub fn keying(&self) -> Keying {
132        match &self.data_set {
133            DataSet::Ksds { key } => Keying::Indexed { prime: *key, alternates: vec![] },
134            DataSet::Rrds => Keying::Relative,
135        }
136    }
137
138    pub fn format(&self) -> Format {
139        self.dd.format.unwrap_or(Format::Fixed)
140    }
141}
142
143/// Parses `NAME=path,KSDS,key=OFFSET:LENGTH,len=RECLEN[,text|,fixed|,variable]` or
144/// `NAME=path,RRDS,len=RECLEN[,text|,fixed|,variable]`.
145pub fn parse_file(spec: &str) -> Result<(String, FileDef), String> {
146    let parts: Vec<&str> = spec.split(',').collect();
147    if parts.len() < 3 {
148        return Err(format!("{spec}: expected NAME=path,KIND,…"));
149    }
150
151    let (name, path) = parts[0]
152        .split_once('=')
153        .ok_or_else(|| format!("{spec}: expected NAME=path"))?;
154    let name = name.to_ascii_uppercase();
155    let kind = parts[1].to_ascii_uppercase();
156
157    let mut key: Option<KeySpan> = None;
158    let mut record_len: Option<usize> = None;
159    let mut format: Option<Format> = None;
160
161    for part in &parts[2..] {
162        let part = part.trim();
163        if let Some(val) = part.strip_prefix("key=") {
164            let (off, len) = val
165                .split_once(':')
166                .ok_or_else(|| format!("{spec}: bad key spec {val}"))?;
167            let off: usize = off.parse().map_err(|_| format!("{spec}: bad key offset {off}"))?;
168            let len: usize = len.parse().map_err(|_| format!("{spec}: bad key length {len}"))?;
169            key = Some(KeySpan { offset: off, len });
170        } else if let Some(val) = part.strip_prefix("len=") {
171            record_len = Some(
172                val.parse()
173                    .map_err(|_| format!("{spec}: bad record length {val}"))?,
174            );
175        } else if let Some(f) = Format::from_keyword(part) {
176            format = Some(f);
177        } else {
178            return Err(format!("{spec}: unknown field {part}"));
179        }
180    }
181
182    let data_set = match kind.as_str() {
183        "KSDS" => {
184            let key = key.ok_or_else(|| format!("{spec}: KSDS requires key=OFFSET:LENGTH"))?;
185            DataSet::Ksds { key }
186        }
187        "RRDS" => DataSet::Rrds,
188        _ => return Err(format!("{spec}: unknown data set kind {kind}")),
189    };
190
191    let record_len = record_len.ok_or_else(|| format!("{spec}: missing len=RECLEN"))?;
192    let dd = Dd { path: PathBuf::from(path), format };
193    Ok((name, FileDef { dd, data_set, record_len }))
194}
195
196/// A temporary-storage queue: its items (item numbers start at 1) and where READQ TS NEXT is.
197#[derive(Clone, Debug, Default, PartialEq, Eq)]
198pub struct TsQueue {
199    pub items: Vec<Vec<u8>>,
200    pub next: usize,
201}
202
203/// A browse's position: the key it is at, and whether the record there is itself next (after
204/// STARTBR or RESETBR) or was the last returned.
205#[derive(Clone, Debug, Default, PartialEq, Eq)]
206pub struct Browse {
207    pub at: Vec<u8>,
208    pub inclusive: bool,
209}
210
211/// The task: who and what started it, and the resources it has touched.
212#[derive(Debug, Default)]
213pub struct Task {
214    pub transid: String,
215    pub termid: String,
216    pub userid: String,
217    pub applid: String,
218    pub sysid: String,
219    pub number: u32,
220    /// The COMMAREA the task starts with; EIBCALEN is its length.
221    pub commarea: Option<Vec<u8>>,
222    pub files: HashMap<String, FileDef>,
223    pub ts: BTreeMap<String, TsQueue>,
224    pub td: BTreeMap<String, Vec<Vec<u8>>>,
225    /// TD queues whose items are appended to a host file as text lines when the task ends.
226    pub td_files: HashMap<String, PathBuf>,
227    /// The key each file's READ UPDATE holds, for REWRITE, DELETE and UNLOCK.
228    pub held: HashMap<String, Vec<u8>>,
229    /// Open browses by file and REQID.
230    pub browses: HashMap<(String, i64), Browse>,
231    pub terminal: Option<Box<dyn Terminal>>,
232    /// The AID key whose input started the task, as EIBAID shows it before any RECEIVE.
233    pub initial_aid: Option<u8>,
234    /// Mapsets already read from the copy libraries, by name.
235    pub mapsets: HashMap<String, crate::bms::Mapset>,
236    /// RETURN TRANSID and COMMAREA, when the task ended that way.
237    pub next_transid: Option<String>,
238    pub returned_commarea: Option<Vec<u8>>,
239}
240
241impl Task {
242    /// WRITEQ TS: appends and returns the new item's number; with Some(item) (REWRITE) replaces
243    /// that item and returns it.
244    pub fn writeq_ts(
245        &mut self,
246        queue: &str,
247        rewrite: Option<usize>,
248        data: &[u8],
249    ) -> Result<usize, Condition> {
250        let queue = queue.trim_end();
251        match rewrite {
252            None => {
253                let q = self
254                    .ts
255                    .entry(queue.to_string())
256                    .or_default();
257                q.items.push(data.to_vec());
258                Ok(q.items.len())
259            }
260            Some(item) => {
261                let q = self.ts.get_mut(queue).ok_or(Condition::QIDERR)?;
262                if item == 0 || item > q.items.len() {
263                    return Err(Condition::ITEMERR);
264                }
265                q.items[item - 1] = data.to_vec();
266                Ok(item)
267            }
268        }
269    }
270
271    /// READQ TS: Some(item) reads that item; None reads the next item after the last read.
272    pub fn readq_ts(
273        &mut self,
274        queue: &str,
275        item: Option<usize>,
276    ) -> Result<(Vec<u8>, usize), Condition> {
277        let queue = queue.trim_end();
278        let q = self.ts.get_mut(queue).ok_or(Condition::QIDERR)?;
279        let item_num = match item {
280            Some(n) => {
281                if n == 0 || n > q.items.len() {
282                    return Err(Condition::ITEMERR);
283                }
284                n
285            }
286            None => {
287                let n = if q.next == 0 { 1 } else { q.next + 1 };
288                if n > q.items.len() {
289                    return Err(Condition::ITEMERR);
290                }
291                n
292            }
293        };
294        let data = q.items[item_num - 1].clone();
295        q.next = item_num;
296        Ok((data, q.items.len()))
297    }
298
299    /// DELETEQ TS.
300    pub fn deleteq_ts(&mut self, queue: &str) -> Result<(), Condition> {
301        let queue = queue.trim_end();
302        self.ts.remove(queue).map(|_| ()).ok_or(Condition::QIDERR)
303    }
304
305    /// WRITEQ TD: appends (creating the queue).
306    pub fn writeq_td(&mut self, queue: &str, data: &[u8]) {
307        let queue = queue.trim_end();
308        self.td.entry(queue.to_string()).or_default().push(data.to_vec());
309    }
310
311    /// READQ TD: removes and returns the oldest item.
312    pub fn readq_td(&mut self, queue: &str) -> Result<Vec<u8>, Condition> {
313        let queue = queue.trim_end();
314        let items = self.td.get_mut(queue).ok_or(Condition::QZERO)?;
315        if items.is_empty() {
316            return Err(Condition::QZERO);
317        }
318        Ok(items.remove(0))
319    }
320
321    /// DELETEQ TD: empties the queue (no error when absent).
322    pub fn deleteq_td(&mut self, queue: &str) {
323        let queue = queue.trim_end();
324        if let Some(items) = self.td.get_mut(queue) {
325            items.clear();
326        }
327    }
328
329    /// Appends each TD queue that has a td_files entry to its file, one line per item, each item
330    /// decoded through `page` with trailing spaces trimmed; then empties those queues.
331    pub fn flush_td(&mut self, page: &zarch::ebcdic::CodePage) -> std::io::Result<()> {
332        let queues: Vec<String> = self.td_files.keys().cloned().collect();
333        for queue in &queues {
334            let Some(path) = self.td_files.get(queue) else { continue };
335            let items: Vec<Vec<u8>> = self
336                .td
337                .get(queue)
338                .cloned()
339                .unwrap_or_default();
340            if items.is_empty() {
341                continue;
342            }
343            let mut file = OpenOptions::new().create(true).append(true).open(path)?;
344            for item in &items {
345                let text = page.decode(item);
346                let line = format!("{}\n", text.trim_end());
347                file.write_all(line.as_bytes())?;
348            }
349            if let Some(q) = self.td.get_mut(queue) {
350                q.clear();
351            }
352        }
353        Ok(())
354    }
355}
356
357/// Milliseconds from 1900-01-01T00:00:00 to a Unix time given as seconds and hundredths.
358pub fn abstime(seconds: i64, hundredths: u32) -> i64 {
359    EPOCH_1900_TO_1970_MILLIS + seconds * 1000 + hundredths as i64 * 10
360}
361
362/// Unix seconds (rounded down) of an ABSTIME.
363pub fn unix_seconds(abstime: i64) -> i64 {
364    (abstime - EPOCH_1900_TO_1970_MILLIS).div_euclid(1000)
365}
366
367/// EIBDATE's value for an ABSTIME: 0CYYDDD as a decimal number.
368pub fn eib_date(abstime: i64) -> i64 {
369    let secs = unix_seconds(abstime);
370    let date = civil(secs);
371    let c = if date.year >= 2000 { 1 } else { 0 };
372    let yy = date.year % 100;
373    c * 100_000 + yy * 1_000 + i64::from(date.day_of_year)
374}
375
376/// EIBTIME's value for an ABSTIME: 0HHMMSS as a decimal number.
377pub fn eib_time(abstime: i64) -> i64 {
378    let secs = unix_seconds(abstime);
379    let time = civil(secs);
380    i64::from(time.hour) * 10_000 + i64::from(time.minute) * 100 + i64::from(time.second)
381}
382
383/// A FORMATTIME output: text for the date and time forms, a number for the counts.
384#[derive(Clone, Debug, PartialEq, Eq)]
385pub enum FormatValue {
386    Text(String),
387    Number(i64),
388}
389
390/// One FORMATTIME option's value for an ABSTIME, or None for an option this does not produce.
391/// `datesep` and `timesep` are the separators when DATESEP / TIMESEP were given.
392pub fn format_time(abstime: i64, option: &str, datesep: Option<char>, timesep: Option<char>) -> Option<FormatValue> {
393    let secs = unix_seconds(abstime);
394    let c = civil(secs);
395    let (year, month, day, hour, minute, second, doy) = (c.year, c.month, c.day, c.hour, c.minute, c.second, c.day_of_year);
396
397    let yy = format!("{:02}", year % 100);
398    let yyyy = format!("{:04}", year);
399    let mm = format!("{:02}", month);
400    let dd = format!("{:02}", day);
401    let hh = format!("{:02}", hour);
402    let mi = format!("{:02}", minute);
403    let ss = format!("{:02}", second);
404    let ddd = format!("{:03}", doy);
405
406    let join = |parts: &[&str], sep: Option<char>| match sep {
407        Some(c) => {
408            let mut s = String::new();
409            for (i, p) in parts.iter().enumerate() {
410                if i > 0 {
411                    s.push(c);
412                }
413                s.push_str(p);
414            }
415            s
416        }
417        None => parts.concat(),
418    };
419
420    let option = option.to_ascii_uppercase();
421
422    match option.as_str() {
423        "YYYYMMDD" => Some(FormatValue::Text(join(&[&yyyy, &mm, &dd], datesep))),
424        "YYMMDD" => Some(FormatValue::Text(join(&[&yy, &mm, &dd], datesep))),
425        "YYDDMM" => Some(FormatValue::Text(join(&[&yy, &dd, &mm], datesep))),
426        "YYYYDDMM" => Some(FormatValue::Text(join(&[&yyyy, &dd, &mm], datesep))),
427        "DDMMYY" => Some(FormatValue::Text(join(&[&dd, &mm, &yy], datesep))),
428        "DDMMYYYY" => Some(FormatValue::Text(join(&[&dd, &mm, &yyyy], datesep))),
429        "MMDDYY" | "DATE" => Some(FormatValue::Text(join(&[&mm, &dd, &yy], datesep))),
430        "MMDDYYYY" | "FULLDATE" => Some(FormatValue::Text(join(&[&mm, &dd, &yyyy], datesep))),
431        "YYDDD" => Some(FormatValue::Text(join(&[&yy, &ddd], datesep))),
432        "YYYYDDD" => Some(FormatValue::Text(join(&[&yyyy, &ddd], datesep))),
433        "DATEFORM" => Some(FormatValue::Text("MMDDYY".to_string())),
434        "TIME" => Some(FormatValue::Text(join(&[&hh, &mi, &ss], timesep))),
435        "DAYCOUNT" => Some(FormatValue::Number((secs + EPOCH_1900_TO_1970_SECONDS) / SECONDS_PER_DAY)),
436        "DAYOFWEEK" => Some(FormatValue::Number(c.cics_weekday())),
437        "DAYOFMONTH" => Some(FormatValue::Number(day as i64)),
438        "MONTHOFYEAR" => Some(FormatValue::Number(month as i64)),
439        "YEAR" => Some(FormatValue::Number(year)),
440        "MILLISECONDS" => Some(FormatValue::Number(abstime.rem_euclid(1000))),
441        _ => None,
442    }
443}
444
445#[cfg(test)]
446mod tests {
447    use super::*;
448
449    #[test]
450    fn every_condition_is_a_dfhresp_row_with_its_number() {
451        for &c in Condition::ALL {
452            assert_eq!(Some(c.resp()), crate::cics_tables::resp(c.name()), "{c:?}");
453            assert_eq!(Condition::from_name(c.name()), Some(c));
454        }
455        let rows: Vec<&str> = crate::cics_tables::resp_table().iter().map(|r| r.condition).collect();
456        let names: Vec<&str> = Condition::ALL.iter().map(|c| c.name()).collect();
457        assert_eq!(names, rows, "one variant per DFHRESP row, in its order");
458    }
459
460    #[test]
461    fn every_default_abend_is_for_a_dfhresp_condition_and_listed_once() {
462        for (i, &(c, abend)) in DEFAULT_ABENDS.iter().enumerate() {
463            assert!(crate::cics_tables::resp(c.name()).is_some(), "{c:?}");
464            assert!(DEFAULT_ABENDS[..i].iter().all(|&(d, _)| d != c), "{c:?} is listed twice");
465            assert_eq!(c.default_abend(), abend);
466        }
467        assert_eq!(DEFAULT_ABENDS.len(), 37);
468        let codes = [Condition::QIDERR, Condition::ROLLEDBACK, Condition::FILENOTFOUND, Condition::INVREQ, Condition::INVMPSZ].map(Condition::default_abend);
469        assert_eq!(codes, ["AEYH", "AEXJ", "AEIL", "AEIP", "AEIP"]);
470    }
471
472    #[test]
473    fn dsiderr_is_filenotfound_by_its_older_name() {
474        let c = Condition::from_name("dsiderr").unwrap();
475        assert_eq!((c, c.name(), c.resp()), (Condition::FILENOTFOUND, "FILENOTFOUND", 12));
476        assert_eq!(Condition::from_name("NOSUCH"), None);
477    }
478
479    #[test]
480    fn parse_file_ksds() {
481        let (name, def) = parse_file("MYFILE=/tmp/data,KSDS,key=0:8,len=80").unwrap();
482        assert_eq!(name, "MYFILE");
483        assert_eq!(def.record_len, 80);
484        assert!(matches!(&def.data_set, DataSet::Ksds { key } if key.offset == 0 && key.len == 8));
485    }
486
487    #[test]
488    fn parse_file_rrds_text() {
489        let (name, def) = parse_file("myfile=/tmp/data,rrds,len=120,text").unwrap();
490        assert_eq!(name, "MYFILE");
491        assert_eq!(def.record_len, 120);
492        assert!(matches!(def.data_set, DataSet::Rrds));
493        assert_eq!(def.dd.format, Some(Format::Text));
494    }
495
496    #[test]
497    fn parse_file_errors() {
498        assert!(parse_file("F=/tmp/d,KSDS,len=80").is_err());
499        assert!(parse_file("F=/tmp/d,RRDS,len=abc").is_err());
500        assert!(parse_file("F=/tmp/d,FOO,len=80").is_err());
501    }
502
503    #[test]
504    fn ts_queue() {
505        let mut task = Task::default();
506        task.writeq_ts("Q1", None, b"one").unwrap();
507        task.writeq_ts("Q1", None, b"two").unwrap();
508        task.writeq_ts("Q1", None, b"three").unwrap();
509
510        let (data, count) = task.readq_ts("Q1", Some(2)).unwrap();
511        assert_eq!(data, b"two");
512        assert_eq!(count, 3);
513
514        let (data, count) = task.readq_ts("Q1", None).unwrap();
515        assert_eq!(data, b"three");
516        assert_eq!(count, 3);
517
518        assert_eq!(task.readq_ts("Q1", None), Err(Condition::ITEMERR));
519
520        task.writeq_ts("Q1", Some(1), b"ONE").unwrap();
521        let (data, _) = task.readq_ts("Q1", Some(1)).unwrap();
522        assert_eq!(data, b"ONE");
523
524        assert_eq!(task.writeq_ts("Q1", Some(9), b"x"), Err(Condition::ITEMERR));
525        assert_eq!(task.readq_ts("NOPE", None), Err(Condition::QIDERR));
526
527        task.deleteq_ts("Q1").unwrap();
528        assert_eq!(task.readq_ts("Q1", None), Err(Condition::QIDERR));
529    }
530
531    #[test]
532    fn td_queue() {
533        let mut task = Task::default();
534        task.writeq_td("Q1", b"first");
535        task.writeq_td("Q1", b"second");
536        task.writeq_td("Q1", b"third");
537
538        assert_eq!(task.readq_td("Q1").unwrap(), b"first");
539        assert_eq!(task.readq_td("Q1").unwrap(), b"second");
540        assert_eq!(task.readq_td("Q1").unwrap(), b"third");
541        assert_eq!(task.readq_td("Q1"), Err(Condition::QZERO));
542    }
543
544    #[test]
545    fn abstime_and_unix() {
546        assert_eq!(abstime(0, 0), 2_208_988_800_000);
547        assert_eq!(abstime(1, 50), 2_208_988_801_500);
548        assert_eq!(unix_seconds(abstime(1, 50)), 1);
549        assert_eq!(unix_seconds(abstime(0, 0)), 0);
550    }
551
552    #[test]
553    fn eib_date_and_time() {
554        // 2026-09-27 13:05:09 UTC
555        let at = abstime(1_790_514_309, 0);
556        assert_eq!(eib_date(at), 126_270);
557        assert_eq!(eib_time(at), 130_509);
558
559        // 1999-12-31 00:00:00 UTC
560        let at = abstime(946_598_400, 0);
561        assert_eq!(eib_date(at), 99_365);
562        assert_eq!(eib_time(at), 0);
563    }
564
565    #[test]
566    fn format_time_options() {
567        let at = abstime(1_790_514_309, 25);
568
569        assert_eq!(format_time(at, "YYYYMMDD", Some('/'), None), Some(FormatValue::Text("2026/09/27".into())));
570        assert_eq!(format_time(at, "MMDDYY", None, None), Some(FormatValue::Text("092726".into())));
571        assert_eq!(format_time(at, "YYDDD", Some('-'), None), Some(FormatValue::Text("26-270".into())));
572        assert_eq!(format_time(at, "TIME", None, Some(':')), Some(FormatValue::Text("13:05:09".into())));
573        assert_eq!(format_time(at, "DAYOFWEEK", None, None), Some(FormatValue::Number(0)));
574        assert_eq!(format_time(at, "DAYOFMONTH", None, None), Some(FormatValue::Number(27)));
575        assert_eq!(format_time(at, "MONTHOFYEAR", None, None), Some(FormatValue::Number(9)));
576        assert_eq!(format_time(at, "YEAR", None, None), Some(FormatValue::Number(2026)));
577        assert_eq!(format_time(at, "MILLISECONDS", None, None), Some(FormatValue::Number(250)));
578        assert_eq!(format_time(at, "DAYCOUNT", None, None), Some(FormatValue::Number(46_290)));
579        assert_eq!(format_time(at, "STRINGFORMAT", None, None), None);
580    }
581}