use super::registry::*;
pub(crate) fn register(registry: &mut CommandRegistry) {
registry.register(cmd_comment());
registry.register(cmd_echo());
registry.register(cmd_date());
registry.register(cmd_cd());
registry.register(cmd_pwd());
registry.register(cmd_epics_env_unset());
registry.register(cmd_epics_env_show());
registry.register(cmd_epics_prt_env_params());
registry.register(cmd_epics_param_show());
registry.register(cmd_epics_thread_sleep());
registry.register(cmd_epics_thread_show_all());
registry.register(cmd_epics_thread_show());
registry.register(cmd_epics_thread_resume());
registry.register(cmd_taskwd_show());
registry.register(cmd_epics_mutex_show_all());
registry.register(cmd_install_last_resort_event_provider());
registry.register(cmd_general_time_report());
registry.register(cmd_iocsh_cmd());
registry.register(cmd_iocsh_run());
registry.register(cmd_iocsh_load());
registry.register(cmd_on());
registry.register(cmd_eltc());
registry.register(cmd_errlog_init());
registry.register(cmd_errlog_init2());
registry.register(cmd_errlog_show());
registry.register(cmd_errlog());
registry.register(cmd_ioc_log_init());
registry.register(cmd_ioc_log_prefix());
registry.register(cmd_ioc_log_show());
registry.register(cmd_set_ioc_log_disable());
update_pwd();
}
fn update_pwd() {
let Ok(cwd) = std::env::current_dir() else {
return;
};
super::iocsh_env_clear("PWD");
unsafe { std::env::set_var("PWD", cwd) };
}
pub(super) fn set_working_dir(dir: impl AsRef<std::path::Path>) -> std::io::Result<()> {
std::env::set_current_dir(dir)?;
update_pwd();
Ok(())
}
fn cmd_general_time_report() -> CommandDef {
CommandDef::new(
"generalTimeReport",
vec![ArgDesc {
name: "interest_level",
arg_type: ArgType::Int,
}],
concat!(
"Display time providers information for given interest level.\n",
"interest level 0 - List providers and their priorities.\n",
" 1 - Additionally show current time obtained from each provider.",
),
|args: &[ArgValue], ctx: &CommandContext| {
let level = match args.first() {
Some(ArgValue::Int(n)) => *n as i32,
_ => 0,
};
let report = crate::runtime::general_time::report(level);
let body = report.strip_suffix('\n').unwrap_or(&report);
ctx.print_fmt(format_args!("{body}"));
Ok(CommandOutcome::Continue)
},
)
}
fn cmd_install_last_resort_event_provider() -> CommandDef {
CommandDef::new(
"installLastResortEventProvider",
vec![],
concat!(
"Installs the optional Last Resort event provider at priority 999,\n",
"which returns the current time for every event number",
),
|_args: &[ArgValue], _ctx: &CommandContext| {
crate::runtime::general_time::install_last_resort_event_provider();
Ok(CommandOutcome::Continue)
},
)
}
fn shell_owned(name: &'static str) -> impl Fn(&[ArgValue], &CommandContext) -> CommandResult {
move |_args: &[ArgValue], _ctx: &CommandContext| {
Err(format!(
"{name} must be run by the shell, not through the command table"
))
}
}
fn cmd_iocsh_cmd() -> CommandDef {
CommandDef::new(
"iocshCmd",
vec![ArgDesc {
name: "command",
arg_type: ArgType::String,
}],
concat!(
"Takes a single IOC shell command and executes it\n",
" * This function is most useful to execute a single IOC shell command\n",
" from vxWorks or RTEMS startup script (or command line)",
),
shell_owned("iocshCmd"),
)
}
fn cmd_iocsh_run() -> CommandDef {
CommandDef::new(
"iocshRun",
vec![
ArgDesc {
name: "command",
arg_type: ArgType::String,
},
ArgDesc {
name: "macros",
arg_type: ArgType::String,
},
],
concat!(
"Takes a single IOC shell command, replaces macros and executes it\n",
" * This function is most useful to execute a single IOC shell command\n",
" from vxWorks or RTEMS startup script (or command line)",
),
shell_owned("iocshRun"),
)
}
fn cmd_iocsh_load() -> CommandDef {
CommandDef::new(
"iocshLoad",
vec![
ArgDesc {
name: "pathname",
arg_type: ArgType::Path,
},
ArgDesc {
name: "macros",
arg_type: ArgType::String,
},
],
concat!(
"Execute IOC shell commands provided in file from first parameter\n",
" * (optional) replace macros within the file with provided values",
),
shell_owned("iocshLoad"),
)
}
fn cmd_on() -> CommandDef {
CommandDef::new(
"on",
vec![ArgDesc {
name: "error [continue | break | halt | wait <delay>]",
arg_type: ArgType::Argv,
}],
concat!(
"Change IOC shell error handling.\n",
" continue (default) - Ignores error and continue with next commands.\n",
" break - Return to caller without executing further commands.\n",
" halt - Suspend process.\n",
" wait - stall process for <delay> seconds, then continue.",
),
shell_owned("on"),
)
}
fn cmd_eltc() -> CommandDef {
CommandDef::new(
"eltc",
vec![ArgDesc {
name: "(0,1)=>(false,true)",
arg_type: ArgType::Int,
}],
concat!(
"Control display of error log messages on console\n",
" 0 - no\n",
" 1 - yes (default)",
),
|args: &[ArgValue], _ctx: &CommandContext| {
let yes = matches!(args.first(), Some(ArgValue::Int(v)) if *v != 0);
crate::runtime::log::eltc(yes);
Ok(CommandOutcome::Continue)
},
)
}
fn cmd_errlog_init() -> CommandDef {
CommandDef::new(
"errlogInit",
vec![ArgDesc {
name: "bufSize",
arg_type: ArgType::Int,
}],
concat!(
"Initialize error log client buffer size\n",
" bufSize - size of circular buffer (default = 1280 bytes)",
),
|args: &[ArgValue], _ctx: &CommandContext| {
let bufsize = int_arg(args.first()).max(0) as usize;
crate::runtime::log::errlog_init(bufsize);
Ok(CommandOutcome::Continue)
},
)
}
fn cmd_errlog_init2() -> CommandDef {
CommandDef::new(
"errlogInit2",
vec![
ArgDesc {
name: "bufSize",
arg_type: ArgType::Int,
},
ArgDesc {
name: "maxMsgSize",
arg_type: ArgType::Int,
},
],
concat!(
"Initialize error log client buffer size and maximum message size\n",
" bufSize - size of circular buffer (default = 1280 bytes)\n",
" maxMsgSize - maximum size of error message (default = 256 bytes)",
),
|args: &[ArgValue], _ctx: &CommandContext| {
let bufsize = int_arg(args.first()).max(0) as usize;
let max_msg = int_arg(args.get(1)).max(0) as usize;
crate::runtime::log::errlog_init2(bufsize, max_msg);
Ok(CommandOutcome::Continue)
},
)
}
fn cmd_errlog_show() -> CommandDef {
CommandDef::new(
"errlogShow",
vec![ArgDesc {
name: "level",
arg_type: ArgType::Int,
}],
concat!(
"Show the contents of the error log private data\n",
"level 0 - Show the size of the buffers and max message size\n",
" 1 - Show the number of listeners\n",
" 2 - Show the contents of the buffer",
),
|args: &[ArgValue], ctx: &CommandContext| {
let level = int_arg(args.first()).max(0) as u32;
for line in crate::runtime::log::errlog_show(level) {
ctx.println(&line);
}
Ok(CommandOutcome::Continue)
},
)
}
fn cmd_errlog() -> CommandDef {
CommandDef::new(
"errlog",
vec![ArgDesc {
name: "message",
arg_type: ArgType::String,
}],
"Send message to errlog",
|args: &[ArgValue], _ctx: &CommandContext| {
let message = match args.first() {
Some(ArgValue::String(s)) => s.clone(),
_ => String::new(),
};
crate::runtime::log::errlog_printf_no_console(&format!("{message}\n"));
crate::runtime::log::errlog_flush();
Ok(CommandOutcome::Continue)
},
)
}
fn cmd_ioc_log_init() -> CommandDef {
CommandDef::new(
"iocLogInit",
vec![],
concat!(
"Initialize IOC logging\n",
" * EPICS environment variable 'EPICS_IOC_LOG_INET' has to be defined\n",
" * Logging controlled via 'iocLogDisable' variable\n",
" see 'setIocLogDisable' command",
),
|_args: &[ArgValue], _ctx: &CommandContext| match crate::runtime::log_client::ioc_log_init()
{
Ok(()) => Ok(CommandOutcome::Continue),
Err(_) => Ok(CommandOutcome::Failed),
},
)
}
fn cmd_ioc_log_prefix() -> CommandDef {
CommandDef::new(
"iocLogPrefix",
vec![ArgDesc {
name: "prefix",
arg_type: ArgType::String,
}],
"Create the prefix for all messages going into IOC log",
|args: &[ArgValue], _ctx: &CommandContext| {
if let Some(ArgValue::String(prefix)) = args.first() {
crate::runtime::log_client::ioc_log_prefix(prefix);
}
Ok(CommandOutcome::Continue)
},
)
}
fn cmd_ioc_log_show() -> CommandDef {
CommandDef::new(
"iocLogShow",
vec![ArgDesc {
name: "level",
arg_type: ArgType::Int,
}],
"Determine if a IOC Log Prefix has been set",
|args: &[ArgValue], ctx: &CommandContext| {
let level = int_arg(args.first()).max(0) as u32;
for line in crate::runtime::log_client::ioc_log_show(level) {
ctx.println(&line);
}
Ok(CommandOutcome::Continue)
},
)
}
fn cmd_set_ioc_log_disable() -> CommandDef {
CommandDef::new(
"setIocLogDisable",
vec![ArgDesc {
name: "(0,1)=>(false,true)",
arg_type: ArgType::Int,
}],
concat!(
"Controls the 'iocLogDisable' variable\n",
" 0 - enable logging\n",
" 1 - disable logging",
),
|args: &[ArgValue], _ctx: &CommandContext| {
crate::runtime::log_client::set_ioc_log_disable(int_arg(args.first()) != 0);
Ok(CommandOutcome::Continue)
},
)
}
fn int_arg(arg: Option<&ArgValue>) -> i64 {
match arg {
Some(ArgValue::Int(v)) => *v,
_ => 0,
}
}
fn cmd_comment() -> CommandDef {
CommandDef::new(
"#",
vec![ArgDesc {
name: "text",
arg_type: ArgType::String,
}],
"",
|_args: &[ArgValue], _ctx: &CommandContext| Ok(CommandOutcome::Continue),
)
}
fn cmd_echo() -> CommandDef {
CommandDef::new(
"echo",
vec![ArgDesc {
name: "string",
arg_type: ArgType::String,
}],
"Print string after expanding macros and environment variables",
|args: &[ArgValue], ctx: &CommandContext| {
match &args[0] {
ArgValue::String(s) => {
ctx.println_bytes(&crate::runtime::epics_string::raw_from_escaped(s))
}
_ => ctx.println(""),
}
Ok(CommandOutcome::Continue)
},
)
}
fn epics_strftime_to_chrono(fmt: &str, now: &chrono::DateTime<chrono::Local>) -> String {
use std::fmt::Write as _;
let renders = |spec: &str| {
let mut probe = String::new();
write!(probe, "{}", now.format(spec)).is_ok()
};
let c: Vec<char> = fmt.chars().collect();
let mut out = String::with_capacity(fmt.len());
let mut i = 0;
while i < c.len() {
if c[i] != '%' {
out.push(c[i]);
i += 1;
continue;
}
let mut j = i + 1;
if j < c.len() && c[j] == '%' {
out.push_str("%%");
i = j + 1;
continue;
}
if j < c.len() && c[j] == '0' {
j += 1;
}
let digits_at = j;
while j < c.len() && c[j].is_ascii_digit() {
j += 1;
}
if j < c.len() && c[j] == 'f' {
let digits: String = c[digits_at..j].iter().collect();
let n = digits.parse::<usize>().unwrap_or(9).clamp(1, 9);
out.push_str(&format!("%{n}f"));
i = j + 1;
continue;
}
match c.get(i + 1) {
Some(&ch) if renders(&format!("%{ch}")) => {
out.push('%');
out.push(ch);
}
Some(&ch) => {
out.push_str("%%");
out.push(ch);
}
None => out.push_str("%%"),
}
i += if i + 1 < c.len() { 2 } else { 1 };
}
out
}
fn cmd_date() -> CommandDef {
CommandDef::new(
"date",
vec![ArgDesc {
name: "format",
arg_type: ArgType::String,
}],
concat!(
"Print current date and time\n",
" (default) - '%Y/%m/%d %H:%M:%S.%06f'",
),
|args: &[ArgValue], ctx: &CommandContext| {
let fmt = match &args[0] {
ArgValue::String(f) if !f.is_empty() => f.as_str(),
_ => "%Y/%m/%d %H:%M:%S.%06f",
};
let now = chrono::Local::now();
ctx.println(&now.format(&epics_strftime_to_chrono(fmt, &now)).to_string());
Ok(CommandOutcome::Continue)
},
)
}
fn cmd_cd() -> CommandDef {
CommandDef::new(
"cd",
vec![ArgDesc {
name: "directory name",
arg_type: ArgType::Path,
}],
"Change directory to new directory provided as parameter",
chdir_handler,
)
}
fn chdir_handler(args: &[ArgValue], ctx: &CommandContext) -> CommandResult {
let changed = match &args[0] {
ArgValue::String(dir) => Some(set_working_dir(dir).is_ok()),
_ => None,
};
if changed == Some(true) {
return Ok(CommandOutcome::Continue);
}
ctx.eprintln("Invalid directory path, ignored");
Ok(if changed.is_none() {
CommandOutcome::Continue
} else {
CommandOutcome::Failed
})
}
fn cmd_pwd() -> CommandDef {
CommandDef::new(
"pwd",
vec![],
"Print name of current/working directory",
|_args: &[ArgValue], ctx: &CommandContext| {
match std::env::current_dir() {
Ok(cwd) => ctx.println(&cwd.display().to_string()),
Err(e) => return Err(format!("pwd: {e}")),
}
Ok(CommandOutcome::Continue)
},
)
}
fn cmd_epics_env_unset() -> CommandDef {
CommandDef::new(
"epicsEnvUnset",
vec![ArgDesc {
name: "name",
arg_type: ArgType::String,
}],
"Remove variable name from the environment",
|args: &[ArgValue], ctx: &CommandContext| {
let ArgValue::String(name) = &args[0] else {
ctx.eprintln("Missing environment variable name argument.");
return Ok(CommandOutcome::Failed);
};
super::iocsh_env_clear(name);
unsafe { std::env::remove_var(name) };
Ok(CommandOutcome::Continue)
},
)
}
fn cmd_epics_env_show() -> CommandDef {
CommandDef::new(
"epicsEnvShow",
vec![ArgDesc {
name: "[name]",
arg_type: ArgType::String,
}],
concat!(
"Show environment variables on your system\n",
" (default) - show all environment variables\n",
" name - show value of specific environment variable\n",
"Example: epicsEnvShow\n",
"Example: epicsEnvShow PATH",
),
|args: &[ArgValue], ctx: &CommandContext| {
let pattern = match &args[0] {
ArgValue::String(p) => Some(p.as_str()),
_ => None,
};
for (k, v) in std::env::vars() {
if let Some(pattern) = pattern
&& !super::commands::epics_strn_glob_match(
k.as_bytes(),
k.len(),
pattern.as_bytes(),
)
{
continue;
}
ctx.println(&format!("{k}={v}"));
}
Ok(CommandOutcome::Continue)
},
)
}
fn print_epics_params(ctx: &CommandContext) {
for line in crate::runtime::env::prt_env_params() {
ctx.println(&line);
}
}
fn cmd_epics_prt_env_params() -> CommandDef {
CommandDef::new(
"epicsPrtEnvParams",
vec![],
"Show the environment variable parameters used by iocCore",
|_args: &[ArgValue], ctx: &CommandContext| {
print_epics_params(ctx);
Ok(CommandOutcome::Continue)
},
)
}
fn cmd_epics_param_show() -> CommandDef {
CommandDef::new(
"epicsParamShow",
vec![],
"Show the environment variable parameters used by iocCore",
|_args: &[ArgValue], ctx: &CommandContext| {
print_epics_params(ctx);
Ok(CommandOutcome::Continue)
},
)
}
fn cmd_epics_thread_sleep() -> CommandDef {
CommandDef::new(
"epicsThreadSleep",
vec![ArgDesc {
name: "seconds",
arg_type: ArgType::Double,
}],
"Pause execution of IOC shell for <seconds> seconds",
|args: &[ArgValue], _ctx: &CommandContext| {
let secs = match &args[0] {
ArgValue::Double(d) => *d,
ArgValue::Int(n) => *n as f64,
_ => 0.0,
};
crate::runtime::time::sleep_secs(secs);
Ok(CommandOutcome::Continue)
},
)
}
fn cmd_epics_thread_show_all() -> CommandDef {
CommandDef::new(
"epicsThreadShowAll",
vec![ArgDesc {
name: "level",
arg_type: ArgType::Int,
}],
"Display info about all threads",
|_args: &[ArgValue], ctx: &CommandContext| {
show_all_threads(ctx);
Ok(CommandOutcome::Continue)
},
)
}
fn show_all_threads(ctx: &CommandContext) {
ctx.println(crate::runtime::task::THREAD_SHOW_HEADER);
for thread in crate::runtime::task::thread_report() {
ctx.println(&thread.show_line());
}
ctx.eprintln(&crate::runtime::task::osd_priority_range_line());
}
fn cmd_epics_thread_show() -> CommandDef {
CommandDef::new(
"epicsThreadShow",
vec![ArgDesc {
name: "[-level] [thread ...]",
arg_type: ArgType::Argv,
}],
"Display info about the specified thread",
|args: &[ArgValue], ctx: &CommandContext| {
let ArgValue::Argv(argv) = &args[0] else {
return Err("epicsThreadShow: expected the argument vector".into());
};
let mut rest = argv.as_slice();
if rest.first().is_some_and(|token| token.starts_with('-')) {
rest = &rest[1..];
}
if rest.is_empty() {
show_all_threads(ctx);
return Ok(CommandOutcome::Continue);
}
let mut header_printed = false;
let mut failed = false;
for token in rest {
let id = match super::registry::parse_iocsh_int(token) {
Ok(value) => value as u64,
Err(_) => match crate::runtime::task::thread_by_name(token) {
Some(thread) => thread.id(),
None => {
ctx.eprintln(&format!("\t'{token}' is not a known thread name"));
failed = true;
continue;
}
},
};
if !header_printed {
show_thread(ctx, 0);
header_printed = true;
}
show_thread(ctx, id);
}
if failed {
Ok(CommandOutcome::Failed)
} else {
Ok(CommandOutcome::Continue)
}
},
)
}
fn cmd_epics_thread_resume() -> CommandDef {
CommandDef::new(
"epicsThreadResume",
vec![ArgDesc {
name: "[thread ...]",
arg_type: ArgType::Argv,
}],
concat!(
"Resume a suspended thread.\n",
"Only do this if you know that it is safe to resume a suspended thread",
),
|args: &[ArgValue], ctx: &CommandContext| {
let ArgValue::Argv(argv) = &args[0] else {
return Err("epicsThreadResume: expected the argument vector".into());
};
let mut failed = false;
for token in argv {
let (kind, found) = match super::registry::parse_iocsh_int(token) {
Ok(id) => ("thread id", crate::runtime::task::thread_by_id(id as u64)),
Err(_) => ("thread name", crate::runtime::task::thread_by_name(token)),
};
let Some(thread) = found else {
ctx.eprintln(&format!("'{token}' is not a valid {kind}"));
failed = true;
continue;
};
if !thread.resume() {
ctx.eprintln(&format!("Thread {token} is not suspended"));
failed = true;
}
}
if failed {
Ok(CommandOutcome::Failed)
} else {
Ok(CommandOutcome::Continue)
}
},
)
}
fn show_thread(ctx: &CommandContext, id: u64) {
if id == 0 {
ctx.println(crate::runtime::task::THREAD_SHOW_HEADER);
return;
}
match crate::runtime::task::thread_by_id(id) {
Some(thread) => ctx.println(&thread.show_line()),
None => ctx.println(&format!("Thread {id:#x} ({id}) not found.")),
}
}
fn cmd_taskwd_show() -> CommandDef {
CommandDef::new(
"taskwdShow",
vec![ArgDesc {
name: "level",
arg_type: ArgType::Int,
}],
"Show number of tasks and monitors registered",
|args: &[ArgValue], ctx: &CommandContext| {
let level = match &args[0] {
ArgValue::Int(n) => *n as u32,
_ => 0,
};
crate::runtime::taskwd::taskwd_show(level, &|line| ctx.println(line));
Ok(CommandOutcome::Continue)
},
)
}
fn cmd_epics_mutex_show_all() -> CommandDef {
CommandDef::new(
"epicsMutexShowAll",
vec![
ArgDesc {
name: "onlyLocked",
arg_type: ArgType::Int,
},
ArgDesc {
name: "level",
arg_type: ArgType::Int,
},
],
concat!(
"Display information about all epicsMutex semaphores\n",
" onlyLocked - non-zero to show only locked semaphores\n",
" level - desired information level to report",
),
|args: &[ArgValue], ctx: &CommandContext| {
let only_locked = matches!(&args[0], ArgValue::Int(n) if *n != 0);
let level = match &args[1] {
ArgValue::Int(n) => *n as u32,
_ => 0,
};
let report = crate::runtime::sync::mutex_report(only_locked);
ctx.println(&format!("ellCount(&mutexList) {}", report.total));
ctx.println(crate::runtime::sync::osd_show_all_line());
for entry in &report.shown {
for line in entry.show_lines(level) {
ctx.println(&line);
}
}
Ok(CommandOutcome::Continue)
},
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::server::database::PvDatabase;
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_env_show(ctx: &CommandContext, tokens: &[&str]) -> String {
let mut reg = CommandRegistry::new();
register(&mut reg);
let cmd = reg.get("epicsEnvShow").unwrap();
let tokens: Vec<String> = tokens.iter().map(|t| t.to_string()).collect();
let args = parse_args(&tokens, &cmd.args).unwrap();
let tmp = tempfile::NamedTempFile::new().unwrap();
let path = tmp.path().to_path_buf();
ctx.with_output(std::fs::File::create(&path).unwrap(), || {
cmd.handler.call(&args, ctx).unwrap();
});
std::fs::read_to_string(&path).unwrap()
}
fn run_split(ctx: &CommandContext, name: &str, tokens: &[&str]) -> (String, String, bool) {
let mut reg = CommandRegistry::new();
register(&mut reg);
let cmd = reg.get(name).unwrap();
let tokens: Vec<String> = tokens.iter().map(|t| t.to_string()).collect();
let args = parse_args(&tokens, &cmd.args).unwrap();
let out_file = tempfile::NamedTempFile::new().unwrap();
let err_file = tempfile::NamedTempFile::new().unwrap();
let (out_path, err_path) = (out_file.path().to_path_buf(), err_file.path().to_path_buf());
let mut failed = false;
ctx.with_error(std::fs::File::create(&err_path).unwrap(), || {
ctx.with_output(std::fs::File::create(&out_path).unwrap(), || {
failed = matches!(
cmd.handler.call(&args, ctx).unwrap(),
CommandOutcome::Failed
);
});
});
(
std::fs::read_to_string(&out_path).unwrap(),
std::fs::read_to_string(&err_path).unwrap(),
failed,
)
}
const C_LIBCOM_ARG_NAMES: &[(&str, &[&str])] = &[
("cd", &["directory name"]),
("date", &["format"]),
("echo", &["string"]),
("eltc", &["(0,1)=>(false,true)"]),
("epicsEnvSet", &["name", "value"]),
("epicsEnvShow", &["[name]"]),
("epicsEnvUnset", &["name"]),
("epicsMutexShowAll", &["onlyLocked", "level"]),
("epicsParamShow", &[]),
("epicsPrtEnvParams", &[]),
("epicsThreadResume", &["[thread ...]"]),
("epicsThreadShow", &["[-level] [thread ...]"]),
("epicsThreadShowAll", &["level"]),
("epicsThreadSleep", &["seconds"]),
("errlog", &["message"]),
("errlogInit", &["bufSize"]),
("errlogInit2", &["bufSize", "maxMsgSize"]),
("errlogShow", &["level"]),
("generalTimeReport", &["interest_level"]),
("installLastResortEventProvider", &[]),
("iocLogInit", &[]),
("iocLogPrefix", &["prefix"]),
("iocLogShow", &["level"]),
("pwd", &[]),
("registryDump", &[]),
("setIocLogDisable", &["(0,1)=>(false,true)"]),
("taskwdShow", &["level"]),
];
#[test]
fn the_argument_names_are_cs_words() {
let mut reg = CommandRegistry::new();
register(&mut reg);
let mut checked = 0;
for (name, want) in C_LIBCOM_ARG_NAMES {
let Some(def) = reg.get(name) else { continue };
let got: Vec<&str> = def.args.iter().map(|a| a.name).collect();
assert_eq!(&got, want, "`{name}` argument names");
checked += 1;
}
assert_eq!(checked, 25);
}
#[test]
fn taskwd_show_is_registered_with_cs_arity() {
let mut reg = CommandRegistry::new();
register(&mut reg);
let cmd = reg
.get("taskwdShow")
.expect("taskwdShow must be registered");
assert_eq!(cmd.args.len(), 1);
assert_eq!(cmd.args[0].name, "level");
assert!(matches!(cmd.args[0].arg_type, ArgType::Int));
}
#[test]
fn the_task_table_appears_exactly_when_cs_level_is_true() {
let ctx = make_ctx();
let watched = crate::runtime::taskwd::taskwd_insert(
"cbTaskwdCmd",
crate::runtime::taskwd::CheckIn::Every(std::time::Duration::from_secs(30)),
None,
);
let (summary, err, failed) = run_split(&ctx, "taskwdShow", &[]);
assert!(!failed);
assert_eq!(err, "", "the whole report is stdout, as C's `printf` is");
assert_eq!(
summary.lines().count(),
1,
"a missing level is C's 0 — summary only, got {summary:?}"
);
assert!(
summary.starts_with("0 monitors, 1 tasks registered"),
"{summary:?}"
);
let (detailed, _, _) = run_split(&ctx, "taskwdShow", &["1"]);
let lines: Vec<&str> = detailed.lines().collect();
assert_eq!(
lines.len(),
3,
"summary, header, one task — got {detailed:?}"
);
assert!(lines[1].starts_with("TASK NAME"), "{:?}", lines[1]);
assert!(lines[2].starts_with("cbTaskwdCmd"), "{:?}", lines[2]);
assert!(
lines[2].contains("Ok"),
"a task that is checking in is not reported — {:?}",
lines[2]
);
let (negative, _, _) = run_split(&ctx, "taskwdShow", &["-1"]);
assert_eq!(
negative.lines().count(),
3,
"C's `if (level)` makes a negative level true — got {negative:?}"
);
drop(watched);
let (empty, _, _) = run_split(&ctx, "taskwdShow", &["1"]);
assert_eq!(
empty.lines().count(),
2,
"dropping the handle is C's `taskwdRemove` — got {empty:?}"
);
}
fn parked_ioc_thread(
name: &'static str,
) -> (std::sync::mpsc::Sender<()>, std::thread::JoinHandle<()>) {
let (started_tx, started_rx) = std::sync::mpsc::channel();
let (finish_tx, finish_rx) = std::sync::mpsc::channel();
let join = std::thread::Builder::new()
.name(name.to_string())
.spawn(move || {
let _ = crate::runtime::task::enter_ioc_thread(
crate::runtime::task::ThreadPriority::ScanLow,
);
started_tx.send(()).unwrap();
finish_rx.recv().unwrap();
})
.unwrap();
started_rx.recv().unwrap();
(finish_tx, join)
}
#[test]
fn epics_thread_show_all_is_registered_with_cs_arity() {
let mut reg = CommandRegistry::new();
register(&mut reg);
let cmd = reg
.get("epicsThreadShowAll")
.expect("epicsThreadShowAll must be registered");
assert_eq!(cmd.args.len(), 1);
assert_eq!(cmd.args[0].name, "level");
assert!(matches!(cmd.args[0].arg_type, ArgType::Int));
}
#[test]
fn epics_thread_show_all_prints_the_header_the_rows_and_a_stderr_trailer() {
let ctx = make_ctx();
let (finish, join) = parked_ioc_thread("cbShowAllRow");
let (out, err, failed) = run_split(&ctx, "epicsThreadShowAll", &[]);
assert!(!failed);
let lines: Vec<&str> = out.lines().collect();
assert_eq!(lines[0], crate::runtime::task::THREAD_SHOW_HEADER);
let row = lines
.iter()
.find(|l| l.contains("cbShowAllRow"))
.unwrap_or_else(|| panic!("the running thread must be listed:\n{out}"));
assert!(row.starts_with(" cbShowAllRow "), "{row}");
assert!(
row.ends_with(&format!(
"{:3}{:8} {:>8.8}",
crate::runtime::task::ThreadPriority::ScanLow.value(),
0,
"OK"
)),
"{row}"
);
assert!(
!out.contains("OSD priority range"),
"the trailer belongs on stderr, not in the redirected listing"
);
assert!(err.starts_with("OSD priority range min: "), "{err}");
assert!(err.trim_end().ends_with(", memory not locked"), "{err}");
finish.send(()).unwrap();
join.join().unwrap();
}
#[test]
fn epics_thread_show_is_registered_with_cs_arity() {
let mut reg = CommandRegistry::new();
register(&mut reg);
let cmd = reg
.get("epicsThreadShow")
.expect("epicsThreadShow must be registered");
assert_eq!(cmd.args.len(), 1);
assert_eq!(cmd.args[0].name, "[-level] [thread ...]");
assert!(matches!(cmd.args[0].arg_type, ArgType::Argv));
}
#[test]
fn epics_thread_show_without_a_thread_is_show_all() {
let ctx = make_ctx();
let (finish, join) = parked_ioc_thread("cbShowBare");
let (all, all_err, _) = run_split(&ctx, "epicsThreadShowAll", &[]);
for tokens in [vec![], vec!["-2"]] {
let (out, err, failed) = run_split(&ctx, "epicsThreadShow", &tokens);
assert!(!failed, "{tokens:?}");
assert_eq!(out, all, "{tokens:?}");
assert_eq!(err, all_err, "{tokens:?}");
}
finish.send(()).unwrap();
join.join().unwrap();
}
#[test]
fn epics_thread_show_by_name_prints_the_header_then_the_one_row() {
let ctx = make_ctx();
let (finish, join) = parked_ioc_thread("cbShowNamed");
let (out, err, failed) = run_split(&ctx, "epicsThreadShow", &["cbShowNamed"]);
assert!(!failed);
let lines: Vec<&str> = out.lines().collect();
assert_eq!(lines.len(), 2, "{out}");
assert_eq!(lines[0], crate::runtime::task::THREAD_SHOW_HEADER);
assert!(lines[1].contains("cbShowNamed"), "{out}");
assert_eq!(err, "", "no trailer on this path");
let (levelled, _, _) = run_split(&ctx, "epicsThreadShow", &["-2", "cbShowNamed"]);
assert_eq!(levelled, out);
finish.send(()).unwrap();
join.join().unwrap();
}
#[test]
fn an_unknown_thread_name_fails_the_line_without_ending_it() {
let ctx = make_ctx();
let (finish, join) = parked_ioc_thread("cbShowAfterBad");
let (out, err, failed) =
run_split(&ctx, "epicsThreadShow", &["nosuchthread", "cbShowAfterBad"]);
assert!(failed, "an unknown name is C's `iocshSetError(-1)`");
assert_eq!(err, "\t'nosuchthread' is not a known thread name\n");
let lines: Vec<&str> = out.lines().collect();
assert_eq!(lines.len(), 2, "{out}");
assert_eq!(lines[0], crate::runtime::task::THREAD_SHOW_HEADER);
assert!(lines[1].contains("cbShowAfterBad"), "{out}");
let (only, only_err, only_failed) = run_split(&ctx, "epicsThreadShow", &["nosuchthread"]);
assert!(only_failed);
assert_eq!(only, "");
assert_eq!(only_err, "\t'nosuchthread' is not a known thread name\n");
finish.send(()).unwrap();
join.join().unwrap();
}
#[test]
fn a_zero_handle_is_the_header_and_an_empty_argument_is_zero() {
let ctx = make_ctx();
let header = crate::runtime::task::THREAD_SHOW_HEADER;
for token in ["0", ""] {
let (out, err, failed) = run_split(&ctx, "epicsThreadShow", &[token]);
assert!(!failed, "{token:?}");
assert_eq!(out, format!("{header}\n{header}\n"), "{token:?}");
assert_eq!(err, "", "{token:?}");
}
}
#[test]
fn an_unresolvable_handle_reports_on_stdout_without_failing() {
let ctx = make_ctx();
let (out, err, failed) = run_split(&ctx, "epicsThreadShow", &["18446744073709551615"]);
assert!(!failed);
assert_eq!(err, "");
assert_eq!(
out,
format!(
"{}\nThread 0xffffffffffffffff (18446744073709551615) not found.\n",
crate::runtime::task::THREAD_SHOW_HEADER
)
);
}
#[test]
fn a_handle_from_the_listing_resolves_back_to_its_row() {
let ctx = make_ctx();
let (finish, join) = parked_ioc_thread("cbShowByHandle");
let thread = crate::runtime::task::thread_by_name("cbShowByHandle").unwrap();
for token in [format!("{:#x}", thread.id()), thread.os_id().to_string()] {
let (out, _, failed) = run_split(&ctx, "epicsThreadShow", &[&token]);
assert!(!failed, "{token}");
let lines: Vec<&str> = out.lines().collect();
assert_eq!(lines.len(), 2, "{token}: {out}");
assert_eq!(lines[1], thread.show_line(), "{token}");
}
finish.send(()).unwrap();
join.join().unwrap();
}
#[test]
fn epics_thread_resume_is_registered_with_cs_arity() {
let mut reg = CommandRegistry::new();
register(&mut reg);
let cmd = reg
.get("epicsThreadResume")
.expect("epicsThreadResume must be registered");
assert_eq!(cmd.args.len(), 1);
assert_eq!(cmd.args[0].name, "[thread ...]");
assert!(matches!(cmd.args[0].arg_type, ArgType::Argv));
}
#[test]
fn an_unknown_thread_name_fails_the_line_on_stderr() {
let ctx = make_ctx();
let (out, err, failed) = run_split(&ctx, "epicsThreadResume", &["noSuchThread"]);
assert!(failed, "C `iocshSetError(-1)`");
assert_eq!(out, "");
assert_eq!(err, "'noSuchThread' is not a valid thread name\n");
}
#[test]
fn a_handle_that_names_no_thread_is_the_id_diagnostic() {
let ctx = make_ctx();
for token in ["0", "18446744073709551615"] {
let (out, err, failed) = run_split(&ctx, "epicsThreadResume", &[token]);
assert!(failed, "{token}");
assert_eq!(out, "", "{token}");
assert_eq!(err, format!("'{token}' is not a valid thread id\n"));
}
}
#[test]
fn a_suspended_thread_is_resumed_and_the_line_succeeds() {
let ctx = make_ctx();
for by_id in [false, true] {
let name = if by_id {
"cbResumeById"
} else {
"cbResumeByName"
};
let (id_tx, id_rx) = std::sync::mpsc::channel();
let (woke_tx, woke_rx) = std::sync::mpsc::channel();
let join = std::thread::Builder::new()
.name(name.to_string())
.spawn(move || {
let _ = crate::runtime::task::enter_ioc_thread(
crate::runtime::task::ThreadPriority::ScanLow,
);
id_tx
.send(crate::runtime::task::current_thread_id())
.unwrap();
crate::runtime::task::suspend_self();
woke_tx.send(()).unwrap();
})
.unwrap();
let id = id_rx.recv().unwrap();
for _ in 0..500 {
if crate::runtime::task::thread_by_id(id).is_some_and(|t| t.is_suspended()) {
break;
}
std::thread::sleep(std::time::Duration::from_millis(10));
}
let row = crate::runtime::task::thread_by_id(id).expect("row");
assert!(row.is_suspended(), "{name} must reach suspend_self");
assert!(
row.show_line().ends_with(" SUSPEND"),
"{:?}",
row.show_line()
);
let token = if by_id {
format!("{:#x}", id)
} else {
name.to_string()
};
let (out, err, failed) = run_split(&ctx, "epicsThreadResume", &[&token]);
assert!(
!failed,
"a resume that acts does not fail the line: {token}"
);
assert_eq!(out, "", "{token}");
assert_eq!(err, "", "{token}");
woke_rx
.recv_timeout(std::time::Duration::from_secs(5))
.unwrap_or_else(|e| panic!("{token} must wake the thread: {e}"));
join.join().unwrap();
}
}
#[test]
fn a_live_thread_resolves_and_reports_not_suspended() {
let ctx = make_ctx();
let (finish, join) = parked_ioc_thread("cbResumeTarget");
let thread = crate::runtime::task::thread_by_name("cbResumeTarget").unwrap();
for token in ["cbResumeTarget".to_string(), format!("{:#x}", thread.id())] {
let (out, err, failed) = run_split(&ctx, "epicsThreadResume", &[&token]);
assert!(failed, "{token}");
assert_eq!(out, "", "{token}");
assert_eq!(err, format!("Thread {token} is not suspended\n"));
}
finish.send(()).unwrap();
join.join().unwrap();
}
#[test]
fn no_arguments_resume_nothing_and_do_not_fail() {
let ctx = make_ctx();
let (out, err, failed) = run_split(&ctx, "epicsThreadResume", &[]);
assert!(!failed);
assert_eq!(out, "");
assert_eq!(err, "");
}
#[test]
fn a_failing_argument_does_not_abandon_the_rest_of_the_line() {
let ctx = make_ctx();
let (finish, join) = parked_ioc_thread("cbResumeRest");
let (out, err, failed) = run_split(
&ctx,
"epicsThreadResume",
&["noSuchThread", "0", "cbResumeRest"],
);
assert!(failed);
assert_eq!(out, "");
assert_eq!(
err,
concat!(
"'noSuchThread' is not a valid thread name\n",
"'0' is not a valid thread id\n",
"Thread cbResumeRest is not suspended\n",
)
);
finish.send(()).unwrap();
join.join().unwrap();
}
#[test]
fn epics_mutex_show_all_is_registered_with_cs_arity() {
let mut reg = CommandRegistry::new();
register(&mut reg);
let cmd = reg
.get("epicsMutexShowAll")
.expect("epicsMutexShowAll must be registered");
assert_eq!(cmd.args.len(), 2);
assert_eq!(cmd.args[0].name, "onlyLocked");
assert_eq!(cmd.args[1].name, "level");
assert!(matches!(cmd.args[0].arg_type, ArgType::Int));
assert!(matches!(cmd.args[1].arg_type, ArgType::Int));
}
#[test]
fn epics_mutex_show_all_prints_the_count_the_protocol_and_the_rows() {
let ctx = make_ctx();
let gate: crate::runtime::sync::PriorityInheritanceMutex<()> =
crate::runtime::sync::PriorityInheritanceMutex::new(());
let (out, err, failed) = run_split(&ctx, "epicsMutexShowAll", &["0", "0"]);
assert!(!failed);
assert_eq!(err, "", "C sends the whole report to stdout");
let lines: Vec<&str> = out.lines().collect();
let total = crate::runtime::sync::mutex_report(false).total;
assert_eq!(lines[0], format!("ellCount(&mutexList) {total}"));
assert_eq!(lines[1], crate::runtime::sync::osd_show_all_line());
assert_eq!(
lines.len(),
2 + total,
"one row per mutex when nothing is filtered:\n{out}"
);
assert!(
lines[2..].iter().all(|l| l.starts_with("epicsMutexId 0x")),
"{out}"
);
drop(gate);
}
#[test]
fn a_level_above_zero_adds_the_osd_line_under_every_row() {
let ctx = make_ctx();
let gate: crate::runtime::sync::PriorityInheritanceMutex<()> =
crate::runtime::sync::PriorityInheritanceMutex::new(());
let (plain, _, _) = run_split(&ctx, "epicsMutexShowAll", &["0", "0"]);
for level in ["1", "-1"] {
let (detailed, _, _) = run_split(&ctx, "epicsMutexShowAll", &["0", level]);
let rows = plain.lines().count() - 2;
assert_eq!(
detailed.lines().count(),
plain.lines().count() + rows,
"level {level} must add one line per row:\n{detailed}"
);
assert!(
detailed.lines().any(|l| l.starts_with(&format!(
" {} uaddr=0x",
crate::runtime::sync::MUTEX_OSD_LABEL
))),
"{detailed}"
);
}
drop(gate);
}
#[test]
fn only_locked_filters_the_rows_but_not_the_count() {
let ctx = make_ctx();
let gate: crate::runtime::sync::PriorityInheritanceMutex<()> =
crate::runtime::sync::PriorityInheritanceMutex::new(());
let (idle, _, _) = run_split(&ctx, "epicsMutexShowAll", &["1", "0"]);
let total = crate::runtime::sync::mutex_report(false).total;
assert_eq!(
idle,
format!(
"ellCount(&mutexList) {total}\n{}\n",
crate::runtime::sync::osd_show_all_line()
),
"nothing is held, so C prints only the two header lines"
);
let held = gate.lock();
let (locked, _, _) = run_split(&ctx, "epicsMutexShowAll", &["1", "0"]);
let lines: Vec<&str> = locked.lines().collect();
assert_eq!(lines[0], format!("ellCount(&mutexList) {total}"));
assert_eq!(lines.len(), 3, "exactly the one held mutex:\n{locked}");
assert!(lines[2].starts_with("epicsMutexId 0x"), "{locked}");
assert!(lines[2].contains("core_commands.rs"), "{locked}");
drop(held);
}
#[test]
fn the_errlog_and_ioc_log_commands_are_registered_with_cs_arities() {
let mut reg = CommandRegistry::new();
register(&mut reg);
for (name, args) in [
("eltc", 1usize),
("errlogInit", 1),
("errlogInit2", 2),
("errlogShow", 1),
("errlog", 1),
("iocLogInit", 0),
("iocLogPrefix", 1),
("iocLogShow", 1),
("setIocLogDisable", 1),
] {
let cmd = reg
.get(name)
.unwrap_or_else(|| panic!("{name} must be registered"));
assert_eq!(cmd.args.len(), args, "{name}: C's argument count");
}
}
#[test]
fn eltc_from_the_shell_flips_the_console_setting() {
let ctx = make_ctx();
let mut reg = CommandRegistry::new();
register(&mut reg);
let cmd = reg.get("eltc").unwrap();
let args = parse_args(&["0".to_string()], &cmd.args).unwrap();
cmd.handler.call(&args, &ctx).unwrap();
assert!(!crate::runtime::log::errlog_to_console());
let args = parse_args(&["1".to_string()], &cmd.args).unwrap();
cmd.handler.call(&args, &ctx).unwrap();
assert!(crate::runtime::log::errlog_to_console());
}
#[test]
fn errlog_show_reports_the_three_level_bands() {
let ctx = make_ctx();
let mut reg = CommandRegistry::new();
register(&mut reg);
let show = |level: &str| {
let cmd = reg
.get("errlogShow")
.expect("errlogShow must be registered");
let args = parse_args(&[level.to_string()], &cmd.args).unwrap();
let tmp = tempfile::NamedTempFile::new().unwrap();
let path = tmp.path().to_path_buf();
ctx.with_output(std::fs::File::create(&path).unwrap(), || {
cmd.handler.call(&args, &ctx).unwrap();
});
std::fs::read_to_string(&path).unwrap()
};
let sizes = format!(
"Error log:\n buffer size: {}\n max message size: {}\n",
crate::runtime::log::MIN_BUFFER_SIZE,
crate::runtime::log::MIN_MESSAGE_SIZE
);
assert_eq!(show("0"), sizes);
let id = crate::runtime::log::errlog_add_listener(|_| {});
assert_eq!(
show("1"),
format!("{sizes} number of listeners: 1\n"),
"level 1 adds C's listener count"
);
assert!(crate::runtime::log::errlog_remove_listener(id));
assert_eq!(
show("2"),
format!(
"{sizes} number of listeners: 0\n\
\x20 buffer(log) contents:\n\
\x20 buffer(log) position: 0 of {size} bytes\n\
\x20 buffer(print) contents:\n\
\x20 buffer(print) position: 0 of {size} bytes\n",
size = crate::runtime::log::MIN_BUFFER_SIZE
)
);
}
#[test]
fn errlog_from_the_shell_reaches_a_listener_with_a_trailing_newline() {
let ctx = make_ctx();
let mut reg = CommandRegistry::new();
register(&mut reg);
let seen = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
let sink = std::sync::Arc::clone(&seen);
let id = crate::runtime::log::errlog_add_listener(move |m| {
sink.lock().expect("sink").push(m.to_string());
});
let cmd = reg.get("errlog").unwrap();
let args = parse_args(&["disk nearly full".to_string()], &cmd.args).unwrap();
cmd.handler.call(&args, &ctx).unwrap();
let lines = seen.lock().expect("sink").clone();
crate::runtime::log::errlog_remove_listener(id);
assert!(
lines.iter().any(|l| l == "disk nearly full\n"),
"the command must have flushed before returning: {lines:?}"
);
}
#[test]
fn the_shell_executed_commands_are_still_in_the_command_table() {
let mut reg = CommandRegistry::new();
register(&mut reg);
for (name, args) in [
("iocshCmd", 1usize),
("iocshRun", 2),
("iocshLoad", 2),
("on", 1),
] {
let cmd = reg
.get(name)
.unwrap_or_else(|| panic!("{name} must be in the command table for help"));
assert_eq!(cmd.args.len(), args, "{name}: C's argument count");
assert!(
super::super::format_help_entry(cmd, false, true).contains(name),
"`help {name}` must name the command"
);
}
}
#[test]
fn a_shell_executed_command_refuses_to_run_from_the_table() {
let ctx = make_ctx();
let mut reg = CommandRegistry::new();
register(&mut reg);
for name in ["iocshCmd", "iocshRun", "iocshLoad", "on"] {
let cmd = reg.get(name).unwrap();
let Err(err) = cmd.handler.call(&[], &ctx) else {
panic!("{name}: the table entry must not execute");
};
assert!(err.contains(name), "{name}: {err}");
}
}
#[test]
fn epics_thread_sleep_survives_every_delay_c_refuses() {
let ctx = make_ctx();
let mut reg = CommandRegistry::new();
register(&mut reg);
let cmd = reg.get("epicsThreadSleep").unwrap();
for arg in ["1e300", "inf", "nan", "-5", "0"] {
let tokens = vec![arg.to_string()];
let args = parse_args(&tokens, &cmd.args).unwrap();
let t = std::time::Instant::now();
cmd.handler.call(&args, &ctx).unwrap();
assert!(
t.elapsed() < std::time::Duration::from_millis(50),
"epicsThreadSleep {arg} must return at once, as C does"
);
}
}
#[test]
#[serial_test::serial(epics_env)]
fn epics_env_show_glob_matches_and_is_silent_on_no_match() {
let ctx = make_ctx();
unsafe {
std::env::set_var("EPICS_RS_SHOW_ONE", "1");
std::env::set_var("EPICS_RS_SHOW_TWO", "2");
}
let globbed = run_env_show(&ctx, &["EPICS_RS_SHOW_*"]);
let mut lines: Vec<&str> = globbed.lines().collect();
lines.sort();
assert_eq!(lines, ["EPICS_RS_SHOW_ONE=1", "EPICS_RS_SHOW_TWO=2"]);
assert_eq!(
run_env_show(&ctx, &["EPICS_RS_SHOW_ONE"]),
"EPICS_RS_SHOW_ONE=1\n"
);
assert_eq!(run_env_show(&ctx, &["EPICS_RS_NO_SUCH_VAR"]), "");
unsafe {
std::env::remove_var("EPICS_RS_SHOW_ONE");
std::env::remove_var("EPICS_RS_SHOW_TWO");
}
}
#[test]
fn date_takes_a_format_argument() {
let ctx = make_ctx();
let mut reg = CommandRegistry::new();
register(&mut reg);
let cmd = reg.get("date").unwrap();
assert_eq!(cmd.args.len(), 1, "C declares one argument");
let args = parse_args(&["%Y".to_string()], &cmd.args).unwrap();
let tmp = tempfile::NamedTempFile::new().unwrap();
let path = tmp.path().to_path_buf();
ctx.with_output(std::fs::File::create(&path).unwrap(), || {
cmd.handler.call(&args, &ctx).unwrap();
});
let out = std::fs::read_to_string(&path).unwrap();
assert_eq!(
out.trim().len(),
4,
"a %Y-only format prints just a year: {out:?}"
);
let now = chrono::Local::now();
assert_eq!(
epics_strftime_to_chrono("%Y/%m/%d %H:%M:%S.%06f", &now),
"%Y/%m/%d %H:%M:%S.%6f"
);
assert_eq!(epics_strftime_to_chrono("%f", &now), "%9f");
assert_eq!(epics_strftime_to_chrono("%d%%%H", &now), "%d%%%H");
assert_eq!(epics_strftime_to_chrono("%Q", &now), "%%Q");
assert_eq!(epics_strftime_to_chrono("a%", &now), "a%%");
}
#[test]
fn an_unknown_conversion_prints_itself_instead_of_panicking() {
let ctx = make_ctx();
let mut reg = CommandRegistry::new();
register(&mut reg);
let cmd = reg.get("date").unwrap();
let args = parse_args(&["%Q".to_string()], &cmd.args).unwrap();
let tmp = tempfile::NamedTempFile::new().unwrap();
let path = tmp.path().to_path_buf();
ctx.with_output(std::fs::File::create(&path).unwrap(), || {
cmd.handler.call(&args, &ctx).unwrap();
});
assert_eq!(std::fs::read_to_string(&path).unwrap(), "%Q\n");
}
#[test]
fn echo_and_pwd_run() {
let ctx = make_ctx();
let mut reg = CommandRegistry::new();
register(&mut reg);
for name in ["echo", "pwd", "date", "#"] {
let cmd = reg.get(name).unwrap();
let args = parse_args(&[], &cmd.args).unwrap();
assert!(
cmd.handler.call(&args, &ctx).is_ok(),
"{name} must run cleanly"
);
}
}
#[test]
#[serial_test::serial(epics_env)]
fn cd_updates_pwd_and_prints_nothing() {
use std::path::PathBuf;
let start = std::env::current_dir().unwrap();
let ctx = make_ctx();
let dir = tempfile::tempdir().unwrap();
let target = dir.path().canonicalize().unwrap();
unsafe { std::env::remove_var("PWD") };
let mut reg = CommandRegistry::new();
register(&mut reg);
assert_eq!(
std::env::var("PWD").ok().map(PathBuf::from),
Some(start.clone()),
"registration must set PWD before any cd"
);
let cmd = reg.get("cd").unwrap();
let args = parse_args(&[target.to_str().unwrap().to_string()], &cmd.args).unwrap();
let tmp = tempfile::NamedTempFile::new().unwrap();
let out_path = tmp.path().to_path_buf();
ctx.with_output(std::fs::File::create(&out_path).unwrap(), || {
cmd.handler.call(&args, &ctx).unwrap();
});
assert_eq!(
std::fs::read_to_string(&out_path).unwrap(),
"",
"C `cd` prints nothing on success"
);
assert_eq!(
PathBuf::from(std::env::var("PWD").unwrap()),
target,
"`cd` must update PWD like C `updatePWD`"
);
std::env::set_current_dir(&start).unwrap();
}
#[test]
fn install_last_resort_event_provider_is_reachable_from_the_shell() {
let ctx = make_ctx();
let mut reg = CommandRegistry::new();
register(&mut reg);
let cmd = reg
.get("installLastResortEventProvider")
.expect("C registers this command");
assert!(cmd.args.is_empty(), "C declares zero arguments");
assert!(
crate::runtime::general_time::get_event(1).is_none(),
"an IOC with no event provider must fail the event stamp"
);
let args = parse_args(&[], &cmd.args).unwrap();
cmd.handler.call(&args, &ctx).unwrap();
assert!(
crate::runtime::general_time::get_event(1).is_some(),
"the last-resort provider answers every event number"
);
assert!(
crate::runtime::general_time::report(1).contains("Last Resort Event"),
"generalTimeReport must name the provider as C does"
);
}
#[test]
fn general_time_report_prints_the_report_verbatim() {
let ctx = make_ctx();
let mut reg = CommandRegistry::new();
register(&mut reg);
let cmd = reg.get("generalTimeReport").expect("C registers this");
assert_eq!(cmd.args.len(), 1, "C declares one argument");
for level in ["0", "1"] {
let args = parse_args(&[level.to_string()], &cmd.args).unwrap();
let tmp = tempfile::NamedTempFile::new().unwrap();
let path = tmp.path().to_path_buf();
ctx.with_output(std::fs::File::create(&path).unwrap(), || {
cmd.handler.call(&args, &ctx).unwrap();
});
let out = std::fs::read_to_string(&path).unwrap();
assert!(
out.starts_with("Backwards time errors prevented "),
"C's first line is the error count: {out:?}"
);
assert!(
out.contains("Current Time Providers:") && out.contains("Event Time Providers:"),
"C prints both headers: {out:?}"
);
assert!(!out.ends_with("\n\n\n"), "no newline added: {out:?}");
}
let args = parse_args(&[], &cmd.args).unwrap();
assert!(cmd.handler.call(&args, &ctx).is_ok());
}
#[test]
fn epics_env_unset_removes_var() {
let ctx = make_ctx();
let mut reg = CommandRegistry::new();
register(&mut reg);
unsafe { std::env::set_var("_CORECMD_TEST", "x") };
let cmd = reg.get("epicsEnvUnset").unwrap();
let args = parse_args(&["_CORECMD_TEST".to_string()], &cmd.args).unwrap();
cmd.handler.call(&args, &ctx).unwrap();
assert!(std::env::var("_CORECMD_TEST").is_err());
}
}