use super::registry::*;
use crate::server::db_loader;
use crate::server::driver_support;
use crate::server::record::dbd_generated::RECORD_TYPES;
use crate::server::record::merged_device_menu;
pub(crate) fn register(registry: &mut CommandRegistry) {
registry.register(cmd_registry_record_type_find());
registry.register(cmd_registry_device_support_find());
registry.register(cmd_registry_driver_support_find());
registry.register(cmd_registry_function_find());
registry.register(cmd_registry_dump());
registry.register(cmd_register_all_record_device_drivers());
}
const RECORD_TYPE_ID: &str = "record type";
const DEVICE_SUPPORT_ID: &str = "device support";
const DRIVER_SUPPORT_ID: &str = "driver support";
const FUNCTION_ID: &str = "function";
const IOCSH_CMD_ID: &str = "iocshCmd";
const IOCSH_VAR_ID: &str = "iocshVar";
fn record_type_entries() -> Vec<(String, usize)> {
let mut entries = db_loader::registered_record_type_entries();
for name in RECORD_TYPES {
if !entries.iter().any(|(n, _)| n.as_str() == *name) {
entries.push(((*name).to_string(), name.as_ptr() as usize));
}
}
entries
}
fn device_support_entries() -> Vec<(String, usize)> {
let mut entries: Vec<(String, usize)> = Vec::new();
for record_type in RECORD_TYPES {
for dtyp in merged_device_menu(record_type) {
if !entries.iter().any(|(n, _)| n.as_str() == dtyp) {
entries.push((dtyp.to_string(), dtyp.as_ptr() as usize));
}
}
}
entries
}
fn driver_support_entries() -> Vec<(String, usize)> {
driver_support::driver_support_entries()
}
fn iocsh_var_entries() -> Vec<(String, usize)> {
super::vars::variable_names()
.into_iter()
.map(|name| (name.to_string(), name.as_ptr() as usize))
.collect()
}
fn format_entry_address(address: Option<usize>) -> String {
match address {
Some(a) => format!("{:#x}", a),
None => "(nil)".to_string(),
}
}
fn find_command(
name: &'static str,
usage: &'static str,
lookup: fn(&CommandContext, &str) -> Option<usize>,
) -> CommandDef {
CommandDef::new(
name,
vec![ArgDesc {
name: "name",
arg_type: ArgType::String,
}],
usage,
move |args: &[ArgValue], ctx: &CommandContext| {
let found = match &args[0] {
ArgValue::String(s) => lookup(ctx, s),
_ => None,
};
ctx.println(&format_entry_address(found));
Ok(CommandOutcome::Continue)
},
)
}
fn cmd_registry_record_type_find() -> CommandDef {
find_command(
"registryRecordTypeFind",
"registryRecordTypeFind <name> — Prints the registry address of the \
record type given as first argument.",
|_ctx, wanted| {
record_type_entries()
.into_iter()
.find(|(n, _)| n == wanted)
.map(|(_, a)| a)
},
)
}
fn cmd_registry_device_support_find() -> CommandDef {
find_command(
"registryDeviceSupportFind",
"registryDeviceSupportFind <name> — Prints the registry address of \
the device support given as first argument.",
|_ctx, wanted| {
device_support_entries()
.into_iter()
.find(|(n, _)| n == wanted)
.map(|(_, a)| a)
},
)
}
fn cmd_registry_driver_support_find() -> CommandDef {
find_command(
"registryDriverSupportFind",
"registryDriverSupportFind <name> — Prints the registry address of \
the driver support given as first argument.",
|_ctx, wanted| {
driver_support_entries()
.into_iter()
.find(|(n, _)| n == wanted)
.map(|(_, a)| a)
},
)
}
fn cmd_registry_function_find() -> CommandDef {
find_command(
"registryFunctionFind",
"registryFunctionFind <name> — Prints the registry address of the \
registered function given as first argument.",
|ctx, wanted| {
ctx.db()
.subroutine_entries()
.into_iter()
.find(|(n, _)| n == wanted)
.map(|(_, a)| a)
},
)
}
fn cmd_registry_dump() -> CommandDef {
CommandDef::new(
"registryDump",
vec![],
"registryDump — Dump a hash table of EPICS registry",
|_args: &[ArgValue], ctx: &CommandContext| {
let sections: [(&str, Vec<(String, usize)>); 6] = [
(RECORD_TYPE_ID, record_type_entries()),
(DEVICE_SUPPORT_ID, device_support_entries()),
(DRIVER_SUPPORT_ID, driver_support_entries()),
(FUNCTION_ID, ctx.db().subroutine_entries()),
(IOCSH_CMD_ID, ctx.command_entries()),
(IOCSH_VAR_ID, iocsh_var_entries()),
];
let total: usize = sections.iter().map(|(_, e)| e.len()).sum();
ctx.println(&format!("Registry has {total} entries"));
for (id, mut entries) in sections {
entries.sort_by(|a, b| a.0.cmp(&b.0));
ctx.println(&format!(" {:16} {:3}", id, entries.len()));
for chunk in entries.chunks(3) {
let mut line = String::from(" ");
for (name, address) in chunk {
line.push_str(&format!(" {} {:#x}", name, address));
}
ctx.println(&line);
}
}
Ok(CommandOutcome::Continue)
},
)
}
fn cmd_register_all_record_device_drivers() -> CommandDef {
CommandDef::new(
"registerAllRecordDeviceDrivers",
vec![ArgDesc {
name: "pdbbase",
arg_type: ArgType::String,
}],
"registerAllRecordDeviceDrivers pdbbase — Register all records, \
devices, from all DBD available.",
|args: &[ArgValue], _ctx: &CommandContext| {
super::commands::check_pdbbase(&args[0])?;
Ok(CommandOutcome::Continue)
},
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::server::database::PvDatabase;
use std::collections::HashMap;
use std::sync::Arc;
fn make_ctx() -> 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, bridge);
std::mem::forget(rt);
ctx
}
fn run(ctx: &CommandContext, name: &str, tokens: &[&str]) -> String {
let mut reg = CommandRegistry::new();
register(&mut reg);
let cmd = reg.get(name).unwrap_or_else(|| panic!("{name} registered"));
let tokens: Vec<String> = tokens.iter().map(|t| (*t).to_string()).collect();
let args = parse_args(&tokens, &cmd.args).expect("arguments parse");
let tmp = tempfile::NamedTempFile::new().unwrap();
let path = tmp.path().to_path_buf();
ctx.with_output(std::fs::File::create(&path).unwrap(), || {
let _ = cmd.handler.call(&args, ctx);
});
std::fs::read_to_string(&path).unwrap()
}
#[test]
fn the_family_registers_every_name_c_does() {
let mut reg = CommandRegistry::new();
register(&mut reg);
for name in [
"registryRecordTypeFind",
"registryDeviceSupportFind",
"registryDriverSupportFind",
"registryFunctionFind",
"registryDump",
"registerAllRecordDeviceDrivers",
] {
assert!(reg.get(name).is_some(), "C registers {name}");
}
assert!(
reg.displaced().is_empty(),
"the family must not claim one name twice: {:?}",
reg.displaced()
);
}
#[test]
fn a_built_in_record_type_is_found_and_an_unknown_one_is_nil() {
let ctx = make_ctx();
assert_ne!(
run(&ctx, "registryRecordTypeFind", &["ai"]).trim(),
"(nil)",
"ai is a registered record type"
);
assert_eq!(
run(&ctx, "registryRecordTypeFind", &["nosuchrecordtype"]).trim(),
"(nil)"
);
}
#[test]
fn an_externally_registered_record_type_is_found() {
let ctx = make_ctx();
let probe = "registryFindProbeType";
assert_eq!(
run(&ctx, "registryRecordTypeFind", &[probe]).trim(),
"(nil)",
"unregistered before the probe registers it"
);
crate::server::db_loader::register_record_type(
probe,
Box::new(|| Box::new(crate::server::records::ai::AiRecord::default())),
);
assert_ne!(
run(&ctx, "registryRecordTypeFind", &[probe]).trim(),
"(nil)"
);
}
#[test]
fn a_declared_device_support_is_found_and_an_unknown_one_is_nil() {
let ctx = make_ctx();
assert_ne!(
run(&ctx, "registryDeviceSupportFind", &["Soft Channel"]).trim(),
"(nil)",
"base declares Soft Channel for ai"
);
assert_eq!(
run(&ctx, "registryDeviceSupportFind", &["nosuchdevicesupport"]).trim(),
"(nil)"
);
}
#[test]
fn a_registered_driver_support_is_found_and_an_unknown_one_is_nil() {
let ctx = make_ctx();
struct Probe;
impl crate::server::driver_support::DriverSupport for Probe {
fn report(&self, _level: i32) -> Option<String> {
None
}
}
let probe = "drvRegistryFindProbe";
assert_eq!(
run(&ctx, "registryDriverSupportFind", &[probe]).trim(),
"(nil)",
"unregistered before the probe registers it"
);
crate::server::driver_support::register_driver_support(probe, std::sync::Arc::new(Probe));
assert_ne!(
run(&ctx, "registryDriverSupportFind", &[probe]).trim(),
"(nil)"
);
assert_eq!(
run(&ctx, "registryDriverSupportFind", &["drvNoSuchDriver"]).trim(),
"(nil)"
);
}
#[test]
fn a_registered_subroutine_is_found_and_an_unknown_one_is_nil() {
let ctx = make_ctx();
assert_eq!(
run(&ctx, "registryFunctionFind", &["mySub"]).trim(),
"(nil)",
"nothing is registered yet"
);
let mut registry: HashMap<String, Arc<crate::server::record::SubroutineFn>> =
HashMap::new();
registry.insert(
"mySub".to_string(),
Arc::new(Box::new(|_r: &mut dyn crate::server::record::Record| Ok(0))),
);
ctx.block_on(ctx.db().install_subroutine_registry(registry));
assert_ne!(
run(&ctx, "registryFunctionFind", &["mySub"]).trim(),
"(nil)"
);
assert_eq!(
run(&ctx, "registryFunctionFind", &["nosuchfunction"]).trim(),
"(nil)"
);
}
#[test]
fn registry_dump_names_every_id_and_totals_them() {
let ctx = make_ctx();
let out = run(&ctx, "registryDump", &[]);
for id in [
RECORD_TYPE_ID,
DEVICE_SUPPORT_ID,
DRIVER_SUPPORT_ID,
FUNCTION_ID,
IOCSH_CMD_ID,
IOCSH_VAR_ID,
] {
assert!(out.contains(id), "registryDump must name {id}: {out:.400}");
}
let expected = record_type_entries().len()
+ device_support_entries().len()
+ driver_support_entries().len()
+ ctx.db().subroutine_entries().len()
+ ctx.command_entries().len()
+ iocsh_var_entries().len();
assert!(
out.starts_with(&format!("Registry has {expected} entries")),
"the header counts every entry the sections list: {out:.200}"
);
assert!(out.contains(" ai 0x"), "an entry prints name then address");
}
#[test]
fn the_iocsh_cmd_registry_follows_the_shell_that_owns_the_context() {
let standalone = make_ctx();
assert!(
standalone.command_entries().is_empty(),
"no shell owns this context, so it has registered no commands"
);
let rt = tokio::runtime::Runtime::new().unwrap();
let bridge = {
let _guard = rt.enter();
crate::runtime::task::BlockingBridge::capture()
};
let shell = crate::server::iocsh::IocShell::new(Arc::new(PvDatabase::new()), bridge);
let tmp = tempfile::NamedTempFile::new().unwrap();
let path = tmp.path().to_path_buf();
let _ = shell.execute_line(&format!("registryDump > {}", path.display()));
std::mem::forget(rt);
let out = std::fs::read_to_string(&path).unwrap();
let (_, cmds) = out
.split_once(IOCSH_CMD_ID)
.expect("the dump has an iocshCmd section");
assert!(
cmds.contains(" registryDump 0x"),
"a shell's own context must see the shell's commands: {cmds:.300}"
);
assert!(
cmds.contains(" dbl 0x"),
"and every other built-in, not just this family: {cmds:.300}"
);
}
#[test]
fn register_all_record_device_drivers_takes_pdbbase_and_refuses_anything_else() {
let ctx = make_ctx();
let mut reg = CommandRegistry::new();
register(&mut reg);
let cmd = reg.get("registerAllRecordDeviceDrivers").unwrap();
for accepted in ["pdbbase", "0"] {
let args = parse_args(&[accepted.to_string()], &cmd.args).unwrap();
assert!(
matches!(cmd.handler.call(&args, &ctx), Ok(CommandOutcome::Continue)),
"C accepts {accepted}"
);
}
let args = parse_args(&["junk".to_string()], &cmd.args).unwrap();
assert_eq!(
cmd.handler.call(&args, &ctx).err(),
Some("Expecting 'pdbbase' got 'junk'.".to_string())
);
}
}