use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use arcature::cli::{Operations, Subcommand, SubcommandError, dispatch, parse};
struct RecordingOps {
method: Arc<AtomicUsize>,
args: Arc<Mutex<Vec<String>>>,
}
impl Operations for RecordingOps {
fn serve(
&self,
args: &[String],
) -> std::pin::Pin<Box<dyn std::future::Future<Output = arcature::Result<()>> + Send + '_>>
{
let method = self.method.clone();
let captured = self.args.clone();
let args = args.to_vec();
Box::pin(async move {
method.store(1, Ordering::SeqCst);
*captured.lock().expect("lock") = args;
Ok(())
})
}
fn migrate(
&self,
args: &[String],
) -> std::pin::Pin<Box<dyn std::future::Future<Output = arcature::Result<()>> + Send + '_>>
{
let method = self.method.clone();
let captured = self.args.clone();
let args = args.to_vec();
Box::pin(async move {
method.store(2, Ordering::SeqCst);
*captured.lock().expect("lock") = args;
Ok(())
})
}
fn queue(
&self,
args: &[String],
) -> std::pin::Pin<Box<dyn std::future::Future<Output = arcature::Result<()>> + Send + '_>>
{
let method = self.method.clone();
let captured = self.args.clone();
let args = args.to_vec();
Box::pin(async move {
method.store(3, Ordering::SeqCst);
*captured.lock().expect("lock") = args;
Ok(())
})
}
fn schedule(
&self,
args: &[String],
) -> std::pin::Pin<Box<dyn std::future::Future<Output = arcature::Result<()>> + Send + '_>>
{
let method = self.method.clone();
let captured = self.args.clone();
let args = args.to_vec();
Box::pin(async move {
method.store(4, Ordering::SeqCst);
*captured.lock().expect("lock") = args;
Ok(())
})
}
fn doctor(
&self,
args: &[String],
) -> std::pin::Pin<Box<dyn std::future::Future<Output = arcature::Result<()>> + Send + '_>>
{
let method = self.method.clone();
let captured = self.args.clone();
let args = args.to_vec();
Box::pin(async move {
method.store(5, Ordering::SeqCst);
*captured.lock().expect("lock") = args;
Ok(())
})
}
fn about(
&self,
args: &[String],
) -> std::pin::Pin<Box<dyn std::future::Future<Output = arcature::Result<()>> + Send + '_>>
{
let method = self.method.clone();
let captured = self.args.clone();
let args = args.to_vec();
Box::pin(async move {
method.store(6, Ordering::SeqCst);
*captured.lock().expect("lock") = args;
Ok(())
})
}
}
#[tokio::test]
async fn dispatch_routes_each_subcommand_to_the_right_method() {
for (sub, marker) in [
(Subcommand::Serve, 1usize),
(Subcommand::Migrate, 2),
(Subcommand::Queue, 3),
(Subcommand::Schedule, 4),
(Subcommand::Doctor, 5),
(Subcommand::About, 6),
] {
let ops = RecordingOps {
method: Arc::new(AtomicUsize::new(0)),
args: Arc::new(Mutex::new(Vec::new())),
};
dispatch(&ops, sub.clone(), &[]).await.expect("dispatch ok");
assert_eq!(
ops.method.load(Ordering::SeqCst),
marker,
"dispatch routed {sub:?} to the wrong operation"
);
}
}
#[tokio::test]
async fn dispatch_forwards_trailing_args() {
let ops = RecordingOps {
method: Arc::new(AtomicUsize::new(0)),
args: Arc::new(Mutex::new(Vec::new())),
};
let args = vec!["up".to_owned(), "--steps".to_owned(), "3".to_owned()];
dispatch(&ops, Subcommand::Migrate, &args)
.await
.expect("migrate ok");
assert_eq!(
ops.args.lock().expect("lock").clone(),
args,
"trailing args forwarded"
);
}
#[tokio::test]
async fn dispatch_surfaces_operation_errors_without_panicking() {
struct FailingOps;
impl Operations for FailingOps {
fn serve(
&self,
_args: &[String],
) -> std::pin::Pin<Box<dyn std::future::Future<Output = arcature::Result<()>> + Send + '_>>
{
Box::pin(async { Err(arcature::EngineError::InvalidPort("nope".to_owned())) })
}
fn migrate(
&self,
_args: &[String],
) -> std::pin::Pin<Box<dyn std::future::Future<Output = arcature::Result<()>> + Send + '_>>
{
Box::pin(async { Ok(()) })
}
fn queue(
&self,
_args: &[String],
) -> std::pin::Pin<Box<dyn std::future::Future<Output = arcature::Result<()>> + Send + '_>>
{
Box::pin(async { Ok(()) })
}
fn schedule(
&self,
_args: &[String],
) -> std::pin::Pin<Box<dyn std::future::Future<Output = arcature::Result<()>> + Send + '_>>
{
Box::pin(async { Ok(()) })
}
fn doctor(
&self,
_args: &[String],
) -> std::pin::Pin<Box<dyn std::future::Future<Output = arcature::Result<()>> + Send + '_>>
{
Box::pin(async { Ok(()) })
}
fn about(
&self,
_args: &[String],
) -> std::pin::Pin<Box<dyn std::future::Future<Output = arcature::Result<()>> + Send + '_>>
{
Box::pin(async { Ok(()) })
}
}
let result = dispatch(&FailingOps, Subcommand::Serve, &[]).await;
assert!(
matches!(result, Err(arcature::EngineError::InvalidPort(_))),
"operation error surfaced, got: {result:?}"
);
}
#[test]
fn parse_routes_each_known_subcommand() {
assert_eq!(parse(["serve"]), Ok(Subcommand::Serve));
assert_eq!(parse(["migrate"]), Ok(Subcommand::Migrate));
assert_eq!(parse(["queue"]), Ok(Subcommand::Queue));
assert_eq!(parse(["schedule"]), Ok(Subcommand::Schedule));
assert_eq!(parse(["doctor"]), Ok(Subcommand::Doctor));
assert_eq!(parse(["about"]), Ok(Subcommand::About));
}
#[test]
fn parse_missing_is_typed_error() {
let result: Result<Subcommand, SubcommandError> = parse(std::iter::empty::<String>());
assert_eq!(result, Err(SubcommandError::Missing));
}
#[test]
fn parse_unknown_is_typed_error_with_name() {
assert_eq!(
parse(["deploy"]),
Err(SubcommandError::Unknown {
name: "deploy".to_owned()
})
);
assert_eq!(
parse(["--help"]),
Err(SubcommandError::Unknown {
name: "--help".to_owned()
})
);
}
#[test]
fn parse_consumes_only_first_positional() {
assert_eq!(
parse(["migrate", "up", "--steps", "3"]),
Ok(Subcommand::Migrate)
);
}