| // Copyright 2023 Google LLC | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| //! Debug command traits and helpers for defining commands for integration | |
| //! into a debug tool. | |
| use futures::channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender}; | |
| use futures::task::AtomicWaker; | |
| use futures::{FutureExt, Stream, StreamExt}; | |
| use log::LevelFilter; | |
| use std::collections::HashMap; | |
| use std::pin::Pin; | |
| use std::str::FromStr; | |
| use std::sync::{Arc, Mutex}; | |
| use std::task::{Context, Poll}; | |
| #[must_use = "if unused the previous log level will immediately be restored"] | |
| pub struct ScopedVerbosityGuard { | |
| prev_level: LevelFilter, | |
| } | |
| impl ScopedVerbosityGuard { | |
| pub fn new(scoped_level: LevelFilter) -> Self { | |
| let prev_level = log::max_level(); | |
| log::set_max_level(scoped_level); | |
| Self { prev_level } | |
| } | |
| } | |
| impl Drop for ScopedVerbosityGuard { | |
| fn drop(&mut self) { | |
| log::set_max_level(self.prev_level); | |
| } | |
| } | |
| pub fn parse_level_filter(s: &str) -> Option<LevelFilter> { | |
| s.trim().parse().ok() | |
| } | |
| pub fn extract_verbosity_flags(args: Vec<String>) -> (Option<LevelFilter>, Vec<String>) { | |
| let mut level = None; | |
| let mut clean_args = Vec::new(); | |
| let mut iter = args.into_iter().peekable(); | |
| while let Some(arg) = iter.next() { | |
| match arg.as_str() { | |
| "-v" | "--verbose" => { | |
| level = Some(LevelFilter::Debug); | |
| } | |
| "-vv" | "--trace" => { | |
| level = Some(LevelFilter::Trace); | |
| } | |
| "-q" | "--quiet" => { | |
| level = Some(LevelFilter::Warn); | |
| } | |
| "--log-level" => { | |
| if let Some(next) = iter.peek() { | |
| if let Some(lvl) = parse_level_filter(next) { | |
| level = Some(lvl); | |
| iter.next(); | |
| continue; | |
| } | |
| } | |
| clean_args.push(arg); | |
| } | |
| _ if arg.starts_with("--log-level=") => { | |
| let val = &arg["--log-level=".len()..]; | |
| if let Some(lvl) = parse_level_filter(val) { | |
| level = Some(lvl); | |
| } else { | |
| clean_args.push(arg); | |
| } | |
| } | |
| _ => { | |
| clean_args.push(arg); | |
| } | |
| } | |
| } | |
| (level, clean_args) | |
| } | |
| pub fn handle_verbose_command(args: &[String]) { | |
| match args.len() { | |
| 0 => { | |
| println!("Current log level: {}", log::max_level()); | |
| } | |
| 1 => { | |
| if let Some(level) = parse_level_filter(&args[0]) { | |
| log::set_max_level(level); | |
| println!("Log level set to {}", level); | |
| } else { | |
| eprintln!( | |
| "Invalid log level '{}'. Valid levels: off, error, warn, info, debug, trace", | |
| args[0] | |
| ); | |
| } | |
| } | |
| _ => { | |
| println!("Usage: verbose [off|error|warn|info|debug|trace]"); | |
| } | |
| } | |
| } | |
| /// A CommandSet is a set of commands (usually an enum) that each represent an | |
| /// action that can be performed. i.e. 'list', 'volume' etc. Each command can | |
| /// take zero or more arguments and have zero or more flags. | |
| /// Typically an Enum of commands would implement CommandSet trait. | |
| pub trait CommandSet: FromStr + ::core::fmt::Display { | |
| /// Returns a vector of strings that are the commands supported by this. | |
| fn variants() -> Vec<String>; | |
| /// Returns a string listing the arguments that this command takes, in <> | |
| /// brackets | |
| fn arguments(&self) -> &'static str; | |
| /// Returns a string displaying the flags that this command supports, in [] | |
| /// brackets | |
| fn flags(&self) -> &'static str; | |
| /// Returns a short description of this command | |
| fn desc(&self) -> &'static str; | |
| /// Help string for this variant (build from Display, arguments and flags by | |
| /// default) | |
| fn help_simple(&self) -> String { | |
| format!("{self} {} {} -- {}", self.flags(), self.arguments(), self.desc()) | |
| } | |
| /// Possibly multi-line help string for all variants of this set. | |
| fn help_all() -> String { | |
| let mut help: String = Self::variants() | |
| .into_iter() | |
| .filter_map(|s| FromStr::from_str(&s).ok()) | |
| .map(|s: Self| format!("{}\n", s)) | |
| .collect(); | |
| if !help.is_empty() { | |
| help.push('\n'); | |
| } | |
| help.push_str("Global verbosity flags: [-v|--verbose] [-vv|--trace] [-q|--quiet] [--log-level=<level>]\n"); | |
| help | |
| } | |
| } | |
| /// A top-level CLI command: either a framework meta-command (e.g. `verbose`) | |
| /// or a profile-specific domain command. | |
| #[derive(Clone, Copy, Debug, PartialEq, Eq)] | |
| pub enum CliCommand<T> { | |
| Verbose, | |
| Domain(T), | |
| } | |
| impl<T> From<T> for CliCommand<T> { | |
| fn from(cmd: T) -> Self { | |
| CliCommand::Domain(cmd) | |
| } | |
| } | |
| impl<T: CommandSet> CommandSet for CliCommand<T> { | |
| fn variants() -> Vec<String> { | |
| let mut variants = T::variants(); | |
| variants.push("verbose".to_string()); | |
| variants | |
| } | |
| fn arguments(&self) -> &'static str { | |
| match self { | |
| CliCommand::Domain(cmd) => cmd.arguments(), | |
| CliCommand::Verbose => "[level] ", | |
| } | |
| } | |
| fn flags(&self) -> &'static str { | |
| match self { | |
| CliCommand::Domain(cmd) => cmd.flags(), | |
| CliCommand::Verbose => "", | |
| } | |
| } | |
| fn desc(&self) -> &'static str { | |
| match self { | |
| CliCommand::Domain(cmd) => cmd.desc(), | |
| CliCommand::Verbose => "Get or set the logging verbosity level", | |
| } | |
| } | |
| } | |
| impl<T: ::core::fmt::Display> ::core::fmt::Display for CliCommand<T> { | |
| fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { | |
| match self { | |
| CliCommand::Domain(cmd) => write!(f, "{cmd}"), | |
| CliCommand::Verbose => write!(f, "verbose"), | |
| } | |
| } | |
| } | |
| impl<T: FromStr> FromStr for CliCommand<T> { | |
| type Err = T::Err; | |
| fn from_str(s: &str) -> Result<Self, Self::Err> { | |
| if s == "verbose" { | |
| Ok(CliCommand::Verbose) | |
| } else { | |
| T::from_str(s).map(CliCommand::Domain) | |
| } | |
| } | |
| } | |
| /// Macro to help build CommandSets | |
| #[macro_export] | |
| macro_rules! gen_commandset { | |
| ($name:ident { | |
| $($variant:ident = ($val:expr, [$($flag:expr),*], [$($arg:expr),*], $help:expr)),*, | |
| }) => { | |
| /// Enum of all possible commands | |
| #[derive(PartialEq, Debug, Clone, Copy)] | |
| pub enum $name { | |
| $($variant,)* | |
| } | |
| impl CommandSet for $name { | |
| fn variants() -> Vec<String> { | |
| let mut variants = Vec::new(); | |
| $(variants.push($val.to_string());)* | |
| variants | |
| } | |
| fn arguments(&self) -> &'static str { | |
| match self { | |
| $( | |
| $name::$variant => concat!($("<", $arg, "> ",)*), | |
| )* | |
| } | |
| } | |
| fn flags(&self) -> &'static str { | |
| match self { | |
| $( | |
| $name::$variant => concat!($("[", $flag, "] ",)*), | |
| )* | |
| } | |
| } | |
| fn desc(&self) -> &'static str { | |
| match self { | |
| $( | |
| $name::$variant => $help, | |
| )* | |
| } | |
| } | |
| } | |
| impl ::core::fmt::Display for $name { | |
| fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { | |
| match *self { | |
| $($name::$variant => write!(f, $val),)* | |
| } | |
| } | |
| } | |
| impl ::std::str::FromStr for $name { | |
| type Err = (); | |
| fn from_str(s: &str) -> Result<$name, ()> { | |
| match s { | |
| $($val => Ok($name::$variant),)* | |
| _ => Err(()), | |
| } | |
| } | |
| } | |
| } | |
| } | |
| /// An event emitted by a [`CommandRunner`] through its | |
| /// [`CommandRunner::event_stream`]. | |
| #[derive(Debug, Clone, PartialEq, Eq)] | |
| pub enum RunnerEvent<E = ()> { | |
| /// An arbitrary human-readable message from the runner or its background | |
| /// tasks to be displayed by the harness. | |
| Message(String), | |
| /// A structured domain event specific to this runner that the harness or a | |
| /// parent composite tool can react to programmatically. | |
| Event(E), | |
| } | |
| impl<E> RunnerEvent<E> { | |
| pub fn message(&self) -> Option<&str> { | |
| match self { | |
| Self::Message(s) => Some(s), | |
| Self::Event(_) => None, | |
| } | |
| } | |
| pub fn event(&self) -> Option<&E> { | |
| match self { | |
| Self::Message(_) => None, | |
| Self::Event(e) => Some(e), | |
| } | |
| } | |
| pub fn into_event(self) -> Option<E> { | |
| match self { | |
| Self::Message(_) => None, | |
| Self::Event(e) => Some(e), | |
| } | |
| } | |
| pub fn into_message(self) -> Option<String> { | |
| match self { | |
| Self::Message(s) => Some(s), | |
| Self::Event(_) => None, | |
| } | |
| } | |
| pub fn map_event<F, T>(self, f: F) -> RunnerEvent<T> | |
| where | |
| F: FnOnce(E) -> T, | |
| { | |
| match self { | |
| Self::Message(m) => RunnerEvent::Message(m), | |
| Self::Event(e) => RunnerEvent::Event(f(e)), | |
| } | |
| } | |
| } | |
| impl<E> From<String> for RunnerEvent<E> { | |
| fn from(s: String) -> Self { | |
| Self::Message(s) | |
| } | |
| } | |
| impl<E> From<&str> for RunnerEvent<E> { | |
| fn from(s: &str) -> Self { | |
| Self::Message(s.to_string()) | |
| } | |
| } | |
| /// CommandRunner is used to perform a specific task based on the command set. | |
| pub trait CommandRunner { | |
| type Set: CommandSet; | |
| type Error: ::std::error::Error; | |
| type Event; | |
| fn run_command( | |
| &self, | |
| cmd: Self::Set, | |
| args: Vec<String>, | |
| ) -> impl futures::Future<Output = Result<(), Self::Error>>; | |
| /// Returns a stream that drives background processing for this runner and | |
| /// yields display messages or structured events for the harness. | |
| /// | |
| /// Crates that do not need background tasks or event delivery can set | |
| /// `type Event = ();` and rely on this default implementation. | |
| fn event_stream(&self) -> impl Stream<Item = RunnerEvent<Self::Event>> + '_ { | |
| futures::stream::pending::<RunnerEvent<Self::Event>>() | |
| } | |
| fn run( | |
| &self, | |
| cmd: impl Into<CliCommand<Self::Set>>, | |
| args: Vec<String>, | |
| ) -> impl futures::Future<Output = Result<(), Self::Error>> { | |
| let cmd = cmd.into(); | |
| async move { | |
| match cmd { | |
| CliCommand::Verbose => { | |
| handle_verbose_command(&args); | |
| Ok(()) | |
| } | |
| CliCommand::Domain(cmd) => { | |
| let (scoped_level, clean_args) = extract_verbosity_flags(args); | |
| let _guard = scoped_level.map(ScopedVerbosityGuard::new); | |
| self.run_command(cmd, clean_args).await | |
| } | |
| } | |
| } | |
| } | |
| } | |
| impl<R: CommandRunner> CommandRunner for &R { | |
| type Set = R::Set; | |
| type Error = R::Error; | |
| type Event = R::Event; | |
| fn run_command( | |
| &self, | |
| cmd: Self::Set, | |
| args: Vec<String>, | |
| ) -> impl futures::Future<Output = Result<(), Self::Error>> { | |
| (**self).run_command(cmd, args) | |
| } | |
| fn event_stream(&self) -> impl Stream<Item = RunnerEvent<Self::Event>> + '_ { | |
| (**self).event_stream() | |
| } | |
| } | |
| impl<R: CommandRunner> CommandRunner for Arc<R> { | |
| type Set = R::Set; | |
| type Error = R::Error; | |
| type Event = R::Event; | |
| fn run_command( | |
| &self, | |
| cmd: Self::Set, | |
| args: Vec<String>, | |
| ) -> impl futures::Future<Output = Result<(), Self::Error>> { | |
| (**self).run_command(cmd, args) | |
| } | |
| fn event_stream(&self) -> impl Stream<Item = RunnerEvent<Self::Event>> + '_ { | |
| (**self).event_stream() | |
| } | |
| } | |
| type BoxedStream<E> = Pin<Box<dyn Stream<Item = RunnerEvent<E>> + Send + 'static>>; | |
| struct StreamsState<E> { | |
| keyed: HashMap<String, BoxedStream<E>>, | |
| anonymous: Vec<BoxedStream<E>>, | |
| } | |
| impl<E> Default for StreamsState<E> { | |
| fn default() -> Self { | |
| Self { keyed: HashMap::new(), anonymous: Vec::new() } | |
| } | |
| } | |
| impl<E: Send + 'static> Stream for StreamsState<E> { | |
| type Item = RunnerEvent<E>; | |
| fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> { | |
| let mut ready = None; | |
| self.keyed.retain(|_key, stream| { | |
| if ready.is_some() { | |
| return true; | |
| } | |
| match stream.as_mut().poll_next(cx) { | |
| Poll::Ready(Some(item)) => { | |
| ready = Some(item); | |
| true | |
| } | |
| Poll::Ready(None) => false, | |
| Poll::Pending => true, | |
| } | |
| }); | |
| if let Some(item) = ready { | |
| return Poll::Ready(Some(item)); | |
| } | |
| self.anonymous.retain_mut(|stream| { | |
| if ready.is_some() { | |
| return true; | |
| } | |
| match stream.as_mut().poll_next(cx) { | |
| Poll::Ready(Some(item)) => { | |
| ready = Some(item); | |
| true | |
| } | |
| Poll::Ready(None) => false, | |
| Poll::Pending => true, | |
| } | |
| }); | |
| if let Some(item) = ready { | |
| return Poll::Ready(Some(item)); | |
| } | |
| Poll::Pending | |
| } | |
| } | |
| struct RunnerBackgroundInner<E: Send + 'static> { | |
| msg_sender: UnboundedSender<RunnerEvent<E>>, | |
| msg_receiver: Mutex<UnboundedReceiver<RunnerEvent<E>>>, | |
| streams: Mutex<StreamsState<E>>, | |
| waker: AtomicWaker, | |
| } | |
| /// Helper for managing background streams, display messages, and structured | |
| /// events for a [`CommandRunner`]. | |
| #[derive(Clone)] | |
| pub struct RunnerBackground<E: Send + 'static = ()> { | |
| inner: Arc<RunnerBackgroundInner<E>>, | |
| } | |
| impl<E: Send + 'static> RunnerBackground<E> { | |
| pub fn new() -> Self { | |
| let (msg_sender, msg_receiver) = unbounded(); | |
| Self { | |
| inner: Arc::new(RunnerBackgroundInner { | |
| msg_sender, | |
| msg_receiver: Mutex::new(msg_receiver), | |
| streams: Mutex::new(StreamsState::default()), | |
| waker: AtomicWaker::new(), | |
| }), | |
| } | |
| } | |
| /// Queues a displayable text message to be emitted by | |
| /// [`Self::event_stream`]. | |
| pub fn println(&self, msg: impl Into<String>) { | |
| let _ = self.inner.msg_sender.unbounded_send(RunnerEvent::Message(msg.into())); | |
| self.inner.waker.wake(); | |
| } | |
| /// Queues a structured domain event to be emitted by | |
| /// [`Self::event_stream`]. | |
| pub fn send(&self, event: E) { | |
| let _ = self.inner.msg_sender.unbounded_send(RunnerEvent::Event(event)); | |
| self.inner.waker.wake(); | |
| } | |
| /// Returns an [`UnboundedSender`] that converts sent items into | |
| /// [`RunnerEvent::Event`]. | |
| pub fn event_sender(&self) -> UnboundedSender<E> { | |
| let (tx, rx) = unbounded::<E>(); | |
| self.spawn_stream(rx.map(RunnerEvent::Event)); | |
| tx | |
| } | |
| /// Dynamically registers or replaces a background stream under a unique | |
| /// key. If a stream was already registered under `key`, the previous | |
| /// stream is cancelled. | |
| pub fn set_stream<K: Into<String>>( | |
| &self, | |
| key: K, | |
| stream: impl Stream<Item = RunnerEvent<E>> + Send + 'static, | |
| ) { | |
| let mut streams = self.inner.streams.lock().unwrap(); | |
| streams.keyed.insert(key.into(), Box::pin(stream)); | |
| drop(streams); | |
| self.inner.waker.wake(); | |
| } | |
| /// Cancels the stream registered under `key`, if present. Returns `true` if | |
| /// a stream was cancelled. | |
| pub fn cancel_stream(&self, key: &str) -> bool { | |
| let mut streams = self.inner.streams.lock().unwrap(); | |
| let removed = streams.keyed.remove(key).is_some(); | |
| drop(streams); | |
| if removed { | |
| self.inner.waker.wake(); | |
| } | |
| removed | |
| } | |
| /// Spawns an anonymous background stream that runs until completion. | |
| pub fn spawn_stream(&self, stream: impl Stream<Item = RunnerEvent<E>> + Send + 'static) { | |
| let mut streams = self.inner.streams.lock().unwrap(); | |
| streams.anonymous.push(Box::pin(stream)); | |
| drop(streams); | |
| self.inner.waker.wake(); | |
| } | |
| /// Returns `true` if a background stream is currently registered under | |
| /// `key`. | |
| pub fn has_stream(&self, key: &str) -> bool { | |
| self.inner.streams.lock().unwrap().keyed.contains_key(key) | |
| } | |
| /// Returns the total number of active background streams (keyed and | |
| /// anonymous). | |
| pub fn active_stream_count(&self) -> usize { | |
| let streams = self.inner.streams.lock().unwrap(); | |
| streams.keyed.len() + streams.anonymous.len() | |
| } | |
| /// Returns a stream yielding messages and events produced by this runner. | |
| pub fn event_stream(&self) -> RunnerBackgroundStream<E> { | |
| RunnerBackgroundStream { inner: self.inner.clone() } | |
| } | |
| } | |
| impl<E: Send + 'static> RunnerBackgroundInner<E> { | |
| fn poll_next_event(&self, cx: &mut Context<'_>) -> Poll<Option<RunnerEvent<E>>> { | |
| self.waker.register(cx.waker()); | |
| // 1. Drain directly queued messages/events from msg_receiver first. | |
| if let Poll::Ready(Some(item)) = self.msg_receiver.lock().unwrap().poll_next_unpin(cx) { | |
| return Poll::Ready(Some(item)); | |
| } | |
| // 2. Poll registered background streams. | |
| if let Poll::Ready(Some(item)) = self.streams.lock().unwrap().poll_next_unpin(cx) { | |
| return Poll::Ready(Some(item)); | |
| } | |
| Poll::Pending | |
| } | |
| } | |
| impl<E: Send + 'static> Default for RunnerBackground<E> { | |
| fn default() -> Self { | |
| Self::new() | |
| } | |
| } | |
| /// Stream returned by [`RunnerBackground::event_stream`]. | |
| pub struct RunnerBackgroundStream<E: Send + 'static = ()> { | |
| inner: Arc<RunnerBackgroundInner<E>>, | |
| } | |
| impl<E: Send + 'static> Stream for RunnerBackgroundStream<E> { | |
| type Item = RunnerEvent<E>; | |
| fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> { | |
| self.inner.poll_next_event(cx) | |
| } | |
| } | |
| /// A harness for driving a [`CommandRunner`] alongside its background event | |
| /// stream. | |
| pub struct DebugHarness<R: CommandRunner> { | |
| runner: R, | |
| } | |
| impl<R: CommandRunner> DebugHarness<R> { | |
| pub fn new(runner: R) -> Self { | |
| Self { runner } | |
| } | |
| pub fn runner(&self) -> &R { | |
| &self.runner | |
| } | |
| pub fn runner_mut(&mut self) -> &mut R { | |
| &mut self.runner | |
| } | |
| pub fn into_runner(self) -> R { | |
| self.runner | |
| } | |
| pub fn event_stream(&self) -> impl Stream<Item = RunnerEvent<R::Event>> + '_ { | |
| self.runner.event_stream() | |
| } | |
| pub async fn run( | |
| &self, | |
| cmd: impl Into<CliCommand<R::Set>>, | |
| args: Vec<String>, | |
| ) -> Result<(), R::Error> { | |
| self.runner.run(cmd, args).await | |
| } | |
| async fn drive_events<Fut, F>(&self, future: Fut, mut on_event: F) -> Fut::Output | |
| where | |
| Fut: futures::Future, | |
| F: FnMut(RunnerEvent<R::Event>), | |
| { | |
| let stream = self.runner.event_stream().fuse(); | |
| futures::pin_mut!(stream); | |
| let future = future.fuse(); | |
| futures::pin_mut!(future); | |
| loop { | |
| futures::select! { | |
| event = stream.select_next_some() => on_event(event), | |
| output = future => { | |
| while let Some(Some(event)) = stream.next().now_or_never() { | |
| on_event(event); | |
| } | |
| return output; | |
| } | |
| } | |
| } | |
| } | |
| /// Concurrently drives the runner's background `event_stream` while | |
| /// executing `future`. Messages are passed to `on_message` and events | |
| /// are passed to `on_event`. | |
| pub async fn drive_with<Fut, FM, FE>( | |
| &self, | |
| future: Fut, | |
| mut on_message: FM, | |
| mut on_event: FE, | |
| ) -> Fut::Output | |
| where | |
| Fut: futures::Future, | |
| FM: FnMut(String), | |
| FE: FnMut(R::Event), | |
| { | |
| self.drive_events(future, |event| match event { | |
| RunnerEvent::Message(msg) => on_message(msg), | |
| RunnerEvent::Event(evt) => on_event(evt), | |
| }) | |
| .await | |
| } | |
| /// Concurrently executes `future` while collecting all `RunnerEvent`s | |
| /// emitted during its execution. | |
| pub async fn run_collecting_events<Fut>( | |
| &self, | |
| future: Fut, | |
| ) -> (Fut::Output, Vec<RunnerEvent<R::Event>>) | |
| where | |
| Fut: futures::Future, | |
| { | |
| let mut events = Vec::new(); | |
| let output = self.drive_events(future, |event| events.push(event)).await; | |
| (output, events) | |
| } | |
| /// Concurrently executes a command while collecting all `RunnerEvent`s | |
| /// emitted during its execution. | |
| pub async fn run_command_collecting( | |
| &self, | |
| cmd: impl Into<CliCommand<R::Set>>, | |
| args: Vec<String>, | |
| ) -> (Result<(), R::Error>, Vec<RunnerEvent<R::Event>>) { | |
| self.run_collecting_events(self.run(cmd, args)).await | |
| } | |
| } | |
| #[cfg(test)] | |
| mod tests { | |
| use super::*; | |
| #[test] | |
| fn gen_commandset_simple() { | |
| gen_commandset! { | |
| TestCmd { | |
| One = ("one", [], [], "First Command"), | |
| WithFlags = ("with-flags", ["-1","-2"], [], "Command with flags"), | |
| WithArgs = ("with-args", [], ["arg", "two"], "Command with args"), | |
| WithBoth = ("with-both", ["-w"], ["simple"], "Command with both flags and args"), | |
| } | |
| } | |
| let cmd: TestCmd = "one".parse().unwrap(); | |
| assert_eq!(cmd, TestCmd::One); | |
| let cmd2: TestCmd = "with-flags".parse().unwrap(); | |
| assert_eq!(cmd2.arguments(), ""); | |
| assert_eq!(cmd2.flags(), "[-1] [-2] "); | |
| let verbose_cmd: CliCommand<TestCmd> = "verbose".parse().unwrap(); | |
| assert_eq!(verbose_cmd, CliCommand::Verbose); | |
| assert_eq!(verbose_cmd.flags(), ""); | |
| assert_eq!(verbose_cmd.to_string(), "verbose"); | |
| let domain_cmd: CliCommand<TestCmd> = "one".parse().unwrap(); | |
| assert_eq!(domain_cmd, CliCommand::Domain(TestCmd::One)); | |
| assert!(!TestCmd::variants().contains(&"verbose".to_string())); | |
| assert!(CliCommand::<TestCmd>::variants().contains(&"verbose".to_string())); | |
| assert!(TestCmd::help_all().contains("Global verbosity flags:")); | |
| assert!(CliCommand::<TestCmd>::help_all().contains("Global verbosity flags:")); | |
| } | |
| #[test] | |
| fn test_extract_verbosity_flags() { | |
| // -v / --verbose | |
| let (lvl, args) = extract_verbosity_flags(vec!["-v".to_string()]); | |
| assert_eq!(lvl, Some(LevelFilter::Debug)); | |
| assert!(args.is_empty()); | |
| let (lvl, args) = extract_verbosity_flags(vec!["--verbose".to_string()]); | |
| assert_eq!(lvl, Some(LevelFilter::Debug)); | |
| assert!(args.is_empty()); | |
| // -vv / --trace | |
| let (lvl, args) = extract_verbosity_flags(vec!["-vv".to_string()]); | |
| assert_eq!(lvl, Some(LevelFilter::Trace)); | |
| assert!(args.is_empty()); | |
| let (lvl, args) = extract_verbosity_flags(vec!["--trace".to_string()]); | |
| assert_eq!(lvl, Some(LevelFilter::Trace)); | |
| assert!(args.is_empty()); | |
| // -q / --quiet | |
| let (lvl, args) = extract_verbosity_flags(vec!["-q".to_string()]); | |
| assert_eq!(lvl, Some(LevelFilter::Warn)); | |
| assert!(args.is_empty()); | |
| let (lvl, args) = extract_verbosity_flags(vec!["--quiet".to_string()]); | |
| assert_eq!(lvl, Some(LevelFilter::Warn)); | |
| assert!(args.is_empty()); | |
| // --log-level=<level> and --log-level <level> (case-insensitive) | |
| for (name, expected) in [ | |
| ("off", LevelFilter::Off), | |
| ("error", LevelFilter::Error), | |
| ("warn", LevelFilter::Warn), | |
| ("info", LevelFilter::Info), | |
| ("debug", LevelFilter::Debug), | |
| ("trace", LevelFilter::Trace), | |
| ("DEBUG", LevelFilter::Debug), | |
| ("Info", LevelFilter::Info), | |
| ] { | |
| let (lvl, args) = extract_verbosity_flags(vec![format!("--log-level={name}")]); | |
| assert_eq!(lvl, Some(expected)); | |
| assert!(args.is_empty()); | |
| let (lvl, args) = | |
| extract_verbosity_flags(vec!["--log-level".to_string(), name.to_string()]); | |
| assert_eq!(lvl, Some(expected)); | |
| assert!(args.is_empty()); | |
| } | |
| // Mixed arguments | |
| let input = vec![ | |
| "action".to_string(), | |
| "-v".to_string(), | |
| "--custom-flag".to_string(), | |
| "target".to_string(), | |
| "--log-level=warn".to_string(), | |
| "extra".to_string(), | |
| ]; | |
| let (lvl, args) = extract_verbosity_flags(input); | |
| assert_eq!(lvl, Some(LevelFilter::Warn)); | |
| assert_eq!(args, vec!["action", "--custom-flag", "target", "extra"]); | |
| // Invalid log level flag should not be extracted | |
| let (lvl, args) = | |
| extract_verbosity_flags(vec!["--log-level=invalid".to_string(), "arg".to_string()]); | |
| assert_eq!(lvl, None); | |
| assert_eq!(args, vec!["--log-level=invalid", "arg"]); | |
| } | |
| // Mutex to avoid parallel tests fighting over log levels | |
| static LOG_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(()); | |
| #[test] | |
| fn test_scoped_verbosity_guard() { | |
| let _lock = LOG_MUTEX.lock().unwrap(); | |
| log::set_max_level(LevelFilter::Info); | |
| futures::executor::block_on(async { | |
| assert_eq!(log::max_level(), LevelFilter::Info); | |
| { | |
| let _guard = ScopedVerbosityGuard::new(LevelFilter::Trace); | |
| assert_eq!(log::max_level(), LevelFilter::Trace); | |
| // Yield across async boundary | |
| futures::future::ready(()).await; | |
| assert_eq!(log::max_level(), LevelFilter::Trace); | |
| } | |
| assert_eq!(log::max_level(), LevelFilter::Info); | |
| }); | |
| } | |
| #[test] | |
| fn test_persistent_verbosity_and_handle_command() { | |
| let _lock = LOG_MUTEX.lock().unwrap(); | |
| log::set_max_level(LevelFilter::Debug); | |
| // handle_verbose_command with 0 args | |
| handle_verbose_command(&[]); | |
| assert_eq!(log::max_level(), LevelFilter::Debug); | |
| // handle_verbose_command with 1 valid arg | |
| handle_verbose_command(&["trace".to_string()]); | |
| assert_eq!(log::max_level(), LevelFilter::Trace); | |
| handle_verbose_command(&["WARN".to_string()]); | |
| assert_eq!(log::max_level(), LevelFilter::Warn); | |
| // handle_verbose_command with invalid arg (should not change) | |
| handle_verbose_command(&["invalid_level".to_string()]); | |
| assert_eq!(log::max_level(), LevelFilter::Warn); | |
| // handle_verbose_command with > 1 args (should not change) | |
| handle_verbose_command(&["info".to_string(), "extra".to_string()]); | |
| assert_eq!(log::max_level(), LevelFilter::Warn); | |
| // Reset back to Info | |
| log::set_max_level(LevelFilter::Info); | |
| } | |
| #[test] | |
| fn test_command_runner_run() { | |
| gen_commandset! { | |
| RunnerCmd { | |
| Action = ("action", [], ["target"], "Perform action"), | |
| } | |
| } | |
| struct TestRunner { | |
| executed: std::sync::Mutex<Vec<(RunnerCmd, Vec<String>)>>, | |
| } | |
| impl CommandRunner for TestRunner { | |
| type Set = RunnerCmd; | |
| type Error = std::io::Error; | |
| type Event = (); | |
| fn run_command( | |
| &self, | |
| cmd: Self::Set, | |
| args: Vec<String>, | |
| ) -> impl futures::Future<Output = Result<(), Self::Error>> { | |
| self.executed.lock().unwrap().push((cmd, args)); | |
| futures::future::ready(Ok(())) | |
| } | |
| } | |
| let runner = TestRunner { executed: std::sync::Mutex::new(Vec::new()) }; | |
| let _lock = LOG_MUTEX.lock().unwrap(); | |
| // Running Verbose command should update persistent verbosity and NOT | |
| // invoke run_command | |
| log::set_max_level(LevelFilter::Info); | |
| futures::executor::block_on(runner.run(CliCommand::Verbose, vec!["trace".to_string()])) | |
| .unwrap(); | |
| assert_eq!(log::max_level(), LevelFilter::Trace); | |
| assert!(runner.executed.lock().unwrap().is_empty()); | |
| // Running normal command with verbosity flag strips flag and runs | |
| // command | |
| let args = vec!["-v".to_string(), "my_target".to_string()]; | |
| futures::executor::block_on(runner.run(RunnerCmd::Action, args)).unwrap(); | |
| let executed = runner.executed.lock().unwrap().clone(); | |
| assert_eq!(executed.len(), 1); | |
| assert_eq!(executed[0], (RunnerCmd::Action, vec!["my_target".to_string()])); | |
| // After run completes, the guard has restored the previous level | |
| assert_eq!(log::max_level(), LevelFilter::Trace); | |
| // Reset persistent verbosity back to Info | |
| log::set_max_level(LevelFilter::Info); | |
| } | |
| #[test] | |
| fn test_command_runner_default_event_stream() { | |
| gen_commandset! { | |
| DefaultCmd { | |
| Action = ("action", [], [], "Default action"), | |
| } | |
| } | |
| struct SimpleRunner; | |
| impl CommandRunner for SimpleRunner { | |
| type Set = DefaultCmd; | |
| type Error = std::io::Error; | |
| type Event = (); | |
| fn run_command( | |
| &self, | |
| _cmd: Self::Set, | |
| _args: Vec<String>, | |
| ) -> impl futures::Future<Output = Result<(), Self::Error>> { | |
| futures::future::ready(Ok(())) | |
| } | |
| } | |
| let runner = SimpleRunner; | |
| let stream = runner.event_stream(); | |
| futures::pin_mut!(stream); | |
| let mut cx = std::task::Context::from_waker(futures::task::noop_waker_ref()); | |
| assert!(stream.as_mut().poll_next(&mut cx).is_pending()); | |
| } | |
| #[test] | |
| fn test_runner_event() { | |
| let msg: RunnerEvent<u32> = RunnerEvent::Message("hello".to_string()); | |
| assert_eq!(msg.message(), Some("hello")); | |
| assert_eq!(msg.event(), None); | |
| assert_eq!(msg.clone().into_event(), None); | |
| assert_eq!(msg.clone().into_message(), Some("hello".to_string())); | |
| let evt: RunnerEvent<u32> = RunnerEvent::Event(42); | |
| assert_eq!(evt.message(), None); | |
| assert_eq!(evt.event(), Some(&42)); | |
| assert_eq!(evt.clone().into_event(), Some(42)); | |
| assert_eq!(evt.into_message(), None); | |
| let str_msg: RunnerEvent<()> = "test msg".into(); | |
| assert_eq!(str_msg, RunnerEvent::Message("test msg".to_string())); | |
| let string_msg: RunnerEvent<()> = String::from("string msg").into(); | |
| assert_eq!(string_msg, RunnerEvent::Message("string msg".to_string())); | |
| let mapped = RunnerEvent::Event(10).map_event(|x| x * 2); | |
| assert_eq!(mapped, RunnerEvent::Event(20)); | |
| let mapped_msg: RunnerEvent<i32> = | |
| RunnerEvent::Message("hi".to_string()).map_event(|x: u32| x as i32); | |
| assert_eq!(mapped_msg, RunnerEvent::Message("hi".to_string())); | |
| } | |
| #[test] | |
| fn test_runner_background_println_and_send() { | |
| futures::executor::block_on(async { | |
| let bg: RunnerBackground<u32> = RunnerBackground::new(); | |
| bg.println("log message 1"); | |
| bg.send(100); | |
| bg.println("log message 2"); | |
| let stream = bg.event_stream(); | |
| futures::pin_mut!(stream); | |
| assert_eq!( | |
| stream.next().await, | |
| Some(RunnerEvent::Message("log message 1".to_string())) | |
| ); | |
| assert_eq!(stream.next().await, Some(RunnerEvent::Event(100))); | |
| assert_eq!( | |
| stream.next().await, | |
| Some(RunnerEvent::Message("log message 2".to_string())) | |
| ); | |
| // When no more events, polling should return Pending. | |
| let mut cx = std::task::Context::from_waker(futures::task::noop_waker_ref()); | |
| assert!(stream.as_mut().poll_next(&mut cx).is_pending()); | |
| }); | |
| } | |
| #[test] | |
| fn test_runner_background_event_sender() { | |
| futures::executor::block_on(async { | |
| let bg: RunnerBackground<String> = RunnerBackground::new(); | |
| let sender = bg.event_sender(); | |
| sender.unbounded_send("hello".to_string()).unwrap(); | |
| sender.unbounded_send("world".to_string()).unwrap(); | |
| let stream = bg.event_stream(); | |
| futures::pin_mut!(stream); | |
| assert_eq!(stream.next().await, Some(RunnerEvent::Event("hello".to_string()))); | |
| assert_eq!(stream.next().await, Some(RunnerEvent::Event("world".to_string()))); | |
| drop(sender); | |
| // Polling after dropping sender should clean up the stream and | |
| // return Pending. | |
| let mut cx = std::task::Context::from_waker(futures::task::noop_waker_ref()); | |
| assert!(stream.as_mut().poll_next(&mut cx).is_pending()); | |
| assert_eq!(bg.active_stream_count(), 0); | |
| }); | |
| } | |
| #[test] | |
| fn test_runner_background_keyed_streams_and_cancellation() { | |
| futures::executor::block_on(async { | |
| let bg: RunnerBackground<u32> = RunnerBackground::new(); | |
| assert_eq!(bg.has_stream("discovery"), false); | |
| let (tx1, rx1) = futures::channel::mpsc::unbounded::<RunnerEvent<u32>>(); | |
| bg.set_stream("discovery", rx1); | |
| assert_eq!(bg.has_stream("discovery"), true); | |
| assert_eq!(bg.active_stream_count(), 1); | |
| tx1.unbounded_send(RunnerEvent::Event(1)).unwrap(); | |
| let stream = bg.event_stream(); | |
| futures::pin_mut!(stream); | |
| assert_eq!(stream.next().await, Some(RunnerEvent::Event(1))); | |
| // Replacing the stream cancels the old one. | |
| let (tx2, rx2) = futures::channel::mpsc::unbounded::<RunnerEvent<u32>>(); | |
| bg.set_stream("discovery", rx2); | |
| assert_eq!(bg.has_stream("discovery"), true); | |
| // Old tx1 should have receiver dropped / closed. | |
| assert!(tx1.is_closed()); | |
| tx2.unbounded_send(RunnerEvent::Event(2)).unwrap(); | |
| assert_eq!(stream.next().await, Some(RunnerEvent::Event(2))); | |
| // Cancelling the stream. | |
| assert!(bg.cancel_stream("discovery")); | |
| assert_eq!(bg.has_stream("discovery"), false); | |
| assert!(tx2.is_closed()); | |
| assert!(!bg.cancel_stream("discovery")); | |
| assert_eq!(bg.active_stream_count(), 0); | |
| }); | |
| } | |
| #[test] | |
| fn test_runner_background_spawn_stream_and_cleanup() { | |
| futures::executor::block_on(async { | |
| let bg: RunnerBackground<u32> = RunnerBackground::new(); | |
| let stream_data = futures::stream::iter(vec![ | |
| RunnerEvent::Message("from stream".to_string()), | |
| RunnerEvent::Event(99), | |
| ]); | |
| bg.spawn_stream(stream_data); | |
| assert_eq!(bg.active_stream_count(), 1); | |
| let stream = bg.event_stream(); | |
| futures::pin_mut!(stream); | |
| assert_eq!(stream.next().await, Some(RunnerEvent::Message("from stream".to_string()))); | |
| assert_eq!(stream.next().await, Some(RunnerEvent::Event(99))); | |
| // Polling again will observe the stream returned None and clean it | |
| // up. | |
| let mut cx = std::task::Context::from_waker(futures::task::noop_waker_ref()); | |
| assert!(stream.as_mut().poll_next(&mut cx).is_pending()); | |
| assert_eq!(bg.active_stream_count(), 0); | |
| }); | |
| } | |
| #[test] | |
| fn test_runner_background_waker_waking() { | |
| let bg: RunnerBackground<u32> = RunnerBackground::new(); | |
| let bg_clone = bg.clone(); | |
| let (polled_tx, polled_rx) = futures::channel::oneshot::channel(); | |
| let handle = std::thread::spawn(move || { | |
| futures::executor::block_on(polled_rx).unwrap(); | |
| bg_clone.println("woken up"); | |
| bg_clone.send(42); | |
| }); | |
| futures::executor::block_on(async { | |
| let stream = bg.event_stream(); | |
| futures::pin_mut!(stream); | |
| let mut polled_tx = Some(polled_tx); | |
| let first = futures::future::poll_fn(|cx| { | |
| let res = stream.as_mut().poll_next(cx); | |
| if let Some(tx) = polled_tx.take() { | |
| assert!(res.is_pending()); | |
| tx.send(()).unwrap(); | |
| } | |
| res | |
| }) | |
| .await; | |
| assert_eq!(first, Some(RunnerEvent::Message("woken up".to_string()))); | |
| assert_eq!(stream.next().await, Some(RunnerEvent::Event(42))); | |
| }); | |
| handle.join().unwrap(); | |
| } | |
| #[test] | |
| fn test_debug_harness() { | |
| gen_commandset! { | |
| HarnessCmd { | |
| Greet = ("greet", [], ["name"], "Greet a user"), | |
| Trigger = ("trigger", [], [], "Trigger background event"), | |
| } | |
| } | |
| struct HarnessRunner { | |
| bg: RunnerBackground<String>, | |
| } | |
| impl CommandRunner for HarnessRunner { | |
| type Set = HarnessCmd; | |
| type Error = std::io::Error; | |
| type Event = String; | |
| fn run_command( | |
| &self, | |
| cmd: Self::Set, | |
| args: Vec<String>, | |
| ) -> impl futures::Future<Output = Result<(), Self::Error>> { | |
| match cmd { | |
| HarnessCmd::Greet => { | |
| let name = args.first().cloned().unwrap_or_else(|| "world".to_string()); | |
| self.bg.println(format!("Hello, {}!", name)); | |
| } | |
| HarnessCmd::Trigger => { | |
| self.bg.send("event_fired".to_string()); | |
| } | |
| } | |
| futures::future::ready(Ok(())) | |
| } | |
| fn event_stream(&self) -> impl Stream<Item = RunnerEvent<Self::Event>> + '_ { | |
| self.bg.event_stream() | |
| } | |
| } | |
| let runner = HarnessRunner { bg: RunnerBackground::new() }; | |
| let harness = DebugHarness::new(runner); | |
| futures::executor::block_on(async { | |
| // Test run_command_collecting | |
| let (res, events) = | |
| harness.run_command_collecting(HarnessCmd::Greet, vec!["Alice".to_string()]).await; | |
| assert!(res.is_ok()); | |
| assert_eq!(events, vec![RunnerEvent::Message("Hello, Alice!".to_string())]); | |
| // Test drive_with with Trigger | |
| let mut messages = Vec::new(); | |
| let mut domain_events = Vec::new(); | |
| harness | |
| .drive_with( | |
| harness.run(HarnessCmd::Trigger, vec![]), | |
| |msg| messages.push(msg), | |
| |evt| domain_events.push(evt), | |
| ) | |
| .await | |
| .unwrap(); | |
| assert!(messages.is_empty()); | |
| assert_eq!(domain_events, vec!["event_fired".to_string()]); | |
| }); | |
| } | |
| #[test] | |
| fn test_debug_harness_terminating_stream() { | |
| gen_commandset! { | |
| FiniteCmd { | |
| Action = ("action", [], [], "Finite action"), | |
| } | |
| } | |
| struct FiniteStreamRunner { | |
| stream_done_tx: std::sync::Mutex<Option<futures::channel::oneshot::Sender<()>>>, | |
| } | |
| impl CommandRunner for FiniteStreamRunner { | |
| type Set = FiniteCmd; | |
| type Error = std::io::Error; | |
| type Event = u32; | |
| fn run_command( | |
| &self, | |
| _cmd: Self::Set, | |
| _args: Vec<String>, | |
| ) -> impl futures::Future<Output = Result<(), Self::Error>> { | |
| futures::future::ready(Ok(())) | |
| } | |
| fn event_stream(&self) -> impl Stream<Item = RunnerEvent<Self::Event>> + '_ { | |
| let mut done_tx = self.stream_done_tx.lock().unwrap().take(); | |
| futures::stream::iter(vec![ | |
| RunnerEvent::Message("finite msg".to_string()), | |
| RunnerEvent::Event(42), | |
| ]) | |
| .chain(futures::stream::poll_fn(move |_| { | |
| if let Some(tx) = done_tx.take() { | |
| let _ = tx.send(()); | |
| } | |
| Poll::Ready(None) | |
| })) | |
| } | |
| } | |
| let (stream_done_tx, stream_done_rx) = futures::channel::oneshot::channel::<()>(); | |
| let harness = DebugHarness::new(FiniteStreamRunner { | |
| stream_done_tx: std::sync::Mutex::new(Some(stream_done_tx)), | |
| }); | |
| futures::executor::block_on(async { | |
| let (tx, rx) = futures::channel::oneshot::channel::<()>(); | |
| let handle = std::thread::spawn(move || { | |
| futures::executor::block_on(stream_done_rx).unwrap(); | |
| let _ = tx.send(()); | |
| }); | |
| // The stream terminates after 2 items while rx is still pending. | |
| // run_collecting_events should not busy-loop and should cleanly | |
| // complete when rx resolves. | |
| let (output, events) = harness.run_collecting_events(rx).await; | |
| assert!(output.is_ok()); | |
| assert_eq!( | |
| events, | |
| vec![RunnerEvent::Message("finite msg".to_string()), RunnerEvent::Event(42),] | |
| ); | |
| handle.join().unwrap(); | |
| }); | |
| } | |
| #[test] | |
| fn test_runner_background_stream_static() { | |
| let bg: RunnerBackground<u32> = RunnerBackground::new(); | |
| bg.send(77); | |
| let stream = bg.event_stream(); | |
| fn assert_static<T: 'static>(_: &T) {} | |
| assert_static(&stream); | |
| futures::executor::block_on(async { | |
| futures::pin_mut!(stream); | |
| assert_eq!(stream.next().await, Some(RunnerEvent::Event(77))); | |
| }); | |
| } | |
| } |