use std::num::NonZeroUsize;
use std::process::ExitCode;
use clap::{ArgMatches, Command, FromArgMatches, Subcommand};
use clap_complete::engine::ArgValueCandidates;
use cerulion_cli_engine::error::CliResult;
use cerulion_cli_engine::topic_cmd::{introspect, TopicScope};
use super::NestedVerbs;
use crate::cli::VerbLogClass;
use crate::completion;
use crate::network_switch::{NetworkRefusal, SwitchEnv};
const PARENT: &str = "topic";
const DEFAULT_BW_WINDOW: u64 = 100;
const MAX_BW_WINDOW: u64 = 100_000;
#[derive(Subcommand)]
enum TopicIntrospectAction {
Type {
#[arg(long)]
local: bool,
#[arg(add = ArgValueCandidates::new(completion::topics))]
topic: String,
},
Find {
#[arg(long)]
local: bool,
#[arg(long, value_name = "LOCATOR")]
connect: Vec<String>,
#[arg(long, value_name = "LOCATOR")]
listen: Vec<String>,
#[arg(value_name = "TYPE", add = ArgValueCandidates::new(completion::schema_names))]
type_name: String,
},
Bw {
#[arg(long)]
local: bool,
#[arg(
long,
value_name = "N",
default_value_t = DEFAULT_BW_WINDOW,
value_parser = clap::value_parser!(u64).range(2..=MAX_BW_WINDOW)
)]
window: u64,
#[arg(add = ArgValueCandidates::new(completion::topics))]
topic: String,
},
}
pub(crate) struct TopicIntrospect;
impl NestedVerbs for TopicIntrospect {
fn parent(&self) -> &'static str {
PARENT
}
fn commands(&self) -> Vec<Command> {
super::derived_verbs::<TopicIntrospectAction>()
}
fn log_class(&self, _matches: &ArgMatches) -> VerbLogClass {
VerbLogClass::OneShot
}
fn network_refusal(&self, _matches: &ArgMatches, _env: &SwitchEnv) -> Option<NetworkRefusal> {
None
}
fn needs_identity(&self, _matches: &ArgMatches) -> bool {
true
}
fn run(&self, matches: &ArgMatches) -> ExitCode {
let action = TopicIntrospectAction::from_arg_matches(matches).unwrap_or_else(|e| e.exit());
super::super::exit_code_of(run(action))
}
}
fn run(action: TopicIntrospectAction) -> CliResult<()> {
let ws = crate::discover_optional_workspace()?;
let schemas_dir = ws.as_ref().map(|w| w.schemas_dir.as_path());
match action {
TopicIntrospectAction::Type { local, topic } => {
let name = introspect::topic_type_with_scope(
&topic,
schemas_dir,
TopicScope::from_local(local),
)?;
introspect::print_lines(&mut std::io::stdout().lock(), [name.as_str()])
}
TopicIntrospectAction::Find {
local,
connect,
listen,
type_name,
} => {
let found = introspect::topic_find(
&type_name,
schemas_dir,
TopicScope::from_local(local),
connect,
listen,
)?;
introspect::print_lines(
&mut std::io::stdout().lock(),
found.topics.iter().map(String::as_str),
)?;
for note in [
&found.remote_unavailable,
&found.unread_local,
&found.silent_local,
&found.unsearched_mirrors,
&found.unsearched_robots,
]
.into_iter()
.flatten()
{
eprint!("{note}");
}
Ok(())
}
TopicIntrospectAction::Bw {
local,
window,
topic,
} => {
let running = crate::setup_ctrlc_handler()?;
let window = usize::try_from(window)
.ok()
.and_then(NonZeroUsize::new)
.unwrap_or(NonZeroUsize::MIN);
introspect::topic_bw_with_scope(
&topic,
schemas_dir,
running,
&mut std::io::stdout(),
TopicScope::from_local(local),
window,
)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(argv: &[&str]) -> Result<TopicIntrospectAction, clap::Error> {
let matches = crate::families::cli_command().try_get_matches_from(argv)?;
let (_, topic) = matches.subcommand().expect("a verb");
TopicIntrospectAction::from_arg_matches(topic)
}
#[test]
fn topic_type_takes_a_topic_and_local() {
match parse(&["cerulion", "topic", "type", "/a/cmd", "--local"]).unwrap() {
TopicIntrospectAction::Type { local, topic } => {
assert!(local);
assert_eq!(topic, "/a/cmd");
}
_ => panic!("expected type"),
}
}
#[test]
fn topic_find_takes_a_type_name() {
match parse(&["cerulion", "topic", "find", "geometry_msgs/msg/Vector3"]).unwrap() {
TopicIntrospectAction::Find {
local,
connect,
listen,
type_name,
} => {
assert!(!local);
assert!(connect.is_empty() && listen.is_empty());
assert_eq!(type_name, "geometry_msgs/msg/Vector3")
}
_ => panic!("expected find"),
}
match parse(&[
"cerulion",
"topic",
"find",
"std_msgs/String",
"--connect",
"tcp/192.0.2.10:7683",
"--listen",
"tcp/0.0.0.0:7447",
"--connect",
"tcp/192.0.2.11:7683",
])
.unwrap()
{
TopicIntrospectAction::Find {
connect, listen, ..
} => {
assert_eq!(connect, ["tcp/192.0.2.10:7683", "tcp/192.0.2.11:7683"]);
assert_eq!(listen, ["tcp/0.0.0.0:7447"]);
}
_ => panic!("expected find"),
}
match parse(&["cerulion", "topic", "find", "std_msgs/String", "--local"]).unwrap() {
TopicIntrospectAction::Find { local, .. } => assert!(local),
_ => panic!("expected find"),
}
}
#[test]
fn topic_bw_defaults_to_a_window_of_one_hundred() {
match parse(&["cerulion", "topic", "bw", "/a/cmd"]).unwrap() {
TopicIntrospectAction::Bw {
local,
window,
topic,
} => {
assert!(!local);
assert_eq!(window, 100);
assert_eq!(topic, "/a/cmd");
}
_ => panic!("expected bw"),
}
match parse(&["cerulion", "topic", "bw", "/a/cmd", "--window", "2"]).unwrap() {
TopicIntrospectAction::Bw { window, .. } => assert_eq!(window, 2),
_ => panic!("expected bw"),
}
}
#[test]
fn topic_bw_refuses_a_window_that_cannot_give_a_rate_or_is_too_large() {
for bad in ["0", "1", "100001", "many"] {
let err = parse(&["cerulion", "topic", "bw", "/a/cmd", "--window", bad])
.err()
.unwrap_or_else(|| panic!("--window {bad} was accepted"));
assert_eq!(err.kind(), clap::error::ErrorKind::ValueValidation, "{bad}");
}
}
#[test]
fn the_new_topic_verbs_are_quiet_and_gated_like_the_built_in_ones() {
for argv in [
&["cerulion", "topic", "type", "/t"][..],
&["cerulion", "topic", "find", "std_msgs/String"],
&["cerulion", "topic", "bw", "/t"],
] {
let matches = crate::families::cli_command()
.try_get_matches_from(argv)
.expect("parse");
let (verbs, sub) = super::super::find(super::super::NESTED, &matches)
.unwrap_or_else(|| panic!("{argv:?} is not a nested verb"));
assert_eq!(verbs.log_class(sub), VerbLogClass::OneShot, "{argv:?}");
assert!(verbs.needs_identity(sub), "{argv:?}");
}
}
}