Skip to main content

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