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