blob: 5d55a9f7b5816c290bab6fd5c3708bea5bcc98d5 [file]
// 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)));
});
}
}