use chrono::{DateTime, Local};
use clap::{CommandFactory, FromArgMatches, Parser};
use epics_base_rs::server::snapshot::{DbrClass, Snapshot};
use epics_base_rs::types::{DBR_CLASS_NAME, WallTime};
use epics_ca_rs::cli::{
FloatFormat, FloatStyle, IntStyle, PV_NAME_WIDTH, ValueFormat, format_value,
};
use epics_ca_rs::client::{
CaClient, ReqCount, enum_cli_readback_dbr, float_as_string_readback_dbr,
};
use epics_ca_rs::{CaError, DbFieldType, EpicsValue};
use std::time::SystemTime;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum OutputMode {
Plain,
Terse,
All,
SpecifiedDbr,
}
fn resolve_output_mode(matches: &clap::ArgMatches) -> OutputMode {
use clap::parser::ValueSource;
let mut opts: Vec<(usize, OutputMode)> = Vec::new();
for (id, m) in [
("terse", OutputMode::Terse),
("wide", OutputMode::All),
("dbr_type", OutputMode::SpecifiedDbr),
] {
if matches.value_source(id) == Some(ValueSource::CommandLine)
&& let Some(i) = matches.index_of(id)
{
opts.push((i, m));
}
}
opts.sort_by_key(|&(i, _)| i);
let mut format = OutputMode::Plain;
for (_, requested) in opts {
if format != OutputMode::Plain {
eprintln!("Options t,d,a are mutually exclusive. ('caget -h' for help.)");
}
format = requested;
}
format
}
const VERSION_INFO: &str = concat!(
"\nEPICS Version epics-rs ",
env!("CARGO_PKG_VERSION"),
", CA Protocol version 4.13"
);
#[derive(Parser)]
#[command(
name = "caget-rs",
about = "Read EPICS PV values",
disable_version_flag = true
)]
struct Args {
#[arg(short = 'V', long, hide = true)]
version: bool,
#[arg(short = 'w', long = "wait")]
timeout: Option<f64>,
#[arg(short = 'c', long)]
callback: bool,
#[arg(short = 'p', long)]
priority: Option<u8>,
#[arg(short = 't', long)]
terse: bool,
#[arg(short = 'a', long)]
wide: bool,
#[arg(short = 'd', long = "dbr-type")]
dbr_type: Option<String>,
#[arg(short = 'n', long = "num-enum")]
enum_as_number: bool,
#[arg(short = '#', long = "max-elements", value_name = "COUNT")]
max_elements: Option<usize>,
#[arg(short = 'S', long = "char-as-string")]
char_array_as_string: bool,
#[arg(short = 'e', long = "format-e", value_name = "PRECISION")]
fmt_e: Option<u32>,
#[arg(short = 'f', long = "format-f", value_name = "PRECISION")]
fmt_f: Option<u32>,
#[arg(short = 'g', long = "format-g", value_name = "PRECISION")]
fmt_g: Option<u32>,
#[arg(short = 's', long = "string-format")]
string_format: bool,
#[arg(long = "lx", conflicts_with_all = ["lo_flag", "lb_flag", "ix_flag", "io_flag", "ib_flag"])]
lx_flag: bool,
#[arg(long = "lo", conflicts_with_all = ["lx_flag", "lb_flag", "ix_flag", "io_flag", "ib_flag"])]
lo_flag: bool,
#[arg(long = "lb", conflicts_with_all = ["lx_flag", "lo_flag", "ix_flag", "io_flag", "ib_flag"])]
lb_flag: bool,
#[arg(long = "0x", conflicts_with_all = ["io_flag", "ib_flag"])]
ix_flag: bool,
#[arg(long = "0o", conflicts_with_all = ["ix_flag", "ib_flag"])]
io_flag: bool,
#[arg(long = "0b", conflicts_with_all = ["ix_flag", "io_flag"])]
ib_flag: bool,
#[arg(short = 'F', long = "field-separator", value_name = "OFS")]
field_separator: Option<char>,
#[arg(required_unless_present_any = ["version"])]
pv_names: Vec<String>,
}
impl Args {
fn value_format(&self) -> ValueFormat {
let mut fmt = ValueFormat::default();
if let Some(p) = self.fmt_e {
fmt.float = FloatFormat {
style: FloatStyle::E,
precision: p,
};
} else if let Some(p) = self.fmt_f {
fmt.float = FloatFormat {
style: FloatStyle::F,
precision: p,
};
} else if let Some(p) = self.fmt_g {
fmt.float = FloatFormat {
style: FloatStyle::G,
precision: p,
};
}
if self.ix_flag || self.lx_flag {
fmt.int_style = IntStyle::Hex;
} else if self.io_flag || self.lo_flag {
fmt.int_style = IntStyle::Oct;
} else if self.ib_flag || self.lb_flag {
fmt.int_style = IntStyle::Bin;
}
fmt.float_as_int = self.lx_flag || self.lo_flag || self.lb_flag;
fmt.enum_as_number = self.enum_as_number;
fmt.char_array_as_string = self.char_array_as_string;
fmt.max_elements = self.max_elements;
if let Some(c) = self.field_separator {
fmt.field_separator = c;
}
fmt
}
}
enum GetResult {
Plain(EpicsValue),
Time(Box<Snapshot>),
Specified {
native: Option<DbFieldType>,
req_type: u16,
snap: Box<Snapshot>,
},
}
fn format_server_timestamp(ts: WallTime) -> String {
let dt: DateTime<Local> = SystemTime::from(ts).into();
dt.format("%Y-%m-%d %H:%M:%S%.6f").to_string()
}
fn sevr_to_str(sevr: u16) -> &'static str {
match sevr {
0 => "NO_ALARM",
1 => "MINOR",
2 => "MAJOR",
3 => "INVALID",
_ => "Illegal value",
}
}
fn stat_to_str(stat: u16) -> &'static str {
match stat {
0 => "NO_ALARM",
1 => "READ",
2 => "WRITE",
3 => "HIHI",
4 => "HIGH",
5 => "LOLO",
6 => "LOW",
7 => "STATE",
8 => "COS",
9 => "COMM",
10 => "TIMEOUT",
11 => "HW_LIMIT",
12 => "CALC",
13 => "SCAN",
14 => "LINK",
15 => "SOFT",
16 => "BAD_SUB",
17 => "UDF",
18 => "DISABLE",
19 => "SIMM",
20 => "READ_ACCESS",
21 => "WRITE_ACCESS",
_ => "Illegal value",
}
}
fn dbf_text(t: DbFieldType) -> &'static str {
match t {
DbFieldType::String => "DBF_STRING",
DbFieldType::Short => "DBF_SHORT",
DbFieldType::Float => "DBF_FLOAT",
DbFieldType::Enum => "DBF_ENUM",
DbFieldType::Char => "DBF_CHAR",
DbFieldType::Long => "DBF_LONG",
DbFieldType::Double => "DBF_DOUBLE",
DbFieldType::Int64 => "DBF_INT64",
DbFieldType::UInt64 => "DBF_UINT64",
DbFieldType::UShort => "DBF_USHORT",
DbFieldType::ULong => "DBF_ULONG",
}
}
fn dbr_text(code: u16) -> &'static str {
const NAMES: [&str; 39] = [
"DBR_STRING",
"DBR_SHORT",
"DBR_FLOAT",
"DBR_ENUM",
"DBR_CHAR",
"DBR_LONG",
"DBR_DOUBLE",
"DBR_STS_STRING",
"DBR_STS_SHORT",
"DBR_STS_FLOAT",
"DBR_STS_ENUM",
"DBR_STS_CHAR",
"DBR_STS_LONG",
"DBR_STS_DOUBLE",
"DBR_TIME_STRING",
"DBR_TIME_SHORT",
"DBR_TIME_FLOAT",
"DBR_TIME_ENUM",
"DBR_TIME_CHAR",
"DBR_TIME_LONG",
"DBR_TIME_DOUBLE",
"DBR_GR_STRING",
"DBR_GR_SHORT",
"DBR_GR_FLOAT",
"DBR_GR_ENUM",
"DBR_GR_CHAR",
"DBR_GR_LONG",
"DBR_GR_DOUBLE",
"DBR_CTRL_STRING",
"DBR_CTRL_SHORT",
"DBR_CTRL_FLOAT",
"DBR_CTRL_ENUM",
"DBR_CTRL_CHAR",
"DBR_CTRL_LONG",
"DBR_CTRL_DOUBLE",
"DBR_PUT_ACKT",
"DBR_PUT_ACKS",
"DBR_STSACK_STRING",
"DBR_CLASS_NAME",
];
NAMES.get(code as usize).copied().unwrap_or("DBR_invalid")
}
fn dbr_extended_str(req_type: u16, snap: &Snapshot) -> String {
if req_type <= 6 {
return String::new();
}
let stat = snap.alarm.status;
let sevr = snap.alarm.severity;
let sts = format!(
" Status: {}\n Severity: {}",
stat_to_str(stat),
sevr_to_str(sevr)
);
match req_type {
7..=13 | 21 | 28 => sts,
14..=20 => format!(
" Timestamp: {}\n{sts}",
format_server_timestamp(snap.timestamp)
),
24 | 31 => {
let labels = snap
.enums
.as_ref()
.map(|e| e.strings.as_slice())
.unwrap_or(&[]);
let mut out = sts;
out.push_str(&format!("\n Enums: ({:2})", labels.len()));
for (i, label) in labels.iter().enumerate() {
out.push_str(&format!("\n [{i:2}] {label}"));
}
out
}
22 | 23 | 25 | 26 | 27 | 29 | 30 | 32 | 33 | 34 => {
let is_ctrl = req_type >= 28;
let is_float = matches!(req_type, 23 | 27 | 30 | 34); let is_int = matches!(req_type, 22 | 25 | 26 | 29 | 32 | 33); let d = snap.display.clone().unwrap_or_default();
let lim = |v: f64| -> String {
if is_int {
format!("{:8}", v as i64)
} else {
format!("{v}")
}
};
let mut out = sts;
out.push_str(&format!("\n Units: {}", d.units));
if is_float {
out.push_str(&format!("\n Precision: {}", d.precision));
}
out.push_str(&format!(
"\n Lo disp limit: {}",
lim(d.lower_disp_limit)
));
out.push_str(&format!(
"\n Hi disp limit: {}",
lim(d.upper_disp_limit)
));
out.push_str(&format!(
"\n Lo alarm limit: {}",
lim(d.lower_alarm_limit)
));
out.push_str(&format!(
"\n Lo warn limit: {}",
lim(d.lower_warning_limit)
));
out.push_str(&format!(
"\n Hi warn limit: {}",
lim(d.upper_warning_limit)
));
out.push_str(&format!(
"\n Hi alarm limit: {}",
lim(d.upper_alarm_limit)
));
if is_ctrl {
let c = snap.control.clone().unwrap_or_default();
out.push_str(&format!(
"\n Lo ctrl limit: {}",
lim(c.lower_ctrl_limit)
));
out.push_str(&format!(
"\n Hi ctrl limit: {}",
lim(c.upper_ctrl_limit)
));
}
out
}
37 => {
let ackt = snap.alarm.ackt.unwrap_or(0);
let acks = snap.alarm.acks.unwrap_or(0);
format!(
"{sts}\n Ack transient?: {}\n Ack severity: {}",
if ackt != 0 { "YES" } else { "NO" },
sevr_to_str(acks)
)
}
_ => String::new(),
}
}
fn specified_dbr_report(
pv_name: &str,
native: Option<DbFieldType>,
req_type: u16,
snap: &Snapshot,
fmt: &ValueFormat,
) -> String {
use std::fmt::Write;
let mut out = String::new();
let _ = writeln!(out, "{pv_name}");
let _ = writeln!(
out,
" Native data type: {}",
native.map(dbf_text).unwrap_or("DBF_NO_ACCESS")
);
let _ = writeln!(out, " Request type: {}", dbr_text(req_type));
if req_type == DBR_CLASS_NAME {
let cn = snap
.class_name
.clone()
.or_else(|| match &snap.value {
EpicsValue::String(s) => Some(s.as_str_lossy().into_owned()),
_ => None,
})
.unwrap_or_default();
let _ = writeln!(out, " Class Name: {cn}");
} else {
let enum_strings = snap.enums.as_ref().map(|e| e.strings.as_slice());
let rendered = format_value(&snap.value, fmt, enum_strings, false);
let _ = writeln!(out, " Element count: {}", snap.value.count());
let _ = writeln!(out, " Value: {rendered}");
let ext = dbr_extended_str(req_type, snap);
if !ext.is_empty() {
let _ = writeln!(out, "{ext}");
}
}
out
}
fn caget_req_count(callback: bool, max_elements: Option<usize>, native: u32) -> ReqCount {
let count = max_elements.map_or(0, |n| (n as u32).min(native));
if callback {
ReqCount::Autosize(count)
} else {
ReqCount::Fixed(count)
}
}
#[tokio::main]
async fn main() {
let matches = Args::command().get_matches();
let args = Args::from_arg_matches(&matches).expect("clap validated the arguments");
if args.version {
println!("{VERSION_INFO}");
return;
}
if args.callback {
}
let client = CaClient::new().await.expect("failed to create CA client");
let timeout = epics_ca_rs::cli::timeout_duration(
args.timeout
.unwrap_or_else(epics_ca_rs::cli::env_default_timeout),
);
let priority = args.priority.unwrap_or(0);
let channels: Vec<_> = args
.pv_names
.iter()
.map(|name| {
(
name.clone(),
client.create_channel_with_priority(name, priority),
)
})
.collect();
let enum_as_number = args.enum_as_number;
let float_as_string = args.string_format;
let req_dbr_type: Option<u16> = match args.dbr_type.as_deref() {
Some(s) => {
let t = parse_dbr_type(s);
if t.is_none() {
eprintln!(
"Requested dbr type out of range or invalid - ignored. \
('caget -h' for help.)"
);
}
t
}
None => None,
};
let mut mode = resolve_output_mode(&matches);
if mode == OutputMode::SpecifiedDbr && req_dbr_type.is_none() {
mode = OutputMode::Plain;
}
let want_time = mode == OutputMode::All;
let max_elements = args.max_elements;
let callback = args.callback;
let mut handles = Vec::new();
for (name, ch) in &channels {
let name = name.clone();
let t = timeout;
let ch = ch.clone();
handles.push(tokio::spawn(async move {
let connect = ch.wait_connected(t).await;
if connect.is_err() {
return (name, Err("not connected".to_string()));
}
let native = ch.element_count().unwrap_or(0);
let req_count = caget_req_count(callback, max_elements, native);
let outcome = if mode == OutputMode::SpecifiedDbr {
let rt = req_dbr_type
.expect("specifiedDbr mode implies a resolved -d type (else reverts to plain)");
let native = ch.native_field_type().ok();
match tokio::time::timeout(t, ch.get_with_dbr_type(rt, req_count)).await {
Ok(Ok(snap)) => Ok(GetResult::Specified {
native,
req_type: rt,
snap: Box::new(snap),
}),
Ok(Err(CaError::Timeout)) => Err("timeout".to_string()),
Ok(Err(e)) => Err(format!("{e}")),
Err(_) => Err("timeout".to_string()),
}
} else {
let nt = ch.native_field_type().ok();
let sub_dbr = nt
.and_then(|nt| enum_cli_readback_dbr(nt, enum_as_number))
.or_else(|| {
float_as_string
.then(|| nt.and_then(float_as_string_readback_dbr))
.flatten()
});
if let Some(rt) = sub_dbr {
match tokio::time::timeout(t, ch.get_with_dbr_type(rt, req_count)).await {
Ok(Ok(snap)) => Ok(if want_time {
GetResult::Time(Box::new(snap))
} else {
GetResult::Plain(snap.value)
}),
Ok(Err(CaError::Timeout)) => Err("timeout".to_string()),
Ok(Err(e)) => Err(format!("{e}")),
Err(_) => Err("timeout".to_string()),
}
} else if want_time {
match tokio::time::timeout(
t,
ch.get_with_metadata_count(DbrClass::Time, req_count),
)
.await
{
Ok(Ok(snap)) => Ok(GetResult::Time(Box::new(snap))),
Ok(Err(CaError::Timeout)) => Err("timeout".to_string()),
Ok(Err(e)) => Err(format!("{e}")),
Err(_) => Err("timeout".to_string()),
}
} else {
match ch.get_with_timeout_count(t, req_count).await {
Ok((_dbr, value)) => Ok(GetResult::Plain(value)),
Err(CaError::Timeout) => Err("timeout".to_string()),
Err(e) => Err(format!("{e}")),
}
}
};
(name, outcome)
}));
}
let mut results = Vec::with_capacity(handles.len());
for h in handles {
results.push(h.await.unwrap());
}
let fmt = args.value_format();
let sep = fmt.field_separator;
let req_elems_present = args.max_elements.is_some();
let pad_name = |is_scalar: bool, name: &str| -> String {
if is_scalar && sep == ' ' {
format!("{name:<width$}", width = PV_NAME_WIDTH)
} else {
name.to_string()
}
};
let mut failed = false;
for (pv_name, result) in &results {
match result {
Ok(GetResult::Plain(value)) => {
let rendered = format_value(value, &fmt, None, req_elems_present);
let is_scalar = value.count() == 1;
if mode == OutputMode::Terse {
println!("{rendered}");
} else {
println!("{}{}{}", pad_name(is_scalar, pv_name), sep, rendered);
}
}
Ok(GetResult::Time(snap)) => {
let enum_strings = snap.enums.as_ref().map(|e| e.strings.as_slice());
let rendered = format_value(&snap.value, &fmt, enum_strings, req_elems_present);
let is_scalar = snap.value.count() == 1;
let ts = format_server_timestamp(snap.timestamp);
let stat = snap.alarm.status;
let sevr = snap.alarm.severity;
if stat == 0 && sevr == 0 {
println!(
"{name}{sep}{ts}{sep}{val}{sep}{sep}",
name = pad_name(is_scalar, pv_name),
sep = sep,
val = rendered,
);
} else {
println!(
"{name}{sep}{ts}{sep}{val}{sep}{stat_str}{sep}{sevr_str}",
name = pad_name(is_scalar, pv_name),
sep = sep,
val = rendered,
stat_str = stat_to_str(stat),
sevr_str = sevr_to_str(sevr),
);
}
}
Ok(GetResult::Specified {
native,
req_type,
snap,
}) => {
print!(
"{}",
specified_dbr_report(pv_name, *native, *req_type, snap, &fmt)
);
}
Err(e) if e.contains("not connected") || e.contains("isconnect") => {
match mode {
OutputMode::All => println!(
"{}{}*** Not connected (PV not found)",
pad_name(true, pv_name),
sep
),
OutputMode::Terse => println!("*** not connected"),
OutputMode::SpecifiedDbr => println!("{pv_name}\n *** not connected"),
OutputMode::Plain => {
println!("{}{}*** not connected", pad_name(true, pv_name), sep)
}
}
failed = true;
}
Err(e) if e.contains("timeout") => {
match mode {
OutputMode::Terse => println!("*** no data available (timeout)"),
OutputMode::SpecifiedDbr => {
println!("{pv_name}\n *** no data available (timeout)")
}
_ => println!(
"{}{}*** no data available (timeout)",
pad_name(true, pv_name),
sep
),
}
}
Err(e) => {
println!(
"{}{}*** no data available ({e})",
pad_name(true, pv_name),
sep
);
failed = true;
}
}
}
if failed {
std::process::exit(1);
}
}
fn scan_leading_i64(s: &str) -> Option<i64> {
let s = s.trim_start();
let bytes = s.as_bytes();
let mut i = 0;
let mut neg = false;
if let Some(&c) = bytes.first()
&& (c == b'+' || c == b'-')
{
neg = c == b'-';
i = 1;
}
let start = i;
while i < bytes.len() && bytes[i].is_ascii_digit() {
i += 1;
}
if i == start {
return None;
}
s[start..i]
.parse::<i64>()
.ok()
.map(|n| if neg { -n } else { n })
}
fn parse_dbr_type(s: &str) -> Option<u16> {
let s = s.trim();
let resolved: Option<i64> = if let Some(n) = scan_leading_i64(s) {
Some(n)
} else {
epics_base_rs::types::dbr_text_to_type(s)
.or_else(|| epics_base_rs::types::dbr_text_to_type(&format!("DBR_{s}")))
.map(i64::from)
};
match resolved {
Some(t) if (0..=38).contains(&t) && t != 35 && t != 36 => Some(t as u16),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::{
Args, OutputMode, ReqCount, caget_req_count, dbr_extended_str, dbr_text, parse_dbr_type,
resolve_output_mode, scan_leading_i64, specified_dbr_report,
};
use clap::CommandFactory;
use epics_base_rs::server::snapshot::{ControlInfo, DisplayInfo, EnumInfo, Snapshot};
use epics_base_rs::types::{
DBR_CLASS_NAME, DBR_CTRL_DOUBLE, DBR_CTRL_STRING, DBR_DOUBLE, DBR_GR_STRING, DBR_STRING,
DBR_STSACK_STRING, DBR_TIME_DOUBLE, DBR_TIME_FLOAT,
};
use epics_ca_rs::EpicsValue;
use epics_ca_rs::cli::ValueFormat;
use std::time::SystemTime;
fn mode_of(argv: &[&str]) -> OutputMode {
let m = Args::command().get_matches_from(argv);
resolve_output_mode(&m)
}
#[test]
fn caget_req_count_callback_preserves_autosize() {
let native = 5u32;
let wire =
|callback, max: Option<usize>| caget_req_count(callback, max, native).resolve(native);
assert_eq!(wire(false, None), native, "sync, no -#");
assert_eq!(wire(false, Some(0)), native, "sync, -# 0");
assert_eq!(wire(false, Some(3)), 3, "sync, -# 3");
assert_eq!(wire(false, Some(9)), native, "sync, -# > native clamps");
assert_eq!(wire(true, None), 0, "callback, no -# => autosize 0");
assert_eq!(wire(true, Some(0)), 0, "callback, -# 0 => autosize 0");
assert_eq!(wire(true, Some(3)), 3, "callback, -# 3");
assert_eq!(wire(true, Some(9)), native, "callback, -# > native clamps");
assert_eq!(caget_req_count(true, None, native), ReqCount::Autosize(0));
assert_eq!(caget_req_count(false, None, native), ReqCount::Fixed(0));
}
#[test]
fn output_mode_resolves_in_command_line_order() {
assert_eq!(mode_of(&["caget", "PV"]), OutputMode::Plain);
assert_eq!(mode_of(&["caget", "-t", "PV"]), OutputMode::Terse);
assert_eq!(mode_of(&["caget", "-a", "PV"]), OutputMode::All);
assert_eq!(
mode_of(&["caget", "-d", "DBR_TIME_DOUBLE", "PV"]),
OutputMode::SpecifiedDbr
);
assert_eq!(
mode_of(&["caget", "-a", "-d", "DBR_TIME_DOUBLE", "PV"]),
OutputMode::SpecifiedDbr
);
assert_eq!(
mode_of(&["caget", "-d", "DBR_TIME_DOUBLE", "-a", "PV"]),
OutputMode::All
);
}
fn ctrl_double_snap() -> Snapshot {
let mut s = Snapshot::new(EpicsValue::Double(1.5), 0, 0, SystemTime::UNIX_EPOCH);
s.display = Some(DisplayInfo {
units: "mm".into(),
precision: 3,
upper_disp_limit: 10.0,
lower_disp_limit: -10.0,
upper_alarm_limit: 8.0,
upper_warning_limit: 6.0,
lower_warning_limit: -6.0,
lower_alarm_limit: -8.0,
..Default::default()
});
s.control = Some(ControlInfo {
upper_ctrl_limit: 9.0,
lower_ctrl_limit: -9.0,
});
s
}
#[test]
fn specified_report_ctrl_double_has_full_metadata_block() {
let snap = ctrl_double_snap();
let out = specified_dbr_report(
"ai:temp",
Some(DbFieldType::Double),
DBR_CTRL_DOUBLE,
&snap,
&ValueFormat::default(),
);
assert!(out.starts_with("ai:temp\n"), "{out}");
assert!(out.contains(" Native data type: DBF_DOUBLE\n"), "{out}");
assert!(
out.contains(" Request type: DBR_CTRL_DOUBLE\n"),
"{out}"
);
assert!(out.contains(" Element count: 1\n"), "{out}");
assert!(out.contains(" Value: 1.5\n"), "{out}");
assert!(out.contains(" Units: mm\n"), "{out}");
assert!(out.contains(" Precision: 3\n"), "{out}");
assert!(out.contains(" Lo ctrl limit: -9\n"), "{out}");
assert!(out.contains(" Hi ctrl limit: 9\n"), "{out}");
}
#[test]
fn specified_report_class_name_prints_class_line_only() {
let mut snap = Snapshot::new(
EpicsValue::String("ai".into()),
0,
0,
SystemTime::UNIX_EPOCH,
);
snap.class_name = Some("ai".to_string());
let out = specified_dbr_report(
"ai:temp",
Some(DbFieldType::Double),
DBR_CLASS_NAME,
&snap,
&ValueFormat::default(),
);
assert!(
out.contains(" Request type: DBR_CLASS_NAME\n"),
"{out}"
);
assert!(out.contains(" Class Name: ai\n"), "{out}");
assert!(!out.contains("Element count"), "{out}");
assert!(!out.contains("Value:"), "{out}");
}
#[test]
fn extended_block_by_class_boundary() {
let basic = Snapshot::new(EpicsValue::Double(1.0), 0, 0, SystemTime::UNIX_EPOCH);
assert_eq!(dbr_extended_str(DBR_DOUBLE, &basic), "");
let t = dbr_extended_str(DBR_TIME_DOUBLE, &basic);
assert!(t.contains(" Timestamp: "), "{t}");
assert!(t.contains(" Status: NO_ALARM"), "{t}");
let mut e = Snapshot::new(EpicsValue::Enum(1), 0, 0, SystemTime::UNIX_EPOCH);
e.enums = Some(EnumInfo {
strings: vec!["OFF".into(), "ON".into()],
});
let es = dbr_extended_str(24, &e);
assert!(es.contains(" Enums: ( 2)"), "{es}");
assert!(es.contains("[ 0] OFF"), "{es}");
assert!(es.contains("[ 1] ON"), "{es}");
}
#[test]
fn gr_ctrl_string_extended_block_is_status_severity_only() {
let snap = ctrl_double_snap();
for req in [DBR_GR_STRING, DBR_CTRL_STRING] {
let ext = dbr_extended_str(req, &snap);
assert!(
ext.contains(" Status: NO_ALARM"),
"req {req} must print Status: {ext}"
);
assert!(
ext.contains(" Severity: NO_ALARM"),
"req {req} must print Severity: {ext}"
);
assert!(!ext.contains("Units"), "req {req} must omit Units: {ext}");
assert!(
!ext.contains("disp limit"),
"req {req} must omit display limits: {ext}"
);
assert!(
!ext.contains("alarm limit"),
"req {req} must omit alarm limits: {ext}"
);
assert!(
!ext.contains("warn limit"),
"req {req} must omit warn limits: {ext}"
);
assert!(
!ext.contains("ctrl limit"),
"req {req} must omit control limits: {ext}"
);
assert!(
!ext.contains("Precision"),
"req {req} must omit Precision: {ext}"
);
}
}
#[test]
fn dbr_text_maps_codes() {
assert_eq!(dbr_text(DBR_DOUBLE), "DBR_DOUBLE");
assert_eq!(dbr_text(DBR_CTRL_DOUBLE), "DBR_CTRL_DOUBLE");
assert_eq!(dbr_text(DBR_CLASS_NAME), "DBR_CLASS_NAME");
assert_eq!(dbr_text(99), "DBR_invalid");
}
use epics_ca_rs::DbFieldType;
#[test]
fn scan_leading_i64_matches_sscanf_d() {
assert_eq!(scan_leading_i64("16"), Some(16));
assert_eq!(scan_leading_i64(" 20 "), Some(20));
assert_eq!(scan_leading_i64("-5"), Some(-5));
assert_eq!(scan_leading_i64("16x"), Some(16));
assert_eq!(scan_leading_i64("0x10"), Some(0));
assert_eq!(scan_leading_i64("DBR_TIME_FLOAT"), None);
assert_eq!(scan_leading_i64(""), None);
}
#[test]
fn numeric_tokens_pass_through_verbatim() {
assert_eq!(parse_dbr_type("0"), Some(DBR_STRING));
assert_eq!(parse_dbr_type("6"), Some(DBR_DOUBLE));
assert_eq!(parse_dbr_type("16"), Some(DBR_TIME_FLOAT));
assert_eq!(parse_dbr_type("20"), Some(DBR_TIME_DOUBLE));
assert_eq!(parse_dbr_type("37"), Some(DBR_STSACK_STRING));
assert_eq!(parse_dbr_type("38"), Some(DBR_CLASS_NAME));
}
#[test]
fn invalid_codes_revert_to_plain() {
assert_eq!(parse_dbr_type("-1"), None);
assert_eq!(parse_dbr_type("35"), None); assert_eq!(parse_dbr_type("36"), None); assert_eq!(parse_dbr_type("39"), None);
assert_eq!(parse_dbr_type("999"), None);
}
#[test]
fn named_types_resolve_exactly() {
assert_eq!(parse_dbr_type("DBR_TIME_FLOAT"), Some(DBR_TIME_FLOAT));
assert_eq!(parse_dbr_type("TIME_FLOAT"), Some(DBR_TIME_FLOAT));
assert_eq!(parse_dbr_type("DBR_DOUBLE"), Some(DBR_DOUBLE));
assert_eq!(parse_dbr_type("DOUBLE"), Some(DBR_DOUBLE));
assert_eq!(parse_dbr_type("DBR_CLASS_NAME"), Some(DBR_CLASS_NAME));
}
#[test]
fn case_sensitive_and_unknown_revert_to_plain() {
assert_eq!(parse_dbr_type("dbr_time_float"), None);
assert_eq!(parse_dbr_type("double"), None);
assert_eq!(parse_dbr_type("NONSENSE"), None);
assert_eq!(parse_dbr_type(""), None);
}
}