use std::any::Any;
use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::Arc;
use crate::error::ClickError;
use crate::source::ParameterSource;
thread_local! {
static CONTEXT_STACK: RefCell<Vec<Arc<Context>>> = const { RefCell::new(Vec::new()) };
}
pub fn push_context(ctx: Arc<Context>) {
CONTEXT_STACK.with(|stack| {
stack.borrow_mut().push(ctx);
});
}
pub fn pop_context() -> Option<Arc<Context>> {
CONTEXT_STACK.with(|stack| stack.borrow_mut().pop())
}
pub fn get_current_context() -> Option<Arc<Context>> {
CONTEXT_STACK.with(|stack| stack.borrow().last().cloned())
}
pub type BoxedValue = Arc<dyn Any + Send + Sync>;
pub struct Context {
parent: Option<Arc<Context>>,
info_name: Option<String>,
params: HashMap<String, BoxedValue>,
args: Vec<String>,
obj: Option<BoxedValue>,
meta: HashMap<String, BoxedValue>,
default_map: Option<HashMap<String, BoxedValue>>,
invoked_subcommand: Option<String>,
terminal_width: Option<usize>,
max_content_width: Option<usize>,
allow_extra_args: bool,
allow_interspersed_args: bool,
ignore_unknown_options: bool,
help_option_names: Vec<String>,
resilient_parsing: bool,
auto_envvar_prefix: Option<String>,
color: Option<bool>,
show_default: Option<bool>,
parameter_source: HashMap<String, ParameterSource>,
close_callbacks: RefCell<Vec<Box<dyn FnOnce() + Send>>>,
}
impl std::fmt::Debug for Context {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Context")
.field(
"parent",
&self.parent.as_ref().map(|p| p.info_name.as_deref()),
)
.field("info_name", &self.info_name)
.field("params", &format!("<{} params>", self.params.len()))
.field("args", &self.args)
.field("obj", &self.obj.as_ref().map(|_| "<obj>"))
.field("meta", &format!("<{} entries>", self.meta.len()))
.field(
"default_map",
&self
.default_map
.as_ref()
.map(|m| format!("<{} entries>", m.len())),
)
.field("invoked_subcommand", &self.invoked_subcommand)
.field("terminal_width", &self.terminal_width)
.field("max_content_width", &self.max_content_width)
.field("allow_extra_args", &self.allow_extra_args)
.field("allow_interspersed_args", &self.allow_interspersed_args)
.field("ignore_unknown_options", &self.ignore_unknown_options)
.field("help_option_names", &self.help_option_names)
.field("resilient_parsing", &self.resilient_parsing)
.field("auto_envvar_prefix", &self.auto_envvar_prefix)
.field("color", &self.color)
.field("show_default", &self.show_default)
.field("parameter_source", &self.parameter_source)
.field(
"close_callbacks",
&format!("<{} callbacks>", self.close_callbacks.borrow().len()),
)
.finish()
}
}
impl Default for Context {
fn default() -> Self {
Self {
parent: None,
info_name: None,
params: HashMap::new(),
args: Vec::new(),
obj: None,
meta: HashMap::new(),
default_map: None,
invoked_subcommand: None,
terminal_width: None,
max_content_width: None,
allow_extra_args: false,
allow_interspersed_args: true,
ignore_unknown_options: false,
help_option_names: vec!["--help".to_string()],
resilient_parsing: false,
auto_envvar_prefix: None,
color: None,
show_default: None,
parameter_source: HashMap::new(),
close_callbacks: RefCell::new(Vec::new()),
}
}
}
impl Context {
#[inline]
pub fn new() -> Self {
Self::default()
}
#[inline]
pub fn parent(&self) -> Option<&Arc<Context>> {
self.parent.as_ref()
}
#[inline]
pub fn info_name(&self) -> Option<&str> {
self.info_name.as_deref()
}
#[inline]
pub fn params(&self) -> &HashMap<String, BoxedValue> {
&self.params
}
#[inline]
pub fn params_mut(&mut self) -> &mut HashMap<String, BoxedValue> {
&mut self.params
}
pub fn get_param<T: 'static>(&self, name: &str) -> Option<&T> {
self.params.get(name).and_then(|v| v.downcast_ref::<T>())
}
#[inline]
pub fn args(&self) -> &[String] {
&self.args
}
#[inline]
pub fn args_mut(&mut self) -> &mut Vec<String> {
&mut self.args
}
pub fn obj<T: 'static>(&self) -> Option<&T> {
self.obj.as_ref().and_then(|v| v.downcast_ref::<T>())
}
pub fn find_obj<T: 'static>(&self) -> Option<&T> {
let mut current: Option<&Context> = Some(self);
while let Some(ctx) = current {
if let Some(obj) = ctx.obj::<T>() {
return Some(obj);
}
current = ctx.parent.as_ref().map(|p| p.as_ref());
}
None
}
pub fn set_obj<T: Any + Send + Sync + 'static>(&mut self, obj: T) {
self.obj = Some(Arc::new(obj));
}
#[inline]
pub fn meta(&self) -> &HashMap<String, BoxedValue> {
&self.meta
}
#[inline]
pub fn meta_mut(&mut self) -> &mut HashMap<String, BoxedValue> {
&mut self.meta
}
pub fn get_meta<T: 'static>(&self, key: &str) -> Option<&T> {
self.meta.get(key).and_then(|v| v.downcast_ref::<T>())
}
#[inline]
pub fn invoked_subcommand(&self) -> Option<&str> {
self.invoked_subcommand.as_deref()
}
#[inline]
pub fn set_invoked_subcommand(&mut self, name: Option<String>) {
self.invoked_subcommand = name;
}
#[inline]
pub fn terminal_width(&self) -> Option<usize> {
self.terminal_width
}
#[inline]
pub fn max_content_width(&self) -> Option<usize> {
self.max_content_width
}
#[inline]
pub fn allow_extra_args(&self) -> bool {
self.allow_extra_args
}
#[inline]
pub fn allow_interspersed_args(&self) -> bool {
self.allow_interspersed_args
}
#[inline]
pub fn ignore_unknown_options(&self) -> bool {
self.ignore_unknown_options
}
#[inline]
pub fn help_option_names(&self) -> &[String] {
&self.help_option_names
}
#[inline]
pub fn resilient_parsing(&self) -> bool {
self.resilient_parsing
}
#[inline]
pub fn auto_envvar_prefix(&self) -> Option<&str> {
self.auto_envvar_prefix.as_deref()
}
#[inline]
pub fn color(&self) -> Option<bool> {
self.color
}
#[inline]
pub fn show_default(&self) -> Option<bool> {
self.show_default
}
pub fn command_path(&self) -> String {
let mut parts = Vec::new();
let mut current: Option<&Context> = Some(self);
while let Some(ctx) = current {
if let Some(name) = &ctx.info_name {
parts.push(name.as_str());
}
current = ctx.parent.as_ref().map(|p| p.as_ref());
}
parts.reverse();
parts.join(" ")
}
pub fn find_root(&self) -> &Context {
let mut current: &Context = self;
while let Some(parent) = ¤t.parent {
current = parent.as_ref();
}
current
}
#[inline]
pub fn get_parameter_source(&self, name: &str) -> Option<ParameterSource> {
self.parameter_source.get(name).copied()
}
#[inline]
pub fn set_parameter_source(&mut self, name: &str, source: ParameterSource) {
self.parameter_source.insert(name.to_string(), source);
}
pub fn fail(&self, message: &str) -> ClickError {
ClickError::usage(message)
}
#[inline]
pub fn abort(&self) -> ClickError {
ClickError::abort()
}
#[inline]
pub fn exit(&self, code: i32) -> ClickError {
ClickError::exit(code)
}
pub fn call_on_close(&self, f: impl FnOnce() + Send + 'static) {
self.close_callbacks.borrow_mut().push(Box::new(f));
}
pub fn close(&self) {
let callbacks: Vec<_> = self.close_callbacks.borrow_mut().drain(..).collect();
for callback in callbacks.into_iter().rev() {
callback();
}
}
pub fn lookup_default(&self, name: &str) -> Option<&dyn Any> {
self.default_map
.as_ref()
.and_then(|map| map.get(name))
.map(|v| v.as_ref() as &dyn Any)
}
pub fn lookup_default_value(&self, name: &str) -> Option<BoxedValue> {
self.default_map
.as_ref()
.and_then(|map| map.get(name))
.cloned()
}
pub fn invoke(
self: &Arc<Self>,
cmd: &dyn crate::group::CommandLike,
args: &[String],
) -> Result<(), ClickError> {
let cmd_name = cmd.name().unwrap_or("invoked");
let child_ctx = cmd.make_context(cmd_name, args.to_vec(), Some(Arc::clone(self)))?;
let child_ctx = Arc::new(child_ctx);
push_context(Arc::clone(&child_ctx));
let result = cmd.invoke(&child_ctx);
pop_context();
child_ctx.close();
result
}
pub fn forward(
self: &Arc<Self>,
cmd: &dyn crate::group::CommandLike,
) -> Result<(), ClickError> {
let cmd_name = cmd.name().unwrap_or("forwarded");
let child_builder = ContextBuilder::new()
.info_name(cmd_name)
.parent(Arc::clone(self));
let mut child_ctx = child_builder.build();
for (key, value) in self.params.iter() {
child_ctx.params.insert(key.clone(), Arc::clone(value));
}
for (key, source) in self.parameter_source.iter() {
child_ctx.parameter_source.insert(key.clone(), *source);
}
let child_ctx = Arc::new(child_ctx);
push_context(Arc::clone(&child_ctx));
let result = cmd.invoke(&child_ctx);
pop_context();
child_ctx.close();
result
}
pub fn with_resource<T, F, C, R>(&self, resource: T, f: F, cleanup: C) -> R
where
T: Send + 'static,
F: FnOnce(&T) -> R,
C: FnOnce() + Send + 'static,
{
self.call_on_close(cleanup);
f(&resource)
}
}
#[derive(Default)]
pub struct ContextBuilder {
parent: Option<Arc<Context>>,
info_name: Option<String>,
obj: Option<BoxedValue>,
auto_envvar_prefix: Option<String>,
default_map: Option<HashMap<String, BoxedValue>>,
terminal_width: Option<usize>,
max_content_width: Option<usize>,
resilient_parsing: bool,
allow_extra_args: Option<bool>,
allow_interspersed_args: Option<bool>,
ignore_unknown_options: Option<bool>,
help_option_names: Option<Vec<String>>,
color: Option<bool>,
show_default: Option<bool>,
}
impl ContextBuilder {
#[inline]
pub fn new() -> Self {
Self::default()
}
pub fn parent(mut self, parent: Arc<Context>) -> Self {
self.parent = Some(parent);
self
}
pub fn info_name(mut self, name: impl Into<String>) -> Self {
self.info_name = Some(name.into());
self
}
pub fn obj<T: Any + Send + Sync + 'static>(mut self, obj: T) -> Self {
self.obj = Some(Arc::new(obj));
self
}
pub fn auto_envvar_prefix(mut self, prefix: impl Into<String>) -> Self {
self.auto_envvar_prefix = Some(prefix.into());
self
}
pub fn default_map(mut self, map: HashMap<String, BoxedValue>) -> Self {
self.default_map = Some(map);
self
}
pub fn terminal_width(mut self, width: usize) -> Self {
self.terminal_width = Some(width);
self
}
pub fn max_content_width(mut self, width: usize) -> Self {
self.max_content_width = Some(width);
self
}
pub fn resilient_parsing(mut self, enabled: bool) -> Self {
self.resilient_parsing = enabled;
self
}
pub fn allow_extra_args(mut self, allow: bool) -> Self {
self.allow_extra_args = Some(allow);
self
}
pub fn allow_interspersed_args(mut self, allow: bool) -> Self {
self.allow_interspersed_args = Some(allow);
self
}
pub fn ignore_unknown_options(mut self, ignore: bool) -> Self {
self.ignore_unknown_options = Some(ignore);
self
}
pub fn help_option_names(mut self, names: Vec<String>) -> Self {
self.help_option_names = Some(names);
self
}
pub fn color(mut self, color: bool) -> Self {
self.color = Some(color);
self
}
pub fn show_default(mut self, show: bool) -> Self {
self.show_default = Some(show);
self
}
pub fn build(self) -> Context {
let (terminal_width, max_content_width, color, show_default, help_option_names, obj, meta) =
if let Some(ref parent) = self.parent {
(
self.terminal_width.or(parent.terminal_width),
self.max_content_width.or(parent.max_content_width),
self.color.or(parent.color),
self.show_default.or(parent.show_default),
self.help_option_names
.unwrap_or_else(|| parent.help_option_names.clone()),
self.obj.or_else(|| parent.obj.clone()),
parent.meta.clone(),
)
} else {
(
self.terminal_width,
self.max_content_width,
self.color,
self.show_default,
self.help_option_names
.unwrap_or_else(|| vec!["--help".to_string()]),
self.obj,
HashMap::new(),
)
};
let auto_envvar_prefix = if let Some(prefix) = self.auto_envvar_prefix {
Some(prefix.to_uppercase().replace('-', "_"))
} else if let (Some(ref parent), Some(ref info_name)) = (&self.parent, &self.info_name) {
parent.auto_envvar_prefix.as_ref().map(|parent_prefix| {
format!(
"{}_{}",
parent_prefix,
info_name.to_uppercase().replace('-', "_")
)
})
} else {
None
};
let parent_default_map = self.parent.as_ref().and_then(|parent| parent.default_map.clone());
let default_map = match (parent_default_map, self.default_map) {
(Some(mut inherited), Some(child)) => {
for (key, value) in child {
inherited.insert(key, value);
}
Some(inherited)
}
(Some(inherited), None) => Some(inherited),
(None, Some(child)) => Some(child),
(None, None) => None,
};
Context {
parent: self.parent,
info_name: self.info_name,
params: HashMap::new(),
args: Vec::new(),
obj,
meta,
default_map,
invoked_subcommand: None,
terminal_width,
max_content_width,
allow_extra_args: self.allow_extra_args.unwrap_or(false),
allow_interspersed_args: self.allow_interspersed_args.unwrap_or(true),
ignore_unknown_options: self.ignore_unknown_options.unwrap_or(false),
help_option_names,
resilient_parsing: self.resilient_parsing,
auto_envvar_prefix,
color,
show_default,
parameter_source: HashMap::new(),
close_callbacks: RefCell::new(Vec::new()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
#[test]
fn test_context_default_values() {
let ctx = Context::new();
assert!(ctx.parent().is_none());
assert!(ctx.info_name().is_none());
assert!(ctx.params().is_empty());
assert!(ctx.args().is_empty());
assert!(!ctx.allow_extra_args());
assert!(ctx.allow_interspersed_args());
assert!(!ctx.ignore_unknown_options());
assert_eq!(ctx.help_option_names(), &["--help".to_string()]);
assert!(!ctx.resilient_parsing());
assert!(ctx.auto_envvar_prefix().is_none());
assert!(ctx.color().is_none());
assert!(ctx.show_default().is_none());
}
#[test]
fn test_context_builder() {
let ctx = ContextBuilder::new()
.info_name("myapp")
.allow_extra_args(true)
.allow_interspersed_args(false)
.ignore_unknown_options(true)
.terminal_width(120)
.max_content_width(100)
.resilient_parsing(true)
.auto_envvar_prefix("MYAPP")
.color(true)
.show_default(false)
.help_option_names(vec!["--help".to_string(), "-h".to_string()])
.build();
assert_eq!(ctx.info_name(), Some("myapp"));
assert!(ctx.allow_extra_args());
assert!(!ctx.allow_interspersed_args());
assert!(ctx.ignore_unknown_options());
assert_eq!(ctx.terminal_width(), Some(120));
assert_eq!(ctx.max_content_width(), Some(100));
assert!(ctx.resilient_parsing());
assert_eq!(ctx.auto_envvar_prefix(), Some("MYAPP"));
assert_eq!(ctx.color(), Some(true));
assert_eq!(ctx.show_default(), Some(false));
assert_eq!(
ctx.help_option_names(),
&["--help".to_string(), "-h".to_string()]
);
}
#[test]
fn test_parent_child_context_chain() {
let parent = Arc::new(
ContextBuilder::new()
.info_name("cli")
.terminal_width(100)
.color(true)
.build(),
);
let child = ContextBuilder::new()
.info_name("subcommand")
.parent(Arc::clone(&parent))
.build();
assert_eq!(child.terminal_width(), Some(100));
assert_eq!(child.color(), Some(true));
assert!(child.parent().is_some());
assert_eq!(child.parent().unwrap().info_name(), Some("cli"));
}
#[test]
fn test_command_path() {
let root = Arc::new(ContextBuilder::new().info_name("cli").build());
let sub = Arc::new(
ContextBuilder::new()
.info_name("group")
.parent(Arc::clone(&root))
.build(),
);
let leaf = ContextBuilder::new()
.info_name("command")
.parent(sub)
.build();
assert_eq!(root.command_path(), "cli");
assert_eq!(leaf.command_path(), "cli group command");
}
#[test]
fn test_find_root() {
let root = Arc::new(ContextBuilder::new().info_name("cli").build());
let child = Arc::new(
ContextBuilder::new()
.info_name("sub")
.parent(Arc::clone(&root))
.build(),
);
let grandchild = ContextBuilder::new()
.info_name("leaf")
.parent(child)
.build();
assert_eq!(grandchild.find_root().info_name(), Some("cli"));
}
#[test]
fn test_thread_local_stack() {
assert!(get_current_context().is_none());
let ctx1 = Arc::new(ContextBuilder::new().info_name("ctx1").build());
let ctx2 = Arc::new(ContextBuilder::new().info_name("ctx2").build());
push_context(Arc::clone(&ctx1));
assert_eq!(get_current_context().unwrap().info_name(), Some("ctx1"));
push_context(Arc::clone(&ctx2));
assert_eq!(get_current_context().unwrap().info_name(), Some("ctx2"));
let popped = pop_context();
assert_eq!(popped.unwrap().info_name(), Some("ctx2"));
assert_eq!(get_current_context().unwrap().info_name(), Some("ctx1"));
pop_context();
assert!(get_current_context().is_none());
}
#[test]
fn test_parameter_source_tracking() {
let mut ctx = Context::new();
assert!(ctx.get_parameter_source("name").is_none());
ctx.set_parameter_source("name", ParameterSource::CommandLine);
assert_eq!(
ctx.get_parameter_source("name"),
Some(ParameterSource::CommandLine)
);
ctx.set_parameter_source("name", ParameterSource::Environment);
assert_eq!(
ctx.get_parameter_source("name"),
Some(ParameterSource::Environment)
);
}
#[test]
fn test_close_callbacks() {
let counter = Arc::new(AtomicUsize::new(0));
let c1 = Arc::clone(&counter);
let c2 = Arc::clone(&counter);
let c3 = Arc::clone(&counter);
let ctx = Context::new();
ctx.call_on_close(move || {
c1.fetch_add(1, Ordering::SeqCst);
});
ctx.call_on_close(move || {
c2.fetch_add(10, Ordering::SeqCst);
});
ctx.call_on_close(move || {
c3.fetch_add(100, Ordering::SeqCst);
});
assert_eq!(counter.load(Ordering::SeqCst), 0);
ctx.close();
assert_eq!(counter.load(Ordering::SeqCst), 111);
ctx.close();
assert_eq!(counter.load(Ordering::SeqCst), 111);
}
#[test]
fn test_error_creation() {
let ctx = ContextBuilder::new().info_name("myapp").build();
let usage_err = ctx.fail("something went wrong");
assert!(matches!(usage_err, ClickError::UsageError { .. }));
assert_eq!(usage_err.exit_code(), 2);
let abort_err = ctx.abort();
assert!(matches!(abort_err, ClickError::Abort));
assert_eq!(abort_err.exit_code(), 1);
let exit_err = ctx.exit(42);
assert!(matches!(exit_err, ClickError::Exit { code: 42 }));
assert_eq!(exit_err.exit_code(), 42);
}
#[test]
fn test_params_access() {
let mut ctx = Context::new();
ctx.params_mut()
.insert("count".to_string(), Arc::new(42i32));
ctx.params_mut()
.insert("name".to_string(), Arc::new("Alice".to_string()));
assert_eq!(ctx.get_param::<i32>("count"), Some(&42));
assert_eq!(ctx.get_param::<String>("name"), Some(&"Alice".to_string()));
assert!(ctx.get_param::<i32>("name").is_none()); assert!(ctx.get_param::<i32>("missing").is_none()); }
#[test]
fn test_obj_access() {
#[derive(Debug, PartialEq)]
struct AppState {
value: i32,
}
let ctx = ContextBuilder::new().obj(AppState { value: 123 }).build();
let state = ctx.obj::<AppState>().unwrap();
assert_eq!(state.value, 123);
assert!(ctx.obj::<String>().is_none());
}
#[test]
fn test_lookup_default() {
let mut defaults: HashMap<String, BoxedValue> = HashMap::new();
defaults.insert("count".to_string(), Arc::new(42i32));
defaults.insert("name".to_string(), Arc::new("default".to_string()));
let ctx = ContextBuilder::new().default_map(defaults).build();
let count_default = ctx.lookup_default("count").unwrap();
assert_eq!(count_default.downcast_ref::<i32>(), Some(&42));
let name_default = ctx.lookup_default("name").unwrap();
assert_eq!(
name_default.downcast_ref::<String>(),
Some(&"default".to_string())
);
assert!(ctx.lookup_default("missing").is_none());
}
#[test]
fn test_default_map_inheritance() {
let mut parent_defaults: HashMap<String, BoxedValue> = HashMap::new();
parent_defaults.insert("count".to_string(), Arc::new(5i32));
let parent = Arc::new(ContextBuilder::new().default_map(parent_defaults).build());
let mut child_defaults: HashMap<String, BoxedValue> = HashMap::new();
child_defaults.insert("count".to_string(), Arc::new(10i32));
child_defaults.insert("name".to_string(), Arc::new("Bob".to_string()));
let child = ContextBuilder::new()
.parent(Arc::clone(&parent))
.default_map(child_defaults)
.build();
let count_default = child.lookup_default("count").unwrap();
assert_eq!(count_default.downcast_ref::<i32>(), Some(&10));
let name_default = child.lookup_default("name").unwrap();
assert_eq!(
name_default.downcast_ref::<String>(),
Some(&"Bob".to_string())
);
}
#[test]
fn test_auto_envvar_prefix_normalization() {
let ctx = ContextBuilder::new().auto_envvar_prefix("my-app").build();
assert_eq!(ctx.auto_envvar_prefix(), Some("MY_APP"));
let parent = Arc::new(ContextBuilder::new().auto_envvar_prefix("MY_APP").build());
let child = ContextBuilder::new()
.info_name("sub-cmd")
.parent(parent)
.build();
assert_eq!(child.auto_envvar_prefix(), Some("MY_APP_SUB_CMD"));
}
#[test]
fn test_args_access() {
let mut ctx = Context::new();
assert!(ctx.args().is_empty());
ctx.args_mut().push("extra1".to_string());
ctx.args_mut().push("extra2".to_string());
assert_eq!(ctx.args(), &["extra1", "extra2"]);
}
#[test]
fn test_meta_access() {
let mut ctx = Context::new();
ctx.meta_mut()
.insert("mymodule.key".to_string(), Arc::new(42i32));
assert_eq!(ctx.get_meta::<i32>("mymodule.key"), Some(&42));
assert!(ctx.get_meta::<String>("mymodule.key").is_none()); assert!(ctx.get_meta::<i32>("other.key").is_none()); }
#[test]
fn test_invoked_subcommand() {
let mut ctx = Context::new();
assert!(ctx.invoked_subcommand().is_none());
ctx.set_invoked_subcommand(Some("subcommand".to_string()));
assert_eq!(ctx.invoked_subcommand(), Some("subcommand"));
ctx.set_invoked_subcommand(Some("*".to_string()));
assert_eq!(ctx.invoked_subcommand(), Some("*"));
ctx.set_invoked_subcommand(None);
assert!(ctx.invoked_subcommand().is_none());
}
#[test]
fn test_context_invoke_command() {
use crate::command::Command;
use std::sync::atomic::{AtomicBool, Ordering};
let invoked = Arc::new(AtomicBool::new(false));
let invoked_clone = Arc::clone(&invoked);
let other_cmd = Command::new("other")
.callback(move |_ctx| {
invoked_clone.store(true, Ordering::SeqCst);
Ok(())
})
.build();
let ctx = Arc::new(ContextBuilder::new().info_name("main").build());
let result = ctx.invoke(&other_cmd, &[]);
assert!(result.is_ok());
assert!(invoked.load(Ordering::SeqCst));
}
#[test]
fn test_context_invoke_with_args() {
use crate::command::Command;
use crate::argument::Argument;
use std::sync::Mutex;
let captured_name = Arc::new(Mutex::new(String::new()));
let captured_clone = Arc::clone(&captured_name);
let other_cmd = Command::new("greet")
.argument(Argument::new("name").build())
.callback(move |ctx| {
if let Some(name) = ctx.get_param::<String>("name") {
let mut lock = captured_clone.lock().unwrap();
*lock = name.clone();
}
Ok(())
})
.build();
let ctx = Arc::new(ContextBuilder::new().info_name("main").build());
let result = ctx.invoke(&other_cmd, &["Alice".to_string()]);
assert!(result.is_ok());
let name = captured_name.lock().unwrap();
assert_eq!(*name, "Alice");
}
#[test]
fn test_context_invoke_creates_child_context() {
use crate::command::Command;
use std::sync::Mutex;
let parent_name = Arc::new(Mutex::new(None::<String>));
let parent_clone = Arc::clone(&parent_name);
let other_cmd = Command::new("child")
.callback(move |ctx| {
if let Some(parent) = ctx.parent() {
let mut lock = parent_clone.lock().unwrap();
*lock = parent.info_name().map(|s| s.to_string());
}
Ok(())
})
.build();
let ctx = Arc::new(ContextBuilder::new().info_name("main").build());
let result = ctx.invoke(&other_cmd, &[]);
assert!(result.is_ok());
let captured = parent_name.lock().unwrap();
assert_eq!(*captured, Some("main".to_string()));
}
#[test]
fn test_context_forward_copies_params() {
use crate::command::Command;
use std::sync::Mutex;
let forwarded_name = Arc::new(Mutex::new(None::<String>));
let forwarded_clone = Arc::clone(&forwarded_name);
let other_cmd = Command::new("receiver")
.callback(move |ctx| {
let mut lock = forwarded_clone.lock().unwrap();
*lock = ctx.get_param::<String>("name").cloned();
Ok(())
})
.build();
let mut ctx = ContextBuilder::new().info_name("sender").build();
ctx.params_mut().insert("name".to_string(), Arc::new("Forwarded".to_string()));
let ctx = Arc::new(ctx);
let result = ctx.forward(&other_cmd);
assert!(result.is_ok());
let captured = forwarded_name.lock().unwrap();
assert_eq!(*captured, Some("Forwarded".to_string()));
}
#[test]
fn test_context_forward_copies_multiple_params() {
use crate::command::Command;
use std::sync::Mutex;
let forwarded_params = Arc::new(Mutex::new((None::<String>, None::<i32>)));
let params_clone = Arc::clone(&forwarded_params);
let other_cmd = Command::new("receiver")
.callback(move |ctx| {
let mut lock = params_clone.lock().unwrap();
lock.0 = ctx.get_param::<String>("name").cloned();
lock.1 = ctx.get_param::<i32>("count").copied();
Ok(())
})
.build();
let mut ctx = ContextBuilder::new().info_name("sender").build();
ctx.params_mut().insert("name".to_string(), Arc::new("Test".to_string()));
ctx.params_mut().insert("count".to_string(), Arc::new(42i32));
let ctx = Arc::new(ctx);
let result = ctx.forward(&other_cmd);
assert!(result.is_ok());
let captured = forwarded_params.lock().unwrap();
assert_eq!(captured.0, Some("Test".to_string()));
assert_eq!(captured.1, Some(42));
}
#[test]
fn test_context_forward_copies_parameter_sources() {
use crate::command::Command;
use std::sync::Mutex;
let forwarded_source = Arc::new(Mutex::new(None::<ParameterSource>));
let source_clone = Arc::clone(&forwarded_source);
let other_cmd = Command::new("receiver")
.callback(move |ctx| {
let mut lock = source_clone.lock().unwrap();
*lock = ctx.get_parameter_source("name");
Ok(())
})
.build();
let mut ctx = ContextBuilder::new().info_name("sender").build();
ctx.params_mut().insert("name".to_string(), Arc::new("Test".to_string()));
ctx.set_parameter_source("name", ParameterSource::CommandLine);
let ctx = Arc::new(ctx);
let result = ctx.forward(&other_cmd);
assert!(result.is_ok());
let captured = forwarded_source.lock().unwrap();
assert_eq!(*captured, Some(ParameterSource::CommandLine));
}
#[test]
fn test_context_forward_creates_child_context() {
use crate::command::Command;
use std::sync::Mutex;
let parent_name = Arc::new(Mutex::new(None::<String>));
let parent_clone = Arc::clone(&parent_name);
let other_cmd = Command::new("receiver")
.callback(move |ctx| {
if let Some(parent) = ctx.parent() {
let mut lock = parent_clone.lock().unwrap();
*lock = parent.info_name().map(|s| s.to_string());
}
Ok(())
})
.build();
let ctx = Arc::new(ContextBuilder::new().info_name("sender").build());
let result = ctx.forward(&other_cmd);
assert!(result.is_ok());
let captured = parent_name.lock().unwrap();
assert_eq!(*captured, Some("sender".to_string()));
}
#[test]
fn test_with_resource_basic() {
use std::sync::atomic::{AtomicBool, Ordering};
struct Resource {
value: i32,
}
let cleaned_up = Arc::new(AtomicBool::new(false));
let cleaned_up_clone = Arc::clone(&cleaned_up);
let ctx = ContextBuilder::new().build();
let result = ctx.with_resource(
Resource { value: 42 },
|res| res.value * 2,
move || {
cleaned_up_clone.store(true, Ordering::SeqCst);
},
);
assert_eq!(result, 84);
assert!(!cleaned_up.load(Ordering::SeqCst));
ctx.close();
assert!(cleaned_up.load(Ordering::SeqCst)); }
#[test]
fn test_with_resource_multiple() {
use std::sync::atomic::{AtomicUsize, Ordering};
let cleanup_count = Arc::new(AtomicUsize::new(0));
let count1 = Arc::clone(&cleanup_count);
let count2 = Arc::clone(&cleanup_count);
let ctx = ContextBuilder::new().build();
let result1 = ctx.with_resource(
10,
|res| *res + 5,
move || {
count1.fetch_add(1, Ordering::SeqCst);
},
);
let result2 = ctx.with_resource(
20,
|res| *res * 2,
move || {
count2.fetch_add(1, Ordering::SeqCst);
},
);
assert_eq!(result1, 15);
assert_eq!(result2, 40);
assert_eq!(cleanup_count.load(Ordering::SeqCst), 0);
ctx.close();
assert_eq!(cleanup_count.load(Ordering::SeqCst), 2);
}
#[test]
fn test_with_resource_string_resource() {
use std::sync::atomic::{AtomicBool, Ordering};
let cleaned_up = Arc::new(AtomicBool::new(false));
let cleaned_up_clone = Arc::clone(&cleaned_up);
let ctx = ContextBuilder::new().build();
let result = ctx.with_resource(
String::from("hello"),
|s| s.len(),
move || {
cleaned_up_clone.store(true, Ordering::SeqCst);
},
);
assert_eq!(result, 5);
ctx.close();
assert!(cleaned_up.load(Ordering::SeqCst));
}
#[test]
fn test_invoke_error_propagation() {
use crate::command::Command;
use crate::error::ClickError;
let other_cmd = Command::new("failing")
.callback(|_ctx| {
Err(ClickError::usage("intentional failure"))
})
.build();
let ctx = Arc::new(ContextBuilder::new().info_name("main").build());
let result = ctx.invoke(&other_cmd, &[]);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, ClickError::UsageError { .. }));
}
#[test]
fn test_forward_error_propagation() {
use crate::command::Command;
use crate::error::ClickError;
let other_cmd = Command::new("failing")
.callback(|_ctx| {
Err(ClickError::usage("intentional failure"))
})
.build();
let ctx = Arc::new(ContextBuilder::new().info_name("main").build());
let result = ctx.forward(&other_cmd);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, ClickError::UsageError { .. }));
}
#[test]
fn test_invoke_runs_child_close_callbacks() {
use crate::command::Command;
use std::sync::atomic::{AtomicBool, Ordering};
let child_closed = Arc::new(AtomicBool::new(false));
let closed_clone = Arc::clone(&child_closed);
let other_cmd = Command::new("child")
.callback(move |ctx| {
let closed_clone = Arc::clone(&closed_clone);
ctx.call_on_close(move || {
closed_clone.store(true, Ordering::SeqCst);
});
Ok(())
})
.build();
let ctx = Arc::new(ContextBuilder::new().info_name("main").build());
let result = ctx.invoke(&other_cmd, &[]);
assert!(result.is_ok());
assert!(child_closed.load(Ordering::SeqCst));
}
#[test]
fn test_forward_runs_child_close_callbacks() {
use crate::command::Command;
use std::sync::atomic::{AtomicBool, Ordering};
let child_closed = Arc::new(AtomicBool::new(false));
let closed_clone = Arc::clone(&child_closed);
let other_cmd = Command::new("child")
.callback(move |ctx| {
let closed_clone = Arc::clone(&closed_clone);
ctx.call_on_close(move || {
closed_clone.store(true, Ordering::SeqCst);
});
Ok(())
})
.build();
let ctx = Arc::new(ContextBuilder::new().info_name("main").build());
let result = ctx.forward(&other_cmd);
assert!(result.is_ok());
assert!(child_closed.load(Ordering::SeqCst));
}
}