use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use async_trait::async_trait;
use clap::{ArgMatches, Command as ClapCommand};
use umbral::{App, Settings};
use umbral_core::cli::{CliError, PluginCommand};
use umbral_core::plugin::Plugin;
struct SmokeCommand {
fired: Arc<AtomicBool>,
}
#[async_trait]
impl PluginCommand for SmokeCommand {
fn command(&self) -> ClapCommand {
ClapCommand::new("smoke-cmd")
.about("dispatch_smoke test fixture - flips an atomic when invoked")
}
async fn run(&self, _matches: &ArgMatches) -> Result<(), CliError> {
self.fired.store(true, Ordering::SeqCst);
Ok(())
}
}
struct SmokePlugin {
fired: Arc<AtomicBool>,
}
impl Plugin for SmokePlugin {
fn name(&self) -> &'static str {
"smoke"
}
fn commands(&self) -> Vec<Box<dyn PluginCommand>> {
vec![Box::new(SmokeCommand {
fired: self.fired.clone(),
})]
}
}
#[tokio::test]
async fn dispatch_routes_argv_to_plugin_contributed_commands() {
let settings = Settings::from_env().expect("figment defaults always load");
let pool = umbral::db::connect_sqlite("sqlite::memory:")
.await
.expect("in-memory sqlite should always connect");
let fired = Arc::new(AtomicBool::new(false));
let plugin = SmokePlugin {
fired: fired.clone(),
};
let app = App::builder()
.settings(settings)
.database("default", pool)
.plugin(plugin)
.build_deferred()
.expect("App::build_deferred should succeed with figment defaults");
let argv: Vec<std::ffi::OsString> = vec!["test-binary".into(), "smoke-cmd".into()];
let result = umbral_cli::dispatch_with_argv(app, argv).await;
assert!(
result.is_ok(),
"dispatch returned an error - plugin command was probably not routed: {:?}",
result.err()
);
assert!(
fired.load(Ordering::SeqCst),
"SmokeCommand::run did not fire - dispatch reached the built-in subcommand parser without consulting plugins. The wire between umbral-cli and umbral-core's cli::dispatch is broken.",
);
}