use super::registry::*;
use crate::server::record::dbd_generated::RECORD_TYPES;
use crate::server::record::{
Asl, FieldDeclaration, FieldDesc, HwLink, HwLinkKind, ParsedLink, Special,
};
use crate::types::DbfCode;
pub(crate) fn register(registry: &mut CommandRegistry) {
registry.register(cmd_db_dump_record());
registry.register(cmd_db_dump_menu());
registry.register(cmd_db_dump_record_type());
registry.register(cmd_db_dump_field());
registry.register(cmd_db_dump_device());
registry.register(cmd_db_dump_driver());
registry.register(cmd_db_dump_link());
registry.register(cmd_db_dump_registrar());
registry.register(cmd_db_dump_function());
registry.register(cmd_db_dump_variable());
registry.register(cmd_db_pvd_dump());
registry.register(cmd_db_pvd_table_size());
registry.register(cmd_db_report_device_config());
registry.register(cmd_dbhcr());
}
fn check_pdbbase(arg: &ArgValue) -> Result<(), String> {
if let ArgValue::String(pdbbase) = arg
&& !(pdbbase.is_empty() || pdbbase.starts_with('0') || pdbbase == "pdbbase")
{
return Err(format!("Expecting 'pdbbase' got '{pdbbase}'."));
}
Ok(())
}
fn print_escaped(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'\x07' => out.push_str("\\a"),
'\x08' => out.push_str("\\b"),
'\x0c' => out.push_str("\\f"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
'\x0b' => out.push_str("\\v"),
'\\' => out.push_str("\\\\"),
'\'' => out.push_str("\\'"),
'"' => out.push_str("\\\""),
c if c.is_ascii_graphic() || c == ' ' => out.push(c),
c => {
let mut buf = [0u8; 4];
for byte in c.encode_utf8(&mut buf).as_bytes() {
out.push_str(&format!("\\x{byte:02x}"));
}
}
}
}
out
}
fn record_names_by_type(ctx: &CommandContext) -> Vec<String> {
let mut names = ctx.block_on(ctx.db().all_record_names());
let rank = |name: &String| {
let record_type = ctx
.db()
.get_record(name)
.map(|rec| rec.read().record.record_type().to_string())
.unwrap_or_default();
match RECORD_TYPES.iter().position(|t| *t == record_type) {
Some(i) => (0usize, i, String::new()),
None => (1usize, 0, record_type),
}
};
names.sort_by_cached_key(rank);
names
}
fn type_filter(arg: &ArgValue) -> Option<&str> {
match arg {
ArgValue::String(s) if !s.is_empty() && s != "*" => Some(s.as_str()),
_ => None,
}
}
fn cmd_db_dump_record() -> CommandDef {
CommandDef::new(
"dbDumpRecord",
vec![
ArgDesc {
name: "pdbbase",
arg_type: ArgType::String,
},
ArgDesc {
name: "recordTypeName",
arg_type: ArgType::String,
},
ArgDesc {
name: "interest level",
arg_type: ArgType::Int,
},
],
concat!(
"Dump information about the recordTypeName with 'interest level' details.\n",
"Example: dbDumpRecord ai 2\n",
"If last argument(s) are missing, dump all recordType information in the database.",
),
|args: &[ArgValue], ctx: &CommandContext| {
check_pdbbase(&args[0])?;
let wanted = type_filter(&args[1]);
let level = match args[2] {
ArgValue::Int(n) => n,
_ => 0,
};
let names = record_names_by_type(ctx);
let record_type_of = |name: &String| {
ctx.db()
.get_record(name)
.map(|rec| rec.read().record.record_type().to_string())
};
if let Some(wanted) = wanted
&& !RECORD_TYPES.contains(&wanted)
&& !names
.iter()
.any(|name| record_type_of(name).as_deref() == Some(wanted))
{
return Err(format!(
"dbWriteRecordFP: No record description for {wanted}"
));
}
for name in &names {
let Some(record_type) = record_type_of(name) else {
continue;
};
if let Some(wanted) = wanted
&& record_type != wanted
{
continue;
}
for line in dump_one_record(ctx, name, &record_type, level) {
ctx.println(&line);
}
}
for name in &names {
if let Some(wanted) = wanted
&& record_type_of(name).as_deref() != Some(wanted)
{
continue;
}
for alias in ctx.db().aliases_for_record(name) {
ctx.println(&format!("alias(\"{name}\",\"{alias}\")"));
}
}
Ok(CommandOutcome::Continue)
},
)
}
fn c_link_type_number(t: crate::server::record::DbLinkType) -> i32 {
use crate::server::record::DbLinkType as L;
match t {
L::Constant => 0,
L::PvLink => 1,
L::VmeIo => 2,
L::CamacIo => 3,
L::AbIo => 4,
L::GpibIo => 5,
L::BitbusIo => 6,
L::JsonLink => 8,
L::DbLink => 10,
L::CaLink => 11,
L::InstIo => 12,
L::BbgpibIo => 13,
L::RfIo => 14,
L::VxiIo => 15,
}
}
fn c_special_name(code: i16) -> Option<&'static str> {
Some(match code {
1 => "SPC_NOMOD",
2 => "SPC_DBADDR",
3 => "SPC_SCAN",
5 => "SPC_ALARMACK",
6 => "SPC_AS",
100 => "SPC_MOD",
101 => "SPC_RESET",
102 => "SPC_LINCONV",
103 => "SPC_CALC",
_ => return None,
})
}
fn c_special_code(special: Special) -> i16 {
special as i16
}
fn dump_field_lines(desc: &FieldDesc, ind_record_type: Option<usize>) -> Vec<String> {
let special = c_special_code(desc.special);
let mut lines = vec![format!(" {}", desc.name)];
if let Some(ind) = ind_record_type {
lines.push(format!("\t indRecordType: {ind}"));
}
lines.extend([
match c_special_name(special) {
Some(name) if special != 0 => format!("\t special: {special} {name}"),
_ => format!("\t special: {special} "),
},
format!("\t field_type: DBF_{}", desc.declared_dbf.name()),
format!("\tprocess_passive: {}", u8::from(desc.pp)),
format!("\t property: {}", u8::from(desc.prop)),
format!("\t interest: {}", desc.interest),
format!(
"\t as_level: {}",
match desc.asl {
Asl::Asl0 => 0,
Asl::Asl1 => 1,
}
),
format!("\t initial: {}", desc.initial.unwrap_or("")),
]);
lines
}
fn cmd_db_dump_menu() -> CommandDef {
CommandDef::new(
"dbDumpMenu",
vec![
ArgDesc {
name: "pdbbase",
arg_type: ArgType::String,
},
ArgDesc {
name: "menuName",
arg_type: ArgType::String,
},
],
concat!(
"Dump information about the available menuNames and choices defined within each menuName.\n",
"Example: dbDumpMenu pdbbase menuAlarmStat \n",
"If last argument(s) are missing, dump all menuNames information in the database.",
),
|args: &[ArgValue], ctx: &CommandContext| {
check_pdbbase(&args[0])?;
let wanted = match &args[1] {
ArgValue::String(s) if !s.is_empty() && s != "*" => Some(s.clone()),
_ => None,
};
for (name, idents, values) in crate::server::record::dbd_generated::MENUS {
if let Some(w) = wanted.as_deref()
&& *name != w
{
continue;
}
ctx.println(&format!("menu({name}) {{"));
for (ident, value) in idents.iter().zip(values.iter()) {
ctx.println(&format!("\tchoice({ident},\"{value}\")"));
}
ctx.println("}");
if wanted.is_some() {
break;
}
}
Ok(CommandOutcome::Continue)
},
)
}
fn cmd_db_dump_record_type() -> CommandDef {
CommandDef::new(
"dbDumpRecordType",
vec![
ArgDesc {
name: "pdbbase",
arg_type: ArgType::String,
},
ArgDesc {
name: "recordTypeName",
arg_type: ArgType::String,
},
],
concat!(
"Dump information about available fields in the recortTypeName sorted by index and name.\n",
"Example: dbDumpRecordType pdbbase calcout\n",
"If last argument(s) are missing, dump fields information for all records in the database.",
),
|args: &[ArgValue], ctx: &CommandContext| {
check_pdbbase(&args[0])?;
let wanted = match &args[1] {
ArgValue::String(s) => Some(s.clone()),
_ => None,
};
for record_type in RECORD_TYPES {
if let Some(w) = wanted.as_deref()
&& *record_type != w
{
continue;
}
for line in dump_record_type_lines(record_type) {
ctx.println(&line);
}
if wanted.is_some() {
break;
}
}
Ok(CommandOutcome::Continue)
},
)
}
fn dump_record_type_lines(record_type: &str) -> Vec<String> {
use crate::server::record::dbd_generated as dbd;
let Some(decl) = dbd::record_declaration_order(record_type) else {
return Vec::new();
};
let own = dbd::record_fields(record_type).unwrap_or(&[]);
let declared_dbf = |name: &str| {
dbd::DB_COMMON_FIELDS
.iter()
.chain(own.iter())
.find(|d| d.name == name)
.map(|d| d.declared_dbf)
};
let link_ind: Vec<usize> = decl
.iter()
.enumerate()
.filter(|(_, name)| {
matches!(
declared_dbf(name),
Some(DbfCode::Inlink | DbfCode::Outlink | DbfCode::Fwdlink)
)
})
.map(|(i, _)| i)
.collect();
let mut lines = vec![
format!(
"name({record_type}) no_fields({}) no_prompt({}) no_links({})",
decl.len(),
dbd::record_no_prompt(record_type).unwrap_or(0),
link_ind.len()
),
"index name\tsortind sortname".to_string(),
];
let mut sort_ind: Vec<usize> = (0..decl.len()).collect();
sort_ind.sort_by_key(|&i| decl[i]);
for (i, name) in decl.iter().enumerate() {
lines.push(format!(
"{:5} {}\t{:7} {}",
i, name, sort_ind[i], decl[sort_ind[i]]
));
}
let mut link_line = String::from("link_ind ");
for i in &link_ind {
link_line.push_str(&format!(" {i}"));
}
lines.push(link_line);
if let Some(indval) = decl.iter().position(|name| *name == "VAL") {
lines.push(format!("indvalFlddes {indval} name VAL"));
}
lines
}
fn cmd_db_dump_field() -> CommandDef {
CommandDef::new(
"dbDumpField",
vec![
ArgDesc {
name: "pdbbase",
arg_type: ArgType::String,
},
ArgDesc {
name: "recordTypeName",
arg_type: ArgType::String,
},
ArgDesc {
name: "fieldName",
arg_type: ArgType::String,
},
],
concat!(
"Dump information about the fieldName in the recordTypeName.\n",
"Example: dbDumpField pdbbase calcout A\n",
"If last argument(s) are missing, dump information\n",
"about all fieldName in all recordTypeName in the database.",
),
|args: &[ArgValue], ctx: &CommandContext| {
check_pdbbase(&args[0])?;
let name_arg = |arg: &ArgValue| match arg {
ArgValue::String(s) => Some(s.clone()),
_ => None,
};
let wanted_type = name_arg(&args[1]);
let wanted_field = name_arg(&args[2]);
for record_type in RECORD_TYPES {
if let Some(wanted) = wanted_type.as_deref()
&& *record_type != wanted
{
continue;
}
ctx.println(&format!("recordtype({record_type}) "));
let common = crate::server::record::dbd_generated::DB_COMMON_FIELDS;
let own =
crate::server::record::dbd_generated::record_fields(record_type).unwrap_or(&[]);
let decl =
crate::server::record::dbd_generated::record_declaration_order(record_type)
.unwrap_or(&[]);
for desc in common.iter().chain(own.iter()) {
if let Some(wanted) = wanted_field.as_deref()
&& desc.name != wanted
{
continue;
}
let ind = decl.iter().position(|name| *name == desc.name);
for line in dump_field_lines(desc, ind) {
ctx.println(&line);
}
}
for (name, value) in ctx.db().record_type_attributes(record_type) {
ctx.println(&format!("Attribute: name {name} value {value}"));
}
if wanted_type.is_some() {
break;
}
}
Ok(CommandOutcome::Continue)
},
)
}
fn cmd_db_dump_device() -> CommandDef {
CommandDef::new(
"dbDumpDevice",
vec![
ArgDesc {
name: "pdbbase",
arg_type: ArgType::String,
},
ArgDesc {
name: "recordTypeName",
arg_type: ArgType::String,
},
],
concat!(
"Dump device support information for the recordTypeName.\n",
"Example: dbDumpDevice pdbbase ai\n",
"If last argument(s) are missing, dump device support\n",
"information for all records in the database.",
),
|args: &[ArgValue], ctx: &CommandContext| {
check_pdbbase(&args[0])?;
let wanted = type_filter(&args[1]);
for record_type in RECORD_TYPES {
if let Some(wanted) = wanted
&& *record_type != wanted
{
continue;
}
let declared = crate::server::record::dbd_generated::device_menu(record_type)
.unwrap_or_default();
let contributed = crate::server::record::contributed_device_menu(record_type);
if declared.is_empty() && contributed.is_empty() {
continue;
}
ctx.println(&format!("recordtype({record_type})"));
for choice in declared.iter().copied().chain(contributed) {
ctx.println(&format!("\tchoice: {choice}"));
if let Some(link_type) =
crate::server::record::dbd_generated::device_link_type(record_type, choice)
{
ctx.println(&format!("\tlink_type: {}", c_link_type_number(link_type)));
}
}
if wanted.is_some() {
break;
}
}
Ok(CommandOutcome::Continue)
},
)
}
fn cmd_db_dump_driver() -> CommandDef {
CommandDef::new(
"dbDumpDriver",
vec![ArgDesc {
name: "pdbbase",
arg_type: ArgType::String,
}],
concat!(
"Dump device support information.\n",
"Example: dbDumpDriver pdbbase\n",
"If the last argument(s) are missing, dump all device support information.",
),
|args: &[ArgValue], ctx: &CommandContext| {
check_pdbbase(&args[0])?;
let mut names: Vec<String> = crate::server::driver_support::driver_supports()
.into_iter()
.map(|(name, _)| name)
.collect();
names.sort();
for name in names {
ctx.println(&format!("driver({name})"));
}
Ok(CommandOutcome::Continue)
},
)
}
fn cmd_db_dump_link() -> CommandDef {
CommandDef::new(
"dbDumpLink",
vec![ArgDesc {
name: "pdbbase",
arg_type: ArgType::String,
}],
concat!(
"Dump list of registered links\n",
"Example: dbDumpLink pdbbase",
),
|args: &[ArgValue], ctx: &CommandContext| {
check_pdbbase(&args[0])?;
for sup in link_sup_list().lock().unwrap().entries() {
ctx.println(&format!("link({},{})", sup.key, sup.jlif_name));
}
Ok(CommandOutcome::Continue)
},
)
}
trait DbdDeclaration {
fn key(&self) -> &str;
}
impl DbdDeclaration for String {
fn key(&self) -> &str {
self
}
}
struct DeclarationList<T> {
built_in: Vec<T>,
loaded: Vec<T>,
}
impl<T> Default for DeclarationList<T> {
fn default() -> Self {
Self {
built_in: Vec::new(),
loaded: Vec::new(),
}
}
}
impl<T: DbdDeclaration> DeclarationList<T> {
fn contains(&self, key: &str) -> bool {
self.built_in
.iter()
.chain(&self.loaded)
.any(|d| d.key() == key)
}
fn declare_built_in(&mut self, decl: T) {
if self.contains(decl.key()) {
return;
}
let at = self.built_in.partition_point(|d| d.key() < decl.key());
self.built_in.insert(at, decl);
}
fn declare_loaded(&mut self, decl: T) {
if self.contains(decl.key()) {
return;
}
self.loaded.push(decl);
}
fn entries(&self) -> impl Iterator<Item = &T> {
self.built_in.iter().chain(&self.loaded)
}
}
fn registrar_list() -> &'static std::sync::Mutex<DeclarationList<String>> {
static LIST: std::sync::OnceLock<std::sync::Mutex<DeclarationList<String>>> =
std::sync::OnceLock::new();
LIST.get_or_init(|| {
let mut list = DeclarationList::default();
for name in BUILT_IN_REGISTRARS {
list.declare_built_in((*name).to_string());
}
std::sync::Mutex::new(list)
})
}
#[derive(Clone, PartialEq, Eq)]
struct LinkDeclaration {
key: String,
jlif_name: String,
}
impl DbdDeclaration for LinkDeclaration {
fn key(&self) -> &str {
&self.key
}
}
fn link_sup_list() -> &'static std::sync::Mutex<DeclarationList<LinkDeclaration>> {
static LIST: std::sync::OnceLock<std::sync::Mutex<DeclarationList<LinkDeclaration>>> =
std::sync::OnceLock::new();
LIST.get_or_init(|| {
let mut list = DeclarationList::default();
for sup in crate::server::record::PORTED_JSON_LINK_TYPES {
list.declare_built_in(LinkDeclaration {
key: sup.key.to_string(),
jlif_name: sup.jlif_name.to_string(),
});
}
std::sync::Mutex::new(list)
})
}
pub(super) fn add_loaded_link_types(types: &[crate::server::db_loader::DbdLinkType]) {
let mut list = link_sup_list().lock().unwrap();
for t in types {
if !t.key.is_empty() {
list.declare_loaded(LinkDeclaration {
key: t.key.clone(),
jlif_name: t.lset.clone(),
});
}
}
}
static BUILT_IN_REGISTRARS: &[&str] = &[
"arrInitialize",
"dbndInitialize",
"decInitialize",
"dlloadRegistar",
"iocshSystemCommand",
"syncInitialize",
"tsInitialize",
"utagInitialize",
];
pub(super) fn add_registrars(names: &[String]) {
let mut list = registrar_list().lock().unwrap();
for name in names {
if !name.is_empty() {
list.declare_built_in(name.clone());
}
}
}
pub(super) fn add_loaded_registrars(names: &[String]) {
let mut list = registrar_list().lock().unwrap();
for name in names {
if !name.is_empty() {
list.declare_loaded(name.clone());
}
}
}
fn cmd_db_dump_registrar() -> CommandDef {
CommandDef::new(
"dbDumpRegistrar",
vec![ArgDesc {
name: "pdbbase",
arg_type: ArgType::String,
}],
concat!(
"Dump list of registered functions including ones for subroutine records,\n",
"and ones that can be invoked from iocsh.\n",
"Example: dbDumpRegistrar pdbbase\n",
"If last argument(s) are missing, dump all registered functions",
),
|args: &[ArgValue], ctx: &CommandContext| {
check_pdbbase(&args[0])?;
for name in registrar_list().lock().unwrap().entries() {
ctx.println(&format!("registrar({name})"));
}
Ok(CommandOutcome::Continue)
},
)
}
fn cmd_db_dump_function() -> CommandDef {
CommandDef::new(
"dbDumpFunction",
vec![ArgDesc {
name: "pdbbase",
arg_type: ArgType::String,
}],
concat!(
"Dump list of registered subroutine functions.\n",
"Example: dbDumpFunction pddbase\n",
"If last argument(s) are missing, dump all registered subroutine functions",
),
|args: &[ArgValue], ctx: &CommandContext| {
check_pdbbase(&args[0])?;
let mut names: Vec<String> = ctx
.db()
.subroutine_entries()
.into_iter()
.map(|(name, _addr)| name)
.collect();
names.sort();
for name in names {
ctx.println(&format!("function({name})"));
}
Ok(CommandOutcome::Continue)
},
)
}
fn cmd_db_dump_variable() -> CommandDef {
CommandDef::new(
"dbDumpVariable",
vec![ArgDesc {
name: "pdbbase",
arg_type: ArgType::String,
}],
concat!(
"Dump list of variables used in the database.\n",
"Example: dbDumpVariable pddbase\n",
"If last argument(s) are missing, dump all variables.",
),
|args: &[ArgValue], ctx: &CommandContext| {
check_pdbbase(&args[0])?;
for (name, dtype) in crate::server::record::dbd_generated::VARIABLES {
ctx.println(&format!("variable({name},{dtype})"));
}
Ok(CommandOutcome::Continue)
},
)
}
fn cmd_db_pvd_dump() -> CommandDef {
CommandDef::new(
"dbPvdDump",
vec![
ArgDesc {
name: "pdbbase",
arg_type: ArgType::String,
},
ArgDesc {
name: "verbose",
arg_type: ArgType::Int,
},
],
"Dump the process variable directory.\n\
If verbose is greater than 0, also print the process variables in it.\n\
Example: dbPvdDump pdbbase 1\n\
If the last argument(s) are missing, dump the directory as though verbose is 0.",
|args: &[ArgValue], ctx: &CommandContext| {
check_pdbbase(&args[0])?;
let verbose = match args[1] {
ArgValue::Int(n) => n != 0,
_ => false,
};
let mut names = ctx.block_on(ctx.db().all_record_names());
names.extend(ctx.db().all_alias_names());
names.extend(ctx.block_on(ctx.db().all_simple_pv_names()));
names.sort();
ctx.println(&format!(
"Process Variable Directory has {} entries",
names.len()
));
if verbose {
for row in names.chunks(4) {
let mut line = String::from(" ");
for name in row {
line.push_str(" ");
line.push_str(name);
}
ctx.println(&line);
}
}
Ok(CommandOutcome::Continue)
},
)
}
fn cmd_db_pvd_table_size() -> CommandDef {
CommandDef::new(
"dbPvdTableSize",
vec![ArgDesc {
name: "size",
arg_type: ArgType::Int,
}],
concat!(
"Change the number of buckets in the process variable directory.\n",
"\n",
"The process variable directory size should be set before loading the database.\n",
"The size of the process variable directory can automatically grow.\n",
"The size must be a power of 2.\n",
"\n",
"Example: dbPvdTableSize 1024",
),
|args: &[ArgValue], ctx: &CommandContext| {
let size = match args[0] {
ArgValue::Int(n) => n as i32,
_ => 0,
};
if size & size.wrapping_sub(1) != 0 {
ctx.println(&format!("dbPvdTableSize: {size} is not a power of 2"));
return Ok(CommandOutcome::Failed);
}
Ok(CommandOutcome::Continue)
},
)
}
fn c_hw_bus(hw: &HwLink) -> Option<&'static str> {
Some(match hw.kind() {
HwLinkKind::InstIo => "INST",
HwLinkKind::VmeIo => "VME",
HwLinkKind::CamacIo => "CAMAC",
HwLinkKind::AbIo => "AB",
HwLinkKind::GpibIo => "GPIB",
HwLinkKind::BitbusIo => "BITBUS",
HwLinkKind::BbgpibIo => "BBGPIB",
HwLinkKind::VxiIo => "VXI",
HwLinkKind::RfIo => return None,
})
}
fn device_config_lines(ctx: &CommandContext) -> Vec<String> {
let mut lines = Vec::new();
for name in record_names_by_type(ctx) {
let Some((bus, link_value)) =
ctx.db()
.record_link_fields(&name)
.into_iter()
.find_map(|(_field, _text, parsed)| match parsed {
ParsedLink::Hw(hw) => c_hw_bus(&hw).map(|bus| (bus, hw.render())),
_ => None,
})
else {
continue;
};
let Some(rec) = ctx.db().get_record(&name) else {
continue;
};
let inst = rec.read();
let field_string =
|field: &str| super::commands::db_get_string_field(&inst, field).unwrap_or_default();
let cvt = if field_string("LINR") == "LINEAR" {
format!("cvt({},{})", field_string("EGUL"), field_string("EGUF"))
} else {
String::new()
};
lines.push(format!(
"{bus:<8} {link_value:<20} {:<20} {name:<20} {cvt}",
field_string("DTYP")
));
}
lines
}
fn cmd_db_report_device_config() -> CommandDef {
CommandDef::new(
"dbReportDeviceConfig",
vec![ArgDesc {
name: "pdbbase",
arg_type: ArgType::String,
}],
concat!(
"Print the link type, link value, device type, record name,\n",
"and linearisation info (if applicable),\n",
"for every record using a specific device type.\n",
"\n",
"Example: dbReportDeviceConfig pdbbase",
),
|args: &[ArgValue], ctx: &CommandContext| {
check_pdbbase(&args[0])?;
for line in device_config_lines(ctx) {
ctx.println(&line);
}
Ok(CommandOutcome::Continue)
},
)
}
fn cmd_dbhcr() -> CommandDef {
CommandDef::new(
"dbhcr",
vec![],
"dbhcr — Report hardware configuration for every record using a device type.",
|_args: &[ArgValue], ctx: &CommandContext| {
for line in device_config_lines(ctx) {
ctx.println(&line);
}
Ok(CommandOutcome::Continue)
},
)
}
fn dump_one_record(ctx: &CommandContext, name: &str, record_type: &str, level: i64) -> Vec<String> {
let Some(rec) = ctx.db().get_record(name) else {
return Vec::new();
};
let mut lines = vec![format!("record({record_type},\"{name}\") {{")];
let inst = rec.read();
let mut descs: Vec<&crate::server::record::FieldDesc> =
crate::server::record::dbd_generated::DB_COMMON_FIELDS
.iter()
.chain(inst.record.field_list())
.collect();
let mut seen = std::collections::HashSet::new();
descs.retain(|d| seen.insert(d.name));
for desc in descs {
let Some(value) = inst.resolve_field(desc.name) else {
continue;
};
if level == 0 && is_default_value(desc, &value) {
continue;
}
lines.push(format!(
"\tfield({},\"{}\")",
desc.name,
print_escaped(&value.to_string())
));
}
let mut tags: Vec<(&String, &String)> = inst.info.iter().collect();
tags.sort();
for (key, value) in tags {
lines.push(format!("\tinfo(\"{key}\",\"{}\")", print_escaped(value)));
}
lines.push("}".to_string());
lines
}
fn is_default_value(
desc: &crate::server::record::FieldDesc,
value: &crate::types::EpicsValue,
) -> bool {
use crate::types::EpicsValue as V;
let initial = desc.initial;
match value {
V::String(s) => match initial {
Some(init) => s.as_str_lossy() == init,
None => s.as_str_lossy().is_empty(),
},
V::Enum(v) | V::EnumWithChoices { index: v, .. } => {
let Some(init) = initial else {
return *v == 0;
};
match desc.menu.and_then(|m| m.iter().position(|c| *c == init)) {
Some(index) => u64::from(*v) == index as u64,
None => init.parse::<u16>().map(|def| *v == def).unwrap_or(false),
}
}
V::Char(v) => int_is_default(u64::from(*v), initial),
V::UChar(v) => int_is_default(u64::from(*v), initial),
V::UShort(v) => int_is_default(u64::from(*v), initial),
V::ULong(v) => int_is_default(u64::from(*v), initial),
V::UInt64(v) => int_is_default(*v, initial),
V::Short(v) => signed_is_default(i64::from(*v), initial),
V::Long(v) => signed_is_default(i64::from(*v), initial),
V::Int64(v) => signed_is_default(*v, initial),
V::Float(v) => float_is_default(f64::from(*v), initial),
V::Double(v) => float_is_default(*v, initial),
_ => true,
}
}
fn int_is_default(value: u64, initial: Option<&str>) -> bool {
match initial {
Some(init) => parse_c_unsigned(init)
.map(|def| value == def)
.unwrap_or(false),
None => value == 0,
}
}
fn signed_is_default(value: i64, initial: Option<&str>) -> bool {
match initial {
Some(init) => parse_c_signed(init)
.map(|def| value == def)
.unwrap_or(false),
None => value == 0,
}
}
fn float_is_default(value: f64, initial: Option<&str>) -> bool {
match initial {
Some(init) => init
.trim()
.parse::<f64>()
.map(|def| value == def)
.unwrap_or(false),
None => value == 0.0,
}
}
fn parse_c_unsigned(s: &str) -> Option<u64> {
let s = s.trim();
if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
return u64::from_str_radix(hex, 16).ok();
}
if s.len() > 1 && s.starts_with('0') {
return u64::from_str_radix(&s[1..], 8).ok();
}
s.parse::<u64>().ok()
}
fn parse_c_signed(s: &str) -> Option<i64> {
let s = s.trim();
if let Some(rest) = s.strip_prefix('-') {
return parse_c_unsigned(rest)
.and_then(|v| i64::try_from(v).ok())
.map(|v| -v);
}
parse_c_unsigned(s.strip_prefix('+').unwrap_or(s)).and_then(|v| i64::try_from(v).ok())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::server::database::PvDatabase;
use std::sync::Arc;
fn make_ctx() -> (Arc<PvDatabase>, CommandContext) {
let rt = tokio::runtime::Runtime::new().unwrap();
let db = Arc::new(PvDatabase::new());
let bridge = {
let _guard = rt.enter();
crate::runtime::task::BlockingBridge::capture()
};
let ctx = CommandContext::new(db.clone(), bridge);
std::mem::forget(rt);
(db, ctx)
}
fn registry() -> CommandRegistry {
let mut reg = CommandRegistry::new();
super::super::commands::register_builtins(&mut reg);
reg
}
fn capture(ctx: &CommandContext, f: impl FnOnce()) -> String {
let tmp = tempfile::NamedTempFile::new().unwrap();
let path = tmp.path().to_path_buf();
ctx.with_output(std::fs::File::create(&path).unwrap(), f);
std::fs::read_to_string(&path).unwrap()
}
const C_RECORD_VARIABLES: &str = "\
variable(boHIGHlimit,double)
variable(boHIGHprecision,int)
variable(calcoutODLYlimit,double)
variable(calcoutODLYprecision,int)
variable(histogramSDELprecision,int)
variable(seqDLYlimit,double)
variable(seqDLYprecision,int)
";
#[test]
fn db_dump_variable_reports_dbd_declarations_and_not_the_iocsh_var_table() {
let (_db, ctx) = make_ctx();
let printed = run(&ctx, "dbDumpVariable", &["pdbbase"]).unwrap();
assert_eq!(printed, C_RECORD_VARIABLES);
assert!(
super::super::vars::variable_names().contains(&"asCheckClientIP"),
"the iocsh variable table holds the knob this report must not"
);
assert!(!printed.contains("asCheckClientIP"));
}
#[test]
fn db_dump_link_lists_exactly_the_types_a_link_field_accepts() {
use crate::server::record::check_json_link_text;
let (_db, ctx) = make_ctx();
let printed = run(&ctx, "dbDumpLink", &["pdbbase"]).unwrap();
assert_eq!(
printed,
"link(ca,lnkCaIf)\n\
link(calc,lnkCalcIf)\n\
link(const,lnkConstIf)\n\
link(pva,lsetPVX)\n\
link(state,lnkStateIf)\n"
);
for text in [
"{const:1}",
"{calc:{expr:\"A\"}}",
"{pva:\"X\"}",
"{ca:\"X\"}",
"{state:\"x\"}",
] {
assert!(
check_json_link_text(text).is_ok(),
"{text} is reported by dbDumpLink and must load"
);
}
for (key, text) in [
("debug", "{debug:{const:1}}"),
("trace", "{trace:{const:1}}"),
] {
assert!(
check_json_link_text(text).is_err(),
"{text} is refused, so it must stay out of the report"
);
assert!(!printed.contains(&format!("link({key},")));
}
}
#[test]
fn a_runtime_link_declaration_appends_with_its_jlif_name() {
use std::io::Write;
let (_db, ctx) = make_ctx();
let mut tmp = tempfile::Builder::new().suffix(".dbd").tempfile().unwrap();
writeln!(tmp, "link(aaa, jlifAaa)").unwrap();
run(&ctx, "dbLoadDatabase", &[tmp.path().to_str().unwrap()]).unwrap();
assert_eq!(
run(&ctx, "dbDumpLink", &["pdbbase"]).unwrap(),
"link(ca,lnkCaIf)\n\
link(calc,lnkCalcIf)\n\
link(const,lnkConstIf)\n\
link(pva,lsetPVX)\n\
link(state,lnkStateIf)\n\
link(aaa,jlifAaa)\n"
);
}
#[test]
fn a_registered_driver_is_visible_to_both_db_dump_driver_and_dbior() {
use crate::server::driver_support::{DriverSupport, register_driver_support};
struct Probe;
impl DriverSupport for Probe {
fn report(&self, _level: i32) -> Option<String> {
Some("probe".into())
}
}
let (_db, ctx) = make_ctx();
assert!(register_driver_support(
"drvDumpDriverProbe",
Arc::new(Probe)
));
let dumped = run(&ctx, "dbDumpDriver", &["pdbbase"]).unwrap();
assert!(dumped.contains("driver(drvDumpDriverProbe)\n"), "{dumped}");
let reported = run(&ctx, "dbior", &["drvDumpDriverProbe", "0"]).unwrap();
assert!(reported.contains("drvDumpDriverProbe"), "{reported}");
}
fn run(ctx: &CommandContext, name: &str, tokens: &[&str]) -> Result<String, String> {
let reg = registry();
let cmd = reg.get(name).unwrap();
let tokens: Vec<String> = tokens.iter().map(|t| t.to_string()).collect();
let args = super::super::registry::parse_args(&tokens, &cmd.args).unwrap();
let mut outcome = Ok(());
let printed = capture(ctx, || {
outcome = cmd.handler.call(&args, ctx).map(|_| ());
});
outcome.map(|()| printed)
}
#[test]
fn dump_record_type_reproduces_c_s_report_for_ai() {
let (_db, ctx) = make_ctx();
let out = run(&ctx, "dbDumpRecordType", &["pdbbase", "ai"]).unwrap();
let lines: Vec<&str> = out.lines().collect();
assert_eq!(lines[0], "name(ai) no_fields(93) no_prompt(49) no_links(6)");
assert_eq!(lines[1], "index name\tsortind sortname");
assert_eq!(lines[2], " 0 NAME\t 24 ACKS");
assert_eq!(lines[94], " 92 SIMPVT\t 48 VAL");
assert_eq!(lines[95], "link_ind 8 12 47 49 84 86");
assert_eq!(lines[96], "indvalFlddes 48 name VAL");
assert_eq!(
lines.len(),
97,
"no line beyond C's, and none of C's dropped"
);
}
#[test]
fn dump_record_type_counts_calcout_s_links_like_c() {
let (_db, ctx) = make_ctx();
let out = run(&ctx, "dbDumpRecordType", &["pdbbase", "calcout"]).unwrap();
let lines: Vec<&str> = out.lines().collect();
assert_eq!(
lines[0],
"name(calcout) no_fields(171) no_prompt(64) no_links(25)"
);
assert_eq!(
lines[173],
"link_ind 8 12 47 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74"
);
assert_eq!(lines[174], "indvalFlddes 49 name VAL");
}
#[test]
fn dump_record_type_takes_no_wildcard() {
let (_db, ctx) = make_ctx();
assert_eq!(run(&ctx, "dbDumpRecordType", &["pdbbase", ""]).unwrap(), "");
assert_eq!(
run(&ctx, "dbDumpRecordType", &["pdbbase", "*"]).unwrap(),
""
);
let all = run(&ctx, "dbDumpRecordType", &["pdbbase"]).unwrap();
assert_eq!(
all.lines().filter(|l| l.starts_with("name(")).count(),
RECORD_TYPES.len()
);
}
#[test]
fn dump_record_type_sort_and_links_hold_for_every_type() {
use crate::server::record::dbd_generated as dbd;
for record_type in RECORD_TYPES {
let lines = dump_record_type_lines(record_type);
let decl = dbd::record_declaration_order(record_type).unwrap();
let rows = &lines[2..2 + decl.len()];
let mut seen: Vec<usize> = rows
.iter()
.map(|row| {
let (_, tail) = row.split_once('\t').unwrap();
tail.split_whitespace().next().unwrap().parse().unwrap()
})
.collect();
let sorted_names: Vec<&str> = seen.iter().map(|&i| decl[i]).collect();
let mut ascending = sorted_names.clone();
ascending.sort_unstable();
assert_eq!(sorted_names, ascending, "{record_type} sortname order");
seen.sort_unstable();
assert_eq!(
seen,
(0..decl.len()).collect::<Vec<_>>(),
"{record_type} sortind permutation"
);
let link_line = lines.iter().find(|l| l.starts_with("link_ind")).unwrap();
let own = dbd::record_fields(record_type).unwrap_or(&[]);
for tok in link_line["link_ind".len()..].split_whitespace() {
let i: usize = tok.parse().unwrap();
let desc = dbd::DB_COMMON_FIELDS
.iter()
.chain(own.iter())
.find(|d| d.name == decl[i])
.unwrap_or_else(|| panic!("{record_type} link_ind {i} has no descriptor"));
assert!(
matches!(
desc.declared_dbf,
DbfCode::Inlink | DbfCode::Outlink | DbfCode::Fwdlink
),
"{record_type} link_ind {i} is {}",
desc.declared_dbf.name()
);
}
}
}
#[test]
fn a_named_menu_prints_c_s_three_columns() {
let (_db, ctx) = make_ctx();
let out = run(&ctx, "dbDumpMenu", &["pdbbase", "menuSimm"]).unwrap();
assert_eq!(
out,
"menu(menuSimm) {\n\
\tchoice(menuSimmNO,\"NO\")\n\
\tchoice(menuSimmYES,\"YES\")\n\
\tchoice(menuSimmRAW,\"RAW\")\n\
}\n"
);
}
#[test]
fn the_choice_identifier_is_not_the_value() {
let (_db, ctx) = make_ctx();
let out = run(&ctx, "dbDumpMenu", &["pdbbase", "menuYesNo"]).unwrap();
assert!(
out.contains("choice(menuYesNoNO,\"NO\")"),
"identifier column missing: {out}"
);
}
#[test]
fn the_three_dump_all_spellings_agree() {
let (_db, ctx) = make_ctx();
let all = run(&ctx, "dbDumpMenu", &["pdbbase"]).unwrap();
let empty = run(&ctx, "dbDumpMenu", &["pdbbase", ""]).unwrap();
let star = run(&ctx, "dbDumpMenu", &["pdbbase", "*"]).unwrap();
assert_eq!(all, empty);
assert_eq!(all, star);
assert_eq!(
all.lines().filter(|l| l.starts_with("menu(")).count(),
crate::server::record::dbd_generated::MENUS.len()
);
}
#[test]
fn an_unknown_menu_prints_nothing_and_succeeds() {
let (_db, ctx) = make_ctx();
assert_eq!(
run(&ctx, "dbDumpMenu", &["pdbbase", "menuNoSuchThing"]).unwrap(),
""
);
}
#[test]
fn every_menu_row_pairs_an_identifier_with_each_choice() {
for (name, idents, values) in crate::server::record::dbd_generated::MENUS {
assert_eq!(
idents.len(),
values.len(),
"menu({name}) has {} identifiers for {} choices",
idents.len(),
values.len()
);
}
}
fn load(ctx: &CommandContext, body: &str) {
use std::io::Write;
let tmp = tempfile::Builder::new().suffix(".db").tempfile().unwrap();
write!(tmp.as_file(), "{body}").unwrap();
let reg = registry();
let cmd = reg.get("dbLoadRecords").unwrap();
let args = super::super::registry::parse_args(
&[tmp.path().to_string_lossy().to_string()],
&cmd.args,
)
.unwrap();
cmd.handler.call(&args, ctx).unwrap();
}
#[test]
fn dump_record_emits_the_db_frame() {
let (_db, ctx) = make_ctx();
load(&ctx, "record(ai,\"AI:ONE\") { field(EGU,\"mm\") }\n");
let out = run(&ctx, "dbDumpRecord", &["pdbbase"]).unwrap();
assert!(
out.contains("record(ai,\"AI:ONE\") {\n"),
"C opens with `record(<type>,\"<name>\") {{` — got:\n{out}"
);
assert!(
out.contains("\tfield(EGU,\"mm\")\n"),
"a non-default field is printed at level 0 — got:\n{out}"
);
assert!(out.contains("\n}\n"), "C closes the block — got:\n{out}");
}
#[test]
fn dump_record_level_selects_default_valued_fields() {
let (_db, ctx) = make_ctx();
load(&ctx, "record(ai,\"AI:LEVEL\") { field(EGU,\"mm\") }\n");
let level0 = run(&ctx, "dbDumpRecord", &["pdbbase", "ai", "0"]).unwrap();
let level1 = run(&ctx, "dbDumpRecord", &["pdbbase", "ai", "1"]).unwrap();
assert!(
level1.lines().count() > level0.lines().count(),
"level 1 drops C's default-value filter — 0:\n{level0}\n1:\n{level1}"
);
assert!(
!level0.contains("\tfield(PREC,"),
"a field still holding its .dbd initial is omitted at level 0 — got:\n{level0}"
);
assert!(
level1.contains("\tfield(PREC,"),
"and printed at level 1 — got:\n{level1}"
);
}
#[test]
fn dump_record_reports_an_unknown_record_type() {
let (_db, ctx) = make_ctx();
load(&ctx, "record(ai,\"AI:KNOWN\") {}\n");
let err = run(&ctx, "dbDumpRecord", &["pdbbase", "nosuchtype"]).unwrap_err();
assert_eq!(err, "dbWriteRecordFP: No record description for nosuchtype");
}
#[test]
fn dump_record_star_is_the_all_types_sentinel() {
let (_db, ctx) = make_ctx();
load(
&ctx,
"record(ai,\"AI:STAR\") {}\nrecord(bi,\"BI:STAR\") {}\n",
);
let star = run(&ctx, "dbDumpRecord", &["pdbbase", "*"]).unwrap();
let bare = run(&ctx, "dbDumpRecord", &["pdbbase"]).unwrap();
assert_eq!(star, bare);
assert!(star.contains("AI:STAR") && star.contains("BI:STAR"));
let only_ai = run(&ctx, "dbDumpRecord", &["pdbbase", "ai"]).unwrap();
assert!(only_ai.contains("AI:STAR") && !only_ai.contains("BI:STAR"));
}
#[test]
fn dump_record_emits_aliases_after_the_records() {
let (_db, ctx) = make_ctx();
load(
&ctx,
"record(ai,\"AI:TARGET\") {}\nalias(\"AI:TARGET\",\"AI:OTHERNAME\")\n",
);
let out = run(&ctx, "dbDumpRecord", &["pdbbase"]).unwrap();
let alias_line = "alias(\"AI:TARGET\",\"AI:OTHERNAME\")";
assert!(out.contains(alias_line), "got:\n{out}");
assert!(
out.find("record(ai,\"AI:TARGET\")").unwrap() < out.find(alias_line).unwrap(),
"aliases follow the record blocks — got:\n{out}"
);
}
#[test]
fn dump_record_emits_info_tags() {
let (_db, ctx) = make_ctx();
load(
&ctx,
"record(ai,\"AI:INFO\") { info(\"Q:group\",\"grp\") }\n",
);
let out = run(&ctx, "dbDumpRecord", &["pdbbase"]).unwrap();
assert!(out.contains("\tinfo(\"Q:group\",\"grp\")\n"), "got:\n{out}");
}
#[test]
fn dump_device_prints_the_choice_and_link_type() {
let (_db, ctx) = make_ctx();
let out = run(&ctx, "dbDumpDevice", &["pdbbase", "ai"]).unwrap();
assert!(out.starts_with("recordtype(ai)\n"), "got:\n{out}");
assert!(out.contains("\tchoice: Soft Channel\n"), "got:\n{out}");
assert!(out.contains("\tlink_type: 0\n"), "got:\n{out}");
assert!(!out.contains("recordtype(bi)"), "got:\n{out}");
}
#[test]
fn dump_device_without_a_type_walks_them_all() {
let (_db, ctx) = make_ctx();
let out = run(&ctx, "dbDumpDevice", &["pdbbase"]).unwrap();
assert!(out.contains("recordtype(ai)\n") && out.contains("recordtype(bi)\n"));
assert_eq!(run(&ctx, "dbDumpDevice", &["pdbbase", "*"]).unwrap(), out);
}
#[test]
fn link_type_numbers_are_c_s() {
use crate::server::record::DbLinkType as L;
assert_eq!(c_link_type_number(L::Constant), 0);
assert_eq!(c_link_type_number(L::PvLink), 1);
assert_eq!(c_link_type_number(L::VmeIo), 2);
assert_eq!(c_link_type_number(L::CamacIo), 3);
assert_eq!(c_link_type_number(L::AbIo), 4);
assert_eq!(c_link_type_number(L::GpibIo), 5);
assert_eq!(c_link_type_number(L::BitbusIo), 6);
assert_eq!(c_link_type_number(L::JsonLink), 8);
assert_eq!(c_link_type_number(L::InstIo), 12);
assert_eq!(c_link_type_number(L::BbgpibIo), 13);
assert_eq!(c_link_type_number(L::RfIo), 14);
assert_eq!(c_link_type_number(L::VxiIo), 15);
}
#[test]
fn dump_function_lists_the_registered_subroutines() {
let (db, ctx) = make_ctx();
assert_eq!(run(&ctx, "dbDumpFunction", &["pdbbase"]).unwrap(), "");
let mut registry: std::collections::HashMap<
String,
Arc<crate::server::record::SubroutineFn>,
> = std::collections::HashMap::new();
for name in ["mySubZebra", "mySubAlpha"] {
registry.insert(
name.to_string(),
Arc::new(
Box::new(|_r: &mut dyn crate::server::record::Record| Ok(0i64))
as crate::server::record::SubroutineFn,
),
);
}
ctx.block_on(db.install_subroutine_registry(registry));
assert_eq!(
run(&ctx, "dbDumpFunction", &["pdbbase"]).unwrap(),
"function(mySubAlpha)\nfunction(mySubZebra)\n"
);
}
#[test]
fn db_dump_registrar_reports_the_compiled_in_declarations() {
let (_db, ctx) = make_ctx();
assert_eq!(
run(&ctx, "dbDumpRegistrar", &["pdbbase"]).unwrap(),
"registrar(arrInitialize)\n\
registrar(dbndInitialize)\n\
registrar(decInitialize)\n\
registrar(dlloadRegistar)\n\
registrar(iocshSystemCommand)\n\
registrar(syncInitialize)\n\
registrar(tsInitialize)\n\
registrar(utagInitialize)\n"
);
}
#[test]
fn a_registrar_another_crate_owns_lands_in_cs_slot_not_last() {
let (_db, ctx) = make_ctx();
super::add_registrars(&["rsrvRegistrar".to_string()]);
assert_eq!(
run(&ctx, "dbDumpRegistrar", &["pdbbase"]).unwrap(),
"registrar(arrInitialize)\n\
registrar(dbndInitialize)\n\
registrar(decInitialize)\n\
registrar(dlloadRegistar)\n\
registrar(iocshSystemCommand)\n\
registrar(rsrvRegistrar)\n\
registrar(syncInitialize)\n\
registrar(tsInitialize)\n\
registrar(utagInitialize)\n"
);
}
#[test]
fn a_runtime_declaration_appends_even_when_it_sorts_first() {
use std::io::Write;
let (_db, ctx) = make_ctx();
let mut tmp = tempfile::Builder::new().suffix(".dbd").tempfile().unwrap();
writeln!(tmp, "registrar(aaaRuntime)").unwrap();
run(&ctx, "dbLoadDatabase", &[tmp.path().to_str().unwrap()]).unwrap();
let out = run(&ctx, "dbDumpRegistrar", &["pdbbase"]).unwrap();
assert!(
out.ends_with("registrar(utagInitialize)\nregistrar(aaaRuntime)\n"),
"runtime name sorted into the build's block: {out}"
);
}
#[test]
fn built_in_registrars_name_only_features_this_crate_implements() {
use crate::server::database::filters::{FilterParseError, try_parse_filter_chain};
let known = |name: &str| {
!matches!(
try_parse_filter_chain(&format!("{{\"{name}\":{{}}}}")),
Err(FilterParseError::UnknownFilter { .. })
)
};
for filter in ["arr", "dbnd", "dec", "sync", "ts", "utag"] {
assert!(known(filter), "{filter}");
}
assert!(!known("nosuchfilter"));
let mut registry = CommandRegistry::new();
super::super::commands::register_builtins(&mut registry);
for command in ["dlload", "system"] {
assert!(registry.get(command).is_some(), "{command}");
}
}
#[test]
fn the_dbd_declaration_lists_all_take_cs_pdbbase_argument() {
let (_db, ctx) = make_ctx();
for name in [
"dbDumpDriver",
"dbDumpLink",
"dbDumpRegistrar",
"dbDumpVariable",
] {
assert_eq!(
run(&ctx, name, &["nonsense"]).unwrap_err(),
"Expecting 'pdbbase' got 'nonsense'.",
"{name}"
);
}
}
#[test]
fn pvd_table_size_rejects_a_non_power_of_two() {
let (_db, ctx) = make_ctx();
let reg = registry();
let cmd = reg.get("dbPvdTableSize").unwrap();
let outcome = |tokens: &[&str]| {
let tokens: Vec<String> = tokens.iter().map(|t| t.to_string()).collect();
let args = super::super::registry::parse_args(&tokens, &cmd.args).unwrap();
let mut failed = false;
let printed = capture(&ctx, || {
failed = matches!(cmd.handler.call(&args, &ctx), Ok(CommandOutcome::Failed));
});
(printed, failed)
};
assert_eq!(outcome(&["1024"]), (String::new(), false));
assert_eq!(outcome(&["1"]), (String::new(), false));
assert_eq!(outcome(&["2"]), (String::new(), false));
assert_eq!(outcome(&["131072"]), (String::new(), false));
assert_eq!(outcome(&["0"]), (String::new(), false));
assert_eq!(
outcome(&["1000"]),
(
"dbPvdTableSize: 1000 is not a power of 2\n".to_string(),
true
)
);
assert_eq!(
outcome(&["-4"]),
("dbPvdTableSize: -4 is not a power of 2\n".to_string(), true)
);
}
#[test]
fn db_pvd_dump_is_registered_with_cs_arity() {
let reg = registry();
let cmd = reg.get("dbPvdDump").expect("dbPvdDump must be registered");
assert_eq!(cmd.args.len(), 2);
assert_eq!(cmd.args[0].name, "pdbbase");
assert_eq!(cmd.args[1].name, "verbose");
assert!(matches!(cmd.args[1].arg_type, ArgType::Int));
}
#[test]
fn the_directory_is_records_plus_aliases_plus_simple_pvs() {
use crate::server::records::{ai::AiRecord, bo::BoRecord};
use crate::types::EpicsValue;
let (db, ctx) = make_ctx();
ctx.block_on(db.add_record("AI:ONE", Box::new(AiRecord::new(1.0))))
.unwrap();
ctx.block_on(db.add_record("BO:TWO", Box::new(BoRecord::new(0))))
.unwrap();
ctx.block_on(db.add_alias("AI:ALIAS", "AI:ONE")).unwrap();
assert_eq!(
run(&ctx, "dbPvdDump", &["pdbbase"]).unwrap(),
"Process Variable Directory has 3 entries\n"
);
ctx.block_on(db.add_pv("SIMPLE:PV", EpicsValue::Double(0.0)))
.unwrap();
assert_eq!(
run(&ctx, "dbPvdDump", &["pdbbase"]).unwrap(),
"Process Variable Directory has 4 entries\n"
);
}
#[test]
fn verbose_dumps_the_directory_by_name_where_dbl_walks_by_type() {
use crate::server::records::{ai::AiRecord, bo::BoRecord};
use crate::types::EpicsValue;
let (db, ctx) = make_ctx();
ctx.block_on(db.add_record("AI:ONE", Box::new(AiRecord::new(1.0))))
.unwrap();
ctx.block_on(db.add_record("BO:TWO", Box::new(BoRecord::new(0))))
.unwrap();
ctx.block_on(db.add_alias("AI:ALIAS", "AI:ONE")).unwrap();
ctx.block_on(db.add_pv("SIMPLE:PV", EpicsValue::Double(0.0)))
.unwrap();
let dumped = run(&ctx, "dbPvdDump", &["pdbbase", "1"]).unwrap();
assert_eq!(
dumped,
format!(
"Process Variable Directory has 4 entries\n{} AI:ALIAS AI:ONE BO:TWO SIMPLE:PV\n",
" ".repeat(9)
),
"got {dumped:?}"
);
let listed = run(&ctx, "dbl", &[]).unwrap();
assert_eq!(
listed, "AI:ONE\nAI:ALIAS\nBO:TWO\n",
"dbl groups by record type in load order, and the simple PV is \
not a record node"
);
}
#[test]
fn the_name_rows_wrap_at_four_like_cs() {
use crate::server::records::ai::AiRecord;
let (db, ctx) = make_ctx();
for i in 0..5 {
ctx.block_on(db.add_record(&format!("PV:{i}"), Box::new(AiRecord::new(1.0))))
.unwrap();
}
let dumped = run(&ctx, "dbPvdDump", &["pdbbase", "1"]).unwrap();
let lines: Vec<&str> = dumped.lines().collect();
assert_eq!(lines.len(), 3, "got {dumped:?}");
assert_eq!(
lines[1],
format!("{} PV:0 PV:1 PV:2 PV:3", " ".repeat(9))
);
assert_eq!(lines[2], format!("{} PV:4", " ".repeat(9)));
}
#[test]
fn a_negative_verbose_prints_the_names_and_zero_does_not() {
use crate::server::records::ai::AiRecord;
let (db, ctx) = make_ctx();
ctx.block_on(db.add_record("AI:ONE", Box::new(AiRecord::new(1.0))))
.unwrap();
let count = |tokens: &[&str]| run(&ctx, "dbPvdDump", tokens).unwrap().lines().count();
assert_eq!(count(&["pdbbase"]), 1);
assert_eq!(count(&["pdbbase", "0"]), 1);
assert_eq!(count(&["pdbbase", "1"]), 2);
assert_eq!(count(&["pdbbase", "-1"]), 2);
}
#[test]
fn link_fields_print_their_declared_dbf_not_their_served_type() {
let (_db, ctx) = make_ctx();
let flnk = run(&ctx, "dbDumpField", &["pdbbase", "ai", "FLNK"]).unwrap();
assert!(
flnk.contains("\t field_type: DBF_FWDLINK\n"),
"dbCommon.FLNK must be DBF_FWDLINK, got:\n{flnk}"
);
let inp = run(&ctx, "dbDumpField", &["pdbbase", "ai", "INP"]).unwrap();
assert!(
inp.contains("\t field_type: DBF_INLINK\n"),
"ai.INP must be DBF_INLINK, got:\n{inp}"
);
let out = run(&ctx, "dbDumpField", &["pdbbase", "ao", "OUT"]).unwrap();
assert!(out.contains("\t field_type: DBF_OUTLINK\n"), "{out}");
let dtyp = run(&ctx, "dbDumpField", &["pdbbase", "ai", "DTYP"]).unwrap();
assert!(dtyp.contains("\t field_type: DBF_DEVICE\n"), "{dtyp}");
let scan = run(&ctx, "dbDumpField", &["pdbbase", "ai", "SCAN"]).unwrap();
assert!(scan.contains("\t field_type: DBF_MENU\n"), "{scan}");
let a = run(&ctx, "dbDumpField", &["pdbbase", "aSub", "A"]).unwrap();
assert!(a.contains("\t field_type: DBF_NOACCESS\n"), "{a}");
}
#[test]
fn one_field_block_matches_c_line_for_line() {
let (_db, ctx) = make_ctx();
let out = run(&ctx, "dbDumpField", &["pdbbase", "ai", "PREC"]).unwrap();
assert_eq!(
out,
"recordtype(ai) \n\
\x20 PREC\n\
\t indRecordType: 50\n\
\t special: 0 \n\
\t field_type: DBF_SHORT\n\
\tprocess_passive: 0\n\
\t property: 1\n\
\t interest: 1\n\
\t as_level: 1\n\
\t initial: \n\
Attribute: name RTYP value ai\n\
Attribute: name VERS value none specified\n",
"got:\n{out:?}"
);
}
#[test]
fn ind_record_type_is_c_s_papflddes_index() {
let (_db, ctx) = make_ctx();
let ind = |field: &str| {
let out = run(&ctx, "dbDumpField", &["pdbbase", "ai", field]).unwrap();
out.lines()
.find_map(|l| l.strip_prefix("\t indRecordType: "))
.unwrap_or_else(|| panic!("no indRecordType for {field}: {out}"))
.to_string()
};
assert_eq!(ind("NAME"), "0");
assert_eq!(ind("DISP"), "16");
assert_eq!(ind("VAL"), "48");
assert_eq!(ind("INP"), "49");
assert_eq!(ind("PREC"), "50");
}
#[test]
fn special_prints_c_s_number_and_name() {
assert_eq!(c_special_code(Special::None), 0);
assert_eq!(c_special_code(Special::NoMod), 1);
assert_eq!(c_special_code(Special::DbAddr), 2);
assert_eq!(c_special_code(Special::Scan), 3);
assert_eq!(c_special_code(Special::AlarmAck), 5);
assert_eq!(c_special_code(Special::As), 6);
assert_eq!(c_special_code(Special::Attribute), 7);
assert_eq!(c_special_code(Special::Mod), 100);
assert_eq!(c_special_code(Special::Reset), 101);
assert_eq!(c_special_code(Special::LinConv), 102);
assert_eq!(c_special_code(Special::Calc), 103);
assert_eq!(c_special_name(7), None);
assert_eq!(c_special_name(5), Some("SPC_ALARMACK"));
assert_eq!(c_special_name(102), Some("SPC_LINCONV"));
let (_db, ctx) = make_ctx();
let out = run(&ctx, "dbDumpField", &["pdbbase", "ai", "SCAN"]).unwrap();
assert!(out.contains("\t special: 3 SPC_SCAN\n"), "{out}");
let acks = run(&ctx, "dbDumpField", &["pdbbase", "ai", "ACKS"]).unwrap();
assert!(acks.contains("\t special: 1 SPC_NOMOD\n"), "{acks}");
}
#[test]
fn dbdumpregistrar_lists_the_dbd_declarations_deduped_in_load_order() {
use std::io::Write;
let (_db, ctx) = make_ctx();
let mut tmp = tempfile::Builder::new().suffix(".dbd").tempfile().unwrap();
writeln!(
tmp,
"registrar(zzRegB)\nregistrar(zzRegA)\nregistrar(zzRegB)"
)
.unwrap();
let path = tmp.path().to_str().unwrap().to_string();
run(&ctx, "dbLoadDatabase", &[&path]).unwrap();
let out = run(&ctx, "dbDumpRegistrar", &["pdbbase"]).unwrap();
assert_eq!(out.matches("registrar(zzRegB)").count(), 1, "{out}");
assert_eq!(out.matches("registrar(zzRegA)").count(), 1, "{out}");
assert!(
out.contains("registrar(zzRegB)\nregistrar(zzRegA)\n"),
"first-declaration order, not sorted: {out}"
);
}
#[test]
fn dbputattribute_lands_in_the_block_in_strcmp_order() {
let (_db, ctx) = make_ctx();
let reg = registry();
let cmd = reg.get("dbPutAttribute").unwrap();
let tokens: Vec<String> = ["ai", "MYATTR", "hello"]
.iter()
.map(|t| t.to_string())
.collect();
let args = super::super::registry::parse_args(&tokens, &cmd.args).unwrap();
capture(&ctx, || {
cmd.handler.call(&args, &ctx).unwrap();
});
let out = run(&ctx, "dbDumpField", &["pdbbase", "ai", "NOSUCH"]).unwrap();
assert_eq!(
out,
"recordtype(ai) \n\
Attribute: name MYATTR value hello\n\
Attribute: name RTYP value ai\n\
Attribute: name VERS value none specified\n"
);
let other = run(&ctx, "dbDumpField", &["pdbbase", "bo", "NOSUCH"]).unwrap();
assert_eq!(
other,
"recordtype(bo) \n\
Attribute: name RTYP value bo\n\
Attribute: name VERS value none specified\n"
);
}
#[test]
fn the_type_argument_is_strcmp_not_a_wildcard() {
let (_db, ctx) = make_ctx();
assert_eq!(run(&ctx, "dbDumpField", &["pdbbase", ""]).unwrap(), "");
assert_eq!(run(&ctx, "dbDumpField", &["pdbbase", "*"]).unwrap(), "");
let all = run(&ctx, "dbDumpField", &["pdbbase"]).unwrap();
assert!(all.contains("recordtype(ai) \n"), "every type is walked");
assert!(all.contains("recordtype(bo) \n"), "every type is walked");
let one = run(&ctx, "dbDumpField", &["pdbbase", "ai"]).unwrap();
assert_eq!(one.matches("recordtype(").count(), 1, "{one}");
let none = run(&ctx, "dbDumpField", &["pdbbase", "ai", "NOSUCH"]).unwrap();
assert_eq!(
none,
"recordtype(ai) \n\
Attribute: name RTYP value ai\n\
Attribute: name VERS value none specified\n"
);
}
#[test]
fn size_is_c_struct_layout_and_is_not_printed() {
let (_db, ctx) = make_ctx();
for field in ["DESC", "VAL", "INP"] {
let out = run(&ctx, "dbDumpField", &["pdbbase", "ai", field]).unwrap();
assert!(!out.contains("size:"), "{field}: {out}");
}
}
#[test]
fn device_config_reports_the_hardware_link() {
let (_db, ctx) = make_ctx();
load(
&ctx,
"record(ai,\"AI:HW\") { field(DTYP,\"Soft Timestamp\") field(INP,\"@a b\") \
field(LINR,\"LINEAR\") field(EGUL,\"1\") field(EGUF,\"10\") }\n",
);
let out = run(&ctx, "dbReportDeviceConfig", &["pdbbase"]).unwrap();
assert_eq!(
out,
format!(
"{:<8} {:<20} {:<20} {:<20} {}\n",
"INST", "@a b", "Soft Timestamp", "AI:HW", "cvt(1,10)"
),
"got:\n{out}"
);
assert_eq!(run(&ctx, "dbhcr", &[]).unwrap(), out);
}
#[test]
fn device_config_cvt_column_is_dbgetstring_not_dbr_string() {
let (_db, ctx) = make_ctx();
load(
&ctx,
"record(ai,\"AI:CVT\") { field(DTYP,\"Soft Timestamp\") field(INP,\"@\") \
field(LINR,\"LINEAR\") field(EGUL,\"1e7\") field(EGUF,\"1e-3\") field(PREC,\"3\") }\n",
);
let out = run(&ctx, "dbReportDeviceConfig", &["pdbbase"]).unwrap();
assert!(out.ends_with("cvt(10000000,1.0e-03)\n"), "got:\n{out}");
}
#[test]
fn device_config_skips_soft_records() {
let (_db, ctx) = make_ctx();
load(
&ctx,
"record(ai,\"AI:SOFT\") { field(INP,\"OTHER.VAL\") }\n\
record(bi,\"BI:CONST\") { field(INP,\"1\") }\n",
);
assert_eq!(run(&ctx, "dbReportDeviceConfig", &["pdbbase"]).unwrap(), "");
}
#[test]
fn device_config_leaves_the_conversion_column_empty() {
let (_db, ctx) = make_ctx();
load(
&ctx,
"record(ai,\"AI:NOLIN\") { field(DTYP,\"Soft Timestamp\") field(INP,\"@x\") }\n",
);
let out = run(&ctx, "dbReportDeviceConfig", &["pdbbase"]).unwrap();
assert!(out.ends_with("AI:NOLIN \n"), "got:\n{out:?}");
}
#[test]
fn hardware_bus_names_follow_db_parse_link() {
let bus = |text: &str| match crate::server::record::parse_link_field(
text,
crate::server::record::LinkFieldType::In,
) {
ParsedLink::Hw(hw) => c_hw_bus(&hw),
_ => None,
};
assert_eq!(bus("@dev parm"), Some("INST"));
assert_eq!(bus(" @dev parm "), Some("INST"));
assert_eq!(bus("#C1 S2 @parm"), Some("VME"));
assert_eq!(bus("#B1 C2 N3"), Some("CAMAC"));
assert_eq!(bus("#B1 C2 N3 A4"), Some("CAMAC"));
assert_eq!(bus("#B1 C2 N3 A4 F5"), Some("CAMAC"));
assert_eq!(bus("#L1 A2 C3 S4 @p"), Some("AB"));
assert_eq!(bus("#L1 A2 @p"), Some("GPIB"));
assert_eq!(bus("#L1 N2 P3 S4 @p"), Some("BITBUS"));
assert_eq!(bus("#L1 B2 G3 @p"), Some("BBGPIB"));
assert_eq!(bus("#V1 C2 S3 @p"), Some("VXI"));
assert_eq!(bus("#V1 S2 @p"), Some("VXI"));
assert_eq!(bus("#C0x10 S-2"), Some("VME"));
assert_eq!(bus("#R1 M2 D3 E4"), None);
assert_eq!(bus("OTHER.VAL"), None);
assert_eq!(bus("1.5"), None);
assert_eq!(bus("{const:1}"), None);
assert_eq!(bus(""), None);
assert_eq!(bus("#C S2"), None);
assert_eq!(bus("#nonsense"), None);
assert_eq!(bus("#A1 B2 C3 D4 E5 F6"), None);
}
#[test]
fn a_hardware_links_type_word_is_its_bus() {
let word = |text: &str| {
crate::server::record::parse_link_field(text, crate::server::record::LinkFieldType::In)
.link_type_after_init(text)
.c_name()
};
assert_eq!(word("@dev p1 p2"), "INST_IO");
assert_eq!(word("#C1 S2 @parm"), "VME_IO");
assert_eq!(word("#B1 C2 N3 A4 F5"), "CAMAC_IO");
assert_eq!(word("#R1 M2 D3 E4"), "RF_IO");
assert_eq!(word("#L1 A2 C3 S4 @p"), "AB_IO");
assert_eq!(word("#L1 A2 @p"), "GPIB_IO");
assert_eq!(word("#L1 N2 P3 S4 @p"), "BITBUS_IO");
assert_eq!(word("#L1 B2 G3 @p"), "BBGPIB_IO");
assert_eq!(word("#V1 C2 S3 @p"), "VXI_IO");
assert_eq!(word("#V1 S2 @p"), "VXI_IO");
assert_eq!(word("#nonsense"), "CONSTANT");
assert_eq!(word("#C S2"), "CONSTANT");
assert_eq!(word("#A1 B2 C3 D4 E5 F6"), "CONSTANT");
assert_eq!(word("#R1 M2 D3 E4 @p"), "CONSTANT");
}
#[test]
fn pdbbase_argument_refuses_anything_else() {
assert!(check_pdbbase(&ArgValue::String("pdbbase".into())).is_ok());
assert!(check_pdbbase(&ArgValue::String(String::new())).is_ok());
assert!(check_pdbbase(&ArgValue::String("0".into())).is_ok());
assert!(check_pdbbase(&ArgValue::Missing).is_ok());
assert_eq!(
check_pdbbase(&ArgValue::String("pdbase".into())).unwrap_err(),
"Expecting 'pdbbase' got 'pdbase'."
);
}
#[test]
fn escaping_matches_epics_str_print_escaped() {
assert_eq!(print_escaped("plain"), "plain");
assert_eq!(print_escaped("a\tb\nc"), "a\\tb\\nc");
assert_eq!(print_escaped("q\"uote"), "q\\\"uote");
assert_eq!(print_escaped("back\\slash"), "back\\\\slash");
assert_eq!(print_escaped("tick'"), "tick\\'");
assert_eq!(print_escaped("\x07\x08\x0c\x0b\r"), "\\a\\b\\f\\v\\r");
assert_eq!(print_escaped("\u{0}"), "\\x00");
assert_eq!(print_escaped("\u{1}"), "\\x01");
assert_eq!(print_escaped("é"), "\\xc3\\xa9");
assert_eq!(print_escaped("with space"), "with space");
}
#[test]
fn default_value_follows_the_dbd_initial() {
use crate::server::record::{Asl, Base, FieldDesc, Special};
use crate::types::{DbFieldType, DbfCode, EpicsValue};
let desc = |initial, menu| FieldDesc {
name: "F",
dbf_type: DbFieldType::Long,
declared_dbf: DbfCode::Long,
runtime_typed: false,
read_only: false,
special: Special::None,
declared_special: Special::None,
pp: false,
asl: Asl::Asl1,
size: 0,
extra: None,
menu,
initial,
interest: 0,
prop: false,
prompt: None,
promptgroup: None,
base: Base::Decimal,
};
assert!(is_default_value(&desc(None, None), &EpicsValue::Long(0)));
assert!(!is_default_value(&desc(None, None), &EpicsValue::Long(1)));
assert!(is_default_value(
&desc(None, None),
&EpicsValue::String("".into())
));
assert!(is_default_value(
&desc(None, None),
&EpicsValue::Double(0.0)
));
assert!(is_default_value(
&desc(Some("5"), None),
&EpicsValue::Long(5)
));
assert!(!is_default_value(
&desc(Some("5"), None),
&EpicsValue::Long(0)
));
assert!(is_default_value(
&desc(Some("0x10"), None),
&EpicsValue::Long(16)
));
assert!(is_default_value(
&desc(Some("020"), None),
&EpicsValue::Long(16)
));
static CHOICES: &[&str] = &["NO", "YES"];
assert!(is_default_value(
&desc(Some("YES"), Some(CHOICES)),
&EpicsValue::Enum(1)
));
assert!(!is_default_value(
&desc(Some("YES"), Some(CHOICES)),
&EpicsValue::Enum(0)
));
assert!(is_default_value(
&desc(Some("1"), Some(CHOICES)),
&EpicsValue::Enum(1)
));
}
}