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, Sink, 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, level_ended};
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    /// The program activations the task has started, which number each one.
259    pub activations: u64,
260    /// The LINKs and HANDLE ABEND PROGRAM exits running, each a logical level below the task's
261    /// first; RETURN TRANSID and COMMAREA belong to that first level.
262    pub links: u32,
263    /// RETURN or XCTL has ended the logical level running: each program CALLed at it ends as its
264    /// CALL comes back (C233).
265    pub ending_level: bool,
266}
267
268impl Task {
269    /// The number of a program activation starting, which owns the HANDLE labels it sets.
270    pub fn next_activation(&mut self) -> u64 {
271        self.activations += 1;
272        self.activations
273    }
274
275    /// WRITEQ TS: appends and returns the new item's number; with Some(item) (REWRITE) replaces
276    /// that item and returns it.
277    pub fn writeq_ts(
278        &mut self,
279        queue: &str,
280        rewrite: Option<usize>,
281        data: &[u8],
282    ) -> Result<usize, Condition> {
283        let queue = queue.trim_end();
284        match rewrite {
285            None => {
286                let q = self
287                    .ts
288                    .entry(queue.to_string())
289                    .or_default();
290                q.items.push(data.to_vec());
291                Ok(q.items.len())
292            }
293            Some(item) => {
294                let q = self.ts.get_mut(queue).ok_or(Condition::QIDERR)?;
295                if item == 0 || item > q.items.len() {
296                    return Err(Condition::ITEMERR);
297                }
298                q.items[item - 1] = data.to_vec();
299                Ok(item)
300            }
301        }
302    }
303
304    /// READQ TS: Some(item) reads that item; None reads the next item after the last read.
305    pub fn readq_ts(
306        &mut self,
307        queue: &str,
308        item: Option<usize>,
309    ) -> Result<(Vec<u8>, usize), Condition> {
310        let queue = queue.trim_end();
311        let q = self.ts.get_mut(queue).ok_or(Condition::QIDERR)?;
312        let item_num = match item {
313            Some(n) => {
314                if n == 0 || n > q.items.len() {
315                    return Err(Condition::ITEMERR);
316                }
317                n
318            }
319            None => {
320                let n = if q.next == 0 { 1 } else { q.next + 1 };
321                if n > q.items.len() {
322                    return Err(Condition::ITEMERR);
323                }
324                n
325            }
326        };
327        let data = q.items[item_num - 1].clone();
328        q.next = item_num;
329        Ok((data, q.items.len()))
330    }
331
332    /// DELETEQ TS.
333    pub fn deleteq_ts(&mut self, queue: &str) -> Result<(), Condition> {
334        let queue = queue.trim_end();
335        self.ts.remove(queue).map(|_| ()).ok_or(Condition::QIDERR)
336    }
337
338    /// WRITEQ TD: appends (creating the queue).
339    pub fn writeq_td(&mut self, queue: &str, data: &[u8]) {
340        let queue = queue.trim_end();
341        self.td.entry(queue.to_string()).or_default().push(data.to_vec());
342    }
343
344    /// READQ TD: removes and returns the oldest item.
345    pub fn readq_td(&mut self, queue: &str) -> Result<Vec<u8>, Condition> {
346        let queue = queue.trim_end();
347        let items = self.td.get_mut(queue).ok_or(Condition::QZERO)?;
348        if items.is_empty() {
349            return Err(Condition::QZERO);
350        }
351        Ok(items.remove(0))
352    }
353
354    /// DELETEQ TD: empties the queue (no error when absent).
355    pub fn deleteq_td(&mut self, queue: &str) {
356        let queue = queue.trim_end();
357        if let Some(items) = self.td.get_mut(queue) {
358            items.clear();
359        }
360    }
361
362    /// Appends each TD queue that has a td_files entry to its file, one line per item, each item
363    /// decoded through `page` with trailing spaces trimmed; then empties those queues.
364    pub fn flush_td(&mut self, page: &zarch::ebcdic::CodePage) -> std::io::Result<()> {
365        let queues: Vec<String> = self.td_files.keys().cloned().collect();
366        for queue in &queues {
367            let Some(path) = self.td_files.get(queue) else { continue };
368            let items: Vec<Vec<u8>> = self
369                .td
370                .get(queue)
371                .cloned()
372                .unwrap_or_default();
373            if items.is_empty() {
374                continue;
375            }
376            let mut file = OpenOptions::new().create(true).append(true).open(path)?;
377            for item in &items {
378                let text = page.decode(item);
379                let line = format!("{}\n", text.trim_end());
380                file.write_all(line.as_bytes())?;
381            }
382            if let Some(q) = self.td.get_mut(queue) {
383                q.clear();
384            }
385        }
386        Ok(())
387    }
388}
389
390/// Milliseconds from 1900-01-01T00:00:00 to a Unix time given as seconds and hundredths.
391pub fn abstime(seconds: i64, hundredths: u32) -> i64 {
392    EPOCH_1900_TO_1970_MILLIS + seconds * 1000 + hundredths as i64 * 10
393}
394
395/// Unix seconds (rounded down) of an ABSTIME.
396pub fn unix_seconds(abstime: i64) -> i64 {
397    (abstime - EPOCH_1900_TO_1970_MILLIS).div_euclid(1000)
398}
399
400/// EIBDATE's value for an ABSTIME: 0CYYDDD as a decimal number.
401pub fn eib_date(abstime: i64) -> i64 {
402    let secs = unix_seconds(abstime);
403    let date = civil(secs);
404    let c = if date.year >= 2000 { 1 } else { 0 };
405    let yy = date.year % 100;
406    c * 100_000 + yy * 1_000 + i64::from(date.day_of_year)
407}
408
409/// EIBTIME's value for an ABSTIME: 0HHMMSS as a decimal number.
410pub fn eib_time(abstime: i64) -> i64 {
411    let secs = unix_seconds(abstime);
412    let time = civil(secs);
413    i64::from(time.hour) * 10_000 + i64::from(time.minute) * 100 + i64::from(time.second)
414}
415
416/// A FORMATTIME output: text for the date and time forms, a number for the counts.
417#[derive(Clone, Debug, PartialEq, Eq)]
418pub enum FormatValue {
419    Text(String),
420    Number(i64),
421}
422
423/// One FORMATTIME option's value for an ABSTIME, or None for an option this does not produce.
424/// `datesep` and `timesep` are the separators when DATESEP / TIMESEP were given.
425pub fn format_time(abstime: i64, option: &str, datesep: Option<char>, timesep: Option<char>) -> Option<FormatValue> {
426    let secs = unix_seconds(abstime);
427    let c = civil(secs);
428    let (year, month, day, hour, minute, second, doy) = (c.year, c.month, c.day, c.hour, c.minute, c.second, c.day_of_year);
429
430    let yy = format!("{:02}", year % 100);
431    let yyyy = format!("{:04}", year);
432    let mm = format!("{:02}", month);
433    let dd = format!("{:02}", day);
434    let hh = format!("{:02}", hour);
435    let mi = format!("{:02}", minute);
436    let ss = format!("{:02}", second);
437    let ddd = format!("{:03}", doy);
438
439    let join = |parts: &[&str], sep: Option<char>| match sep {
440        Some(c) => {
441            let mut s = String::new();
442            for (i, p) in parts.iter().enumerate() {
443                if i > 0 {
444                    s.push(c);
445                }
446                s.push_str(p);
447            }
448            s
449        }
450        None => parts.concat(),
451    };
452
453    let option = option.to_ascii_uppercase();
454
455    match option.as_str() {
456        "YYYYMMDD" => Some(FormatValue::Text(join(&[&yyyy, &mm, &dd], datesep))),
457        "YYMMDD" => Some(FormatValue::Text(join(&[&yy, &mm, &dd], datesep))),
458        "YYDDMM" => Some(FormatValue::Text(join(&[&yy, &dd, &mm], datesep))),
459        "YYYYDDMM" => Some(FormatValue::Text(join(&[&yyyy, &dd, &mm], datesep))),
460        "DDMMYY" => Some(FormatValue::Text(join(&[&dd, &mm, &yy], datesep))),
461        "DDMMYYYY" => Some(FormatValue::Text(join(&[&dd, &mm, &yyyy], datesep))),
462        "MMDDYY" | "DATE" => Some(FormatValue::Text(join(&[&mm, &dd, &yy], datesep))),
463        "MMDDYYYY" | "FULLDATE" => Some(FormatValue::Text(join(&[&mm, &dd, &yyyy], datesep))),
464        "YYDDD" => Some(FormatValue::Text(join(&[&yy, &ddd], datesep))),
465        "YYYYDDD" => Some(FormatValue::Text(join(&[&yyyy, &ddd], datesep))),
466        "DATEFORM" => Some(FormatValue::Text("MMDDYY".to_string())),
467        "TIME" => Some(FormatValue::Text(join(&[&hh, &mi, &ss], timesep))),
468        "DAYCOUNT" => Some(FormatValue::Number((secs + EPOCH_1900_TO_1970_SECONDS) / SECONDS_PER_DAY)),
469        "DAYOFWEEK" => Some(FormatValue::Number(c.cics_weekday())),
470        "DAYOFMONTH" => Some(FormatValue::Number(day as i64)),
471        "MONTHOFYEAR" => Some(FormatValue::Number(month as i64)),
472        "YEAR" => Some(FormatValue::Number(year)),
473        "MILLISECONDS" => Some(FormatValue::Number(abstime.rem_euclid(1000))),
474        _ => None,
475    }
476}
477
478#[cfg(test)]
479mod tests {
480    use super::*;
481
482    #[test]
483    fn every_condition_is_a_dfhresp_row_with_its_number() {
484        for &c in Condition::ALL {
485            assert_eq!(Some(c.resp()), crate::cics_tables::resp(c.name()), "{c:?}");
486            assert_eq!(Condition::from_name(c.name()), Some(c));
487        }
488        let rows: Vec<&str> = crate::cics_tables::resp_table().iter().map(|r| r.condition).collect();
489        let names: Vec<&str> = Condition::ALL.iter().map(|c| c.name()).collect();
490        assert_eq!(names, rows, "one variant per DFHRESP row, in its order");
491    }
492
493    #[test]
494    fn every_default_abend_is_for_a_dfhresp_condition_and_listed_once() {
495        for (i, &(c, abend)) in DEFAULT_ABENDS.iter().enumerate() {
496            assert!(crate::cics_tables::resp(c.name()).is_some(), "{c:?}");
497            assert!(DEFAULT_ABENDS[..i].iter().all(|&(d, _)| d != c), "{c:?} is listed twice");
498            assert_eq!(c.default_abend(), abend);
499        }
500        assert_eq!(DEFAULT_ABENDS.len(), 37);
501        let codes = [Condition::QIDERR, Condition::ROLLEDBACK, Condition::FILENOTFOUND, Condition::INVREQ, Condition::INVMPSZ].map(Condition::default_abend);
502        assert_eq!(codes, ["AEYH", "AEXJ", "AEIL", "AEIP", "AEIP"]);
503    }
504
505    #[test]
506    fn dsiderr_is_filenotfound_by_its_older_name() {
507        let c = Condition::from_name("dsiderr").unwrap();
508        assert_eq!((c, c.name(), c.resp()), (Condition::FILENOTFOUND, "FILENOTFOUND", 12));
509        assert_eq!(Condition::from_name("NOSUCH"), None);
510    }
511
512    #[test]
513    fn parse_file_ksds() {
514        let (name, def) = parse_file("MYFILE=/tmp/data,KSDS,key=0:8,len=80").unwrap();
515        assert_eq!(name, "MYFILE");
516        assert_eq!(def.record_len, 80);
517        assert!(matches!(&def.data_set, DataSet::Ksds { key } if key.offset == 0 && key.len == 8));
518    }
519
520    #[test]
521    fn parse_file_rrds_text() {
522        let (name, def) = parse_file("myfile=/tmp/data,rrds,len=120,text").unwrap();
523        assert_eq!(name, "MYFILE");
524        assert_eq!(def.record_len, 120);
525        assert!(matches!(def.data_set, DataSet::Rrds));
526        assert_eq!(def.dd.format, Some(Format::Text));
527    }
528
529    #[test]
530    fn parse_file_errors() {
531        assert!(parse_file("F=/tmp/d,KSDS,len=80").is_err());
532        assert!(parse_file("F=/tmp/d,RRDS,len=abc").is_err());
533        assert!(parse_file("F=/tmp/d,FOO,len=80").is_err());
534    }
535
536    #[test]
537    fn ts_queue() {
538        let mut task = Task::default();
539        task.writeq_ts("Q1", None, b"one").unwrap();
540        task.writeq_ts("Q1", None, b"two").unwrap();
541        task.writeq_ts("Q1", None, b"three").unwrap();
542
543        let (data, count) = task.readq_ts("Q1", Some(2)).unwrap();
544        assert_eq!(data, b"two");
545        assert_eq!(count, 3);
546
547        let (data, count) = task.readq_ts("Q1", None).unwrap();
548        assert_eq!(data, b"three");
549        assert_eq!(count, 3);
550
551        assert_eq!(task.readq_ts("Q1", None), Err(Condition::ITEMERR));
552
553        task.writeq_ts("Q1", Some(1), b"ONE").unwrap();
554        let (data, _) = task.readq_ts("Q1", Some(1)).unwrap();
555        assert_eq!(data, b"ONE");
556
557        assert_eq!(task.writeq_ts("Q1", Some(9), b"x"), Err(Condition::ITEMERR));
558        assert_eq!(task.readq_ts("NOPE", None), Err(Condition::QIDERR));
559
560        task.deleteq_ts("Q1").unwrap();
561        assert_eq!(task.readq_ts("Q1", None), Err(Condition::QIDERR));
562    }
563
564    #[test]
565    fn td_queue() {
566        let mut task = Task::default();
567        task.writeq_td("Q1", b"first");
568        task.writeq_td("Q1", b"second");
569        task.writeq_td("Q1", b"third");
570
571        assert_eq!(task.readq_td("Q1").unwrap(), b"first");
572        assert_eq!(task.readq_td("Q1").unwrap(), b"second");
573        assert_eq!(task.readq_td("Q1").unwrap(), b"third");
574        assert_eq!(task.readq_td("Q1"), Err(Condition::QZERO));
575    }
576
577    #[test]
578    fn abstime_and_unix() {
579        assert_eq!(abstime(0, 0), 2_208_988_800_000);
580        assert_eq!(abstime(1, 50), 2_208_988_801_500);
581        assert_eq!(unix_seconds(abstime(1, 50)), 1);
582        assert_eq!(unix_seconds(abstime(0, 0)), 0);
583    }
584
585    #[test]
586    fn eib_date_and_time() {
587        // 2026-09-27 13:05:09 UTC
588        let at = abstime(1_790_514_309, 0);
589        assert_eq!(eib_date(at), 126_270);
590        assert_eq!(eib_time(at), 130_509);
591
592        // 1999-12-31 00:00:00 UTC
593        let at = abstime(946_598_400, 0);
594        assert_eq!(eib_date(at), 99_365);
595        assert_eq!(eib_time(at), 0);
596    }
597
598    #[test]
599    fn format_time_options() {
600        let at = abstime(1_790_514_309, 25);
601
602        assert_eq!(format_time(at, "YYYYMMDD", Some('/'), None), Some(FormatValue::Text("2026/09/27".into())));
603        assert_eq!(format_time(at, "MMDDYY", None, None), Some(FormatValue::Text("092726".into())));
604        assert_eq!(format_time(at, "YYDDD", Some('-'), None), Some(FormatValue::Text("26-270".into())));
605        assert_eq!(format_time(at, "TIME", None, Some(':')), Some(FormatValue::Text("13:05:09".into())));
606        assert_eq!(format_time(at, "DAYOFWEEK", None, None), Some(FormatValue::Number(0)));
607        assert_eq!(format_time(at, "DAYOFMONTH", None, None), Some(FormatValue::Number(27)));
608        assert_eq!(format_time(at, "MONTHOFYEAR", None, None), Some(FormatValue::Number(9)));
609        assert_eq!(format_time(at, "YEAR", None, None), Some(FormatValue::Number(2026)));
610        assert_eq!(format_time(at, "MILLISECONDS", None, None), Some(FormatValue::Number(250)));
611        assert_eq!(format_time(at, "DAYCOUNT", None, None), Some(FormatValue::Number(46_290)));
612        assert_eq!(format_time(at, "STRINGFORMAT", None, None), None);
613    }
614}