aboutsummaryrefslogtreecommitdiff
path: root/mingling_core/src/program
diff options
context:
space:
mode:
Diffstat (limited to 'mingling_core/src/program')
-rw-r--r--mingling_core/src/program/config.rs142
-rw-r--r--mingling_core/src/program/error.rs222
-rw-r--r--mingling_core/src/program/flag.rs53
-rw-r--r--mingling_core/src/program/hook.rs208
-rw-r--r--mingling_core/src/program/setup.rs82
-rw-r--r--mingling_core/src/program/single_instance.rs46
-rw-r--r--mingling_core/src/program/string_vec.rs59
7 files changed, 812 insertions, 0 deletions
diff --git a/mingling_core/src/program/config.rs b/mingling_core/src/program/config.rs
index 42603a5..4e193f2 100644
--- a/mingling_core/src/program/config.rs
+++ b/mingling_core/src/program/config.rs
@@ -206,3 +206,145 @@ impl std::fmt::Display for GeneralRendererSetting {
}
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn program_stdout_setting_default() {
+ let s = ProgramStdoutSetting::default();
+ assert!(s.error_output);
+ assert!(s.render_output);
+ assert!(!s.silence_panic);
+ assert!(!s.verbose);
+ assert!(!s.quiet);
+ assert!(!s.debug);
+ assert!(s.color);
+ assert!(s.progress);
+ }
+
+ #[test]
+ fn program_user_context_default() {
+ let ctx = ProgramUserContext::default();
+ assert!(!ctx.help);
+ assert!(ctx.run_hook);
+ assert!(!ctx.confirm);
+ assert!(!ctx.dry_run);
+ assert!(!ctx.force);
+ assert!(!ctx.interactive);
+ assert!(!ctx.assume_yes);
+ }
+
+ #[cfg(feature = "general_renderer")]
+ mod general_renderer_tests {
+ use super::*;
+
+ #[test]
+ fn from_str_disable() {
+ let val: GeneralRendererSetting = "disable".parse().unwrap();
+ assert!(matches!(val, GeneralRendererSetting::Disable));
+ }
+
+ #[cfg(feature = "json_serde_fmt")]
+ #[test]
+ fn from_str_json() {
+ let val: GeneralRendererSetting = "json".parse().unwrap();
+ assert!(matches!(val, GeneralRendererSetting::Json));
+ }
+
+ #[cfg(feature = "json_serde_fmt")]
+ #[test]
+ fn from_str_json_pretty() {
+ let val: GeneralRendererSetting = "json-pretty".parse().unwrap();
+ assert!(matches!(val, GeneralRendererSetting::JsonPretty));
+ }
+
+ #[cfg(feature = "yaml_serde_fmt")]
+ #[test]
+ fn from_str_yaml() {
+ let val: GeneralRendererSetting = "yaml".parse().unwrap();
+ assert!(matches!(val, GeneralRendererSetting::Yaml));
+ }
+
+ #[cfg(feature = "toml_serde_fmt")]
+ #[test]
+ fn from_str_toml() {
+ let val: GeneralRendererSetting = "toml".parse().unwrap();
+ assert!(matches!(val, GeneralRendererSetting::Toml));
+ }
+
+ #[cfg(feature = "ron_serde_fmt")]
+ #[test]
+ fn from_str_ron() {
+ let val: GeneralRendererSetting = "ron".parse().unwrap();
+ assert!(matches!(val, GeneralRendererSetting::Ron));
+ }
+
+ #[cfg(feature = "ron_serde_fmt")]
+ #[test]
+ fn from_str_ron_pretty() {
+ let val: GeneralRendererSetting = "ron-pretty".parse().unwrap();
+ assert!(matches!(val, GeneralRendererSetting::RonPretty));
+ }
+
+ #[test]
+ fn from_str_invalid() {
+ let res: Result<GeneralRendererSetting, String> = "invalid".parse();
+ assert!(res.is_err());
+ }
+
+ #[test]
+ fn from_str_kebab_case() {
+ let val: GeneralRendererSetting = "JsonPretty".parse().unwrap();
+ assert!(matches!(val, GeneralRendererSetting::JsonPretty));
+ }
+
+ #[test]
+ fn from_str_case_insensitive() {
+ let val: GeneralRendererSetting = "JSON".parse().unwrap();
+ assert!(matches!(val, GeneralRendererSetting::Json));
+ }
+
+ #[test]
+ fn from_and_str() {
+ let val = <GeneralRendererSetting as From<&str>>::from("json");
+ assert!(
+ matches!(val, GeneralRendererSetting::Disable)
+ || matches!(val, GeneralRendererSetting::Json)
+ );
+
+ let val = <GeneralRendererSetting as From<&str>>::from("invalid");
+ assert!(matches!(val, GeneralRendererSetting::Disable));
+ }
+
+ #[test]
+ fn from_string() {
+ let val = <GeneralRendererSetting as From<String>>::from("json-pretty".to_string());
+ assert!(
+ matches!(val, GeneralRendererSetting::Disable)
+ || matches!(val, GeneralRendererSetting::JsonPretty)
+ );
+ }
+
+ #[test]
+ fn display_disable() {
+ assert_eq!(GeneralRendererSetting::Disable.to_string(), "disable");
+ }
+
+ #[cfg(feature = "json_serde_fmt")]
+ #[test]
+ fn display_json() {
+ assert_eq!(GeneralRendererSetting::Json.to_string(), "json");
+ }
+
+ #[cfg(feature = "json_serde_fmt")]
+ #[test]
+ fn display_json_pretty() {
+ assert_eq!(
+ GeneralRendererSetting::JsonPretty.to_string(),
+ "json-pretty"
+ );
+ }
+ }
+}
diff --git a/mingling_core/src/program/error.rs b/mingling_core/src/program/error.rs
index 822e429..144b5ab 100644
--- a/mingling_core/src/program/error.rs
+++ b/mingling_core/src/program/error.rs
@@ -30,3 +30,225 @@ impl fmt::Debug for ProgramPanic {
write!(f, "{:?}", self.payload)
}
}
+
+#[cfg(test)]
+mod tests {
+ use crate::error::ChainProcessError;
+ use crate::error::ProgramPanic;
+ use crate::program::exec::error::ProgramExecuteError;
+ use crate::program::exec::error::ProgramInternalExecuteError;
+
+ // ProgramPanic
+
+ #[test]
+ fn test_program_panic_new_with_str() {
+ let panic = ProgramPanic::new(Box::new("hello world"));
+ assert_eq!(format!("{panic}"), "hello world");
+ }
+
+ #[test]
+ fn test_program_panic_new_with_string() {
+ let panic = ProgramPanic::new(Box::new("owned string".to_string()));
+ assert_eq!(format!("{panic}"), "owned string");
+ }
+
+ #[test]
+ fn test_program_panic_new_with_i32() {
+ let panic = ProgramPanic::new(Box::new(42));
+ assert_eq!(format!("{panic}"), "");
+ }
+
+ #[test]
+ fn test_program_panic_debug() {
+ let panic = ProgramPanic::new(Box::new("debug me"));
+ let debug = format!("{panic:?}");
+ assert_eq!(debug, "Any { .. }");
+ }
+
+ // ProgramExecuteError
+
+ #[test]
+ fn test_program_execute_error_display_dispatcher_not_found() {
+ let err = ProgramExecuteError::DispatcherNotFound;
+ assert_eq!(format!("{err}"), "No Dispatcher Found");
+ }
+
+ #[test]
+ fn test_program_execute_error_display_renderer_not_found() {
+ let err = ProgramExecuteError::RendererNotFound("markdown".into());
+ assert_eq!(format!("{err}"), "No Renderer (`markdown`) Found");
+ }
+
+ #[test]
+ fn test_program_execute_error_display_panic() {
+ let p = ProgramPanic::new(Box::new("oops"));
+ let err = ProgramExecuteError::Panic(p);
+ let display = format!("{err}");
+ assert!(display.starts_with("Panic: "));
+ }
+
+ #[test]
+ fn test_program_execute_error_display_other() {
+ let err = ProgramExecuteError::Other("something went wrong".into());
+ assert_eq!(format!("{err}"), "Other error: something went wrong");
+ }
+
+ #[test]
+ fn test_program_execute_error_error_trait_no_source() {
+ use std::error::Error;
+ let err = ProgramExecuteError::Other("msg".into());
+ assert!(err.source().is_none());
+
+ let err = ProgramExecuteError::DispatcherNotFound;
+ assert!(err.source().is_none());
+
+ let err = ProgramExecuteError::RendererNotFound("json".into());
+ assert!(err.source().is_none());
+
+ let panic = ProgramPanic::new(Box::new("panic"));
+ let err = ProgramExecuteError::Panic(panic);
+ assert!(err.source().is_none());
+ }
+
+ #[test]
+ fn test_from_program_panic_into_program_execute_error() {
+ let panic = ProgramPanic::new(Box::new("from panic"));
+ let err: ProgramExecuteError = panic.into();
+ assert!(matches!(err, ProgramExecuteError::Panic(_)));
+ }
+
+ // ProgramInternalExecuteError
+
+ #[test]
+ fn test_program_internal_execute_error_display_dispatcher_not_found() {
+ let err = ProgramInternalExecuteError::DispatcherNotFound;
+ assert_eq!(format!("{err}"), "No Dispatcher Found");
+ }
+
+ #[test]
+ fn test_program_internal_execute_error_display_renderer_not_found() {
+ let err = ProgramInternalExecuteError::RendererNotFound("html".into());
+ assert_eq!(format!("{err}"), "No Renderer (`html`) Found");
+ }
+
+ #[test]
+ fn test_program_internal_execute_error_display_other() {
+ let err = ProgramInternalExecuteError::Other("internal issue".into());
+ assert_eq!(format!("{err}"), "Other error: internal issue");
+ }
+
+ #[test]
+ fn test_program_internal_execute_error_display_io() {
+ let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
+ let err = ProgramInternalExecuteError::IO(io_err);
+ let display = format!("{err}");
+ assert!(display.contains("IO error"));
+ assert!(display.contains("file missing"));
+ }
+
+ #[test]
+ fn test_program_internal_execute_error_display_repl_panic() {
+ let p = ProgramPanic::new(Box::new("repl crash"));
+ let err = ProgramInternalExecuteError::REPLPanic(p);
+ let display = format!("{err}");
+ assert!(display.starts_with("A single REPL execution failed: "));
+ assert!(display.contains("repl crash"));
+ }
+
+ #[test]
+ fn test_program_internal_execute_error_error_trait_source() {
+ use std::error::Error;
+
+ let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied");
+ let err = ProgramInternalExecuteError::IO(io_err);
+ assert!(err.source().is_some());
+
+ let err = ProgramInternalExecuteError::DispatcherNotFound;
+ assert!(err.source().is_none());
+
+ let err = ProgramInternalExecuteError::RendererNotFound("txt".into());
+ assert!(err.source().is_none());
+
+ let err = ProgramInternalExecuteError::Other("msg".into());
+ assert!(err.source().is_none());
+
+ let p = ProgramPanic::new(Box::new("msg"));
+ let err = ProgramInternalExecuteError::REPLPanic(p);
+ assert!(err.source().is_none());
+ }
+
+ #[test]
+ fn test_from_io_error_into_program_internal_execute_error() {
+ let io_err = std::io::Error::new(std::io::ErrorKind::TimedOut, "timeout");
+ let err: ProgramInternalExecuteError = io_err.into();
+ assert!(matches!(err, ProgramInternalExecuteError::IO(_)));
+ }
+
+ // From<ProgramInternalExecuteError> for ProgramExecuteError
+
+ #[test]
+ fn test_from_internal_to_execute_dispatcher_not_found() {
+ let internal = ProgramInternalExecuteError::DispatcherNotFound;
+ let err: ProgramExecuteError = internal.into();
+ assert!(matches!(err, ProgramExecuteError::DispatcherNotFound));
+ }
+
+ #[test]
+ fn test_from_internal_to_execute_renderer_not_found() {
+ let internal = ProgramInternalExecuteError::RendererNotFound("yaml".into());
+ let err: ProgramExecuteError = internal.into();
+ assert!(matches!(err, ProgramExecuteError::RendererNotFound(_)));
+ assert_eq!(format!("{err}"), "No Renderer (`yaml`) Found");
+ }
+
+ #[test]
+ fn test_from_internal_to_execute_other() {
+ let internal = ProgramInternalExecuteError::Other("custom".into());
+ let err: ProgramExecuteError = internal.into();
+ assert!(matches!(err, ProgramExecuteError::Other(_)));
+ assert_eq!(format!("{err}"), "Other error: custom");
+ }
+
+ #[test]
+ fn test_from_internal_to_execute_io_becomes_other() {
+ let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused");
+ let internal = ProgramInternalExecuteError::IO(io_err);
+ let err: ProgramExecuteError = internal.into();
+ assert!(matches!(err, ProgramExecuteError::Other(_)));
+ let display = format!("{err}");
+ assert!(display.starts_with("Other error: "));
+ assert!(display.contains("refused"));
+ }
+
+ #[test]
+ fn test_from_internal_to_execute_repl_panic_becomes_other() {
+ let p = ProgramPanic::new(Box::new("panic in repl"));
+ let internal = ProgramInternalExecuteError::REPLPanic(p);
+ let err: ProgramExecuteError = internal.into();
+ assert!(matches!(err, ProgramExecuteError::Other(_)));
+ let display = format!("{err}");
+ assert!(display.contains("A single REPL execution failed"));
+ assert!(display.contains("panic in repl"));
+ }
+
+ // From<ChainProcessError> for ProgramInternalExecuteError
+
+ #[test]
+ fn test_from_chain_process_error_other_into_internal() {
+ let chain_err = ChainProcessError::Other("chain other".into());
+ let err: ProgramInternalExecuteError = chain_err.into();
+ assert!(matches!(err, ProgramInternalExecuteError::Other(_)));
+ assert_eq!(format!("{err}"), "Other error: chain other");
+ }
+
+ #[test]
+ fn test_from_chain_process_error_io_into_internal() {
+ let io_err = std::io::Error::new(std::io::ErrorKind::BrokenPipe, "broken");
+ let chain_err = ChainProcessError::IO(io_err);
+ let err: ProgramInternalExecuteError = chain_err.into();
+ assert!(matches!(err, ProgramInternalExecuteError::IO(_)));
+ let display = format!("{err}");
+ assert!(display.contains("IO error"));
+ assert!(display.contains("broken"));
+ }
+}
diff --git a/mingling_core/src/program/flag.rs b/mingling_core/src/program/flag.rs
index bc1c922..0865414 100644
--- a/mingling_core/src/program/flag.rs
+++ b/mingling_core/src/program/flag.rs
@@ -161,6 +161,8 @@ macro_rules! special_arguments {
#[cfg(test)]
mod tests {
+ use crate::Flag;
+
#[test]
fn test_special_flag() {
// Test flag found and removed
@@ -466,6 +468,57 @@ mod tests {
assert_eq!(result, vec!["a", "b"]);
assert_eq!(args, vec!["--next", "1"]);
}
+
+ #[test]
+ fn test_flag_from_empty_tuple() {
+ let flag = Flag::from(());
+ assert_eq!(flag.as_ref(), &[] as &[&str]);
+ }
+
+ #[test]
+ fn test_flag_from_static_str() {
+ let flag = Flag::from("-h");
+ assert_eq!(flag.as_ref(), &["-h"]);
+ }
+
+ #[test]
+ fn test_flag_from_slice() {
+ let flag = Flag::from(&["-h", "--help"][..]);
+ assert_eq!(flag.as_ref(), &["-h", "--help"]);
+ }
+
+ #[test]
+ fn test_flag_from_array() {
+ let flag = Flag::from(["-v", "--verbose"]);
+ assert_eq!(flag.as_ref(), &["-v", "--verbose"]);
+ }
+
+ #[test]
+ fn test_flag_from_ref_array() {
+ let flag = Flag::from(&["-f", "--file"]);
+ assert_eq!(flag.as_ref(), &["-f", "--file"]);
+ }
+
+ #[test]
+ fn test_flag_from_ref_flag() {
+ let original = Flag::from("-x");
+ let cloned = Flag::from(&original);
+ assert_eq!(cloned.as_ref(), &["-x"]);
+ }
+
+ #[test]
+ fn test_flag_as_ref() {
+ let flag = Flag::from("-h");
+ let r: &[&str] = flag.as_ref();
+ assert_eq!(r, &["-h"]);
+ }
+
+ #[test]
+ fn test_flag_deref() {
+ let flag = Flag::from(["-a", "-b"]);
+ let collected: Vec<&&str> = flag.iter().collect();
+ assert_eq!(collected, vec![&"-a", &"-b"]);
+ }
}
impl<C> Program<C>
diff --git a/mingling_core/src/program/hook.rs b/mingling_core/src/program/hook.rs
index 929eac2..f6df2e2 100644
--- a/mingling_core/src/program/hook.rs
+++ b/mingling_core/src/program/hook.rs
@@ -543,3 +543,211 @@ where
self
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::Groupped;
+ use std::sync::atomic::{AtomicBool, Ordering};
+
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
+ #[cfg_attr(feature = "general_renderer", derive(serde::Serialize))]
+ enum MockHookEnum {
+ A,
+ B,
+ }
+
+ impl std::fmt::Display for MockHookEnum {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{:?}", self)
+ }
+ }
+
+ impl Groupped<MockHookEnum> for MockHookEnum {
+ fn member_id() -> MockHookEnum {
+ MockHookEnum::A
+ }
+ }
+
+ impl ProgramCollect for MockHookEnum {
+ type Enum = MockHookEnum;
+ type ErrorDispatcherNotFound = MockHookEnum;
+ type ErrorRendererNotFound = MockHookEnum;
+ type ResultEmpty = MockHookEnum;
+
+ fn build_renderer_not_found(_member_id: MockHookEnum) -> AnyOutput<MockHookEnum> {
+ unreachable!()
+ }
+
+ fn build_dispatcher_not_found(_args: Vec<String>) -> AnyOutput<MockHookEnum> {
+ unreachable!()
+ }
+
+ fn build_empty_result() -> AnyOutput<MockHookEnum> {
+ unreachable!()
+ }
+
+ fn render(_any: AnyOutput<MockHookEnum>, _r: &mut RenderResult) {
+ unreachable!()
+ }
+
+ fn render_help(_any: AnyOutput<MockHookEnum>, _r: &mut RenderResult) {
+ unreachable!()
+ }
+
+ fn do_chain(_any: AnyOutput<MockHookEnum>) -> crate::ChainProcess<MockHookEnum> {
+ unreachable!()
+ }
+
+ fn has_renderer(_any: &AnyOutput<MockHookEnum>) -> bool {
+ unreachable!()
+ }
+
+ fn has_chain(_any: &AnyOutput<MockHookEnum>) -> bool {
+ unreachable!()
+ }
+
+ #[cfg(feature = "comp")]
+ fn do_comp(_any: &AnyOutput<MockHookEnum>, _ctx: &crate::ShellContext) -> crate::Suggest {
+ unreachable!()
+ }
+
+ #[cfg(feature = "general_renderer")]
+ fn general_render(
+ _any: AnyOutput<MockHookEnum>,
+ _setting: &crate::GeneralRendererSetting,
+ ) -> Result<crate::RenderResult, crate::error::GeneralRendererSerializeError> {
+ unreachable!()
+ }
+ }
+
+ #[test]
+ fn test_hook_empty() {
+ let hook = ProgramHook::<MockHookEnum>::empty();
+ assert!(hook.begin.is_none());
+ assert!(hook.pre_dispatch.is_none());
+ assert!(hook.post_dispatch.is_none());
+ assert!(hook.pre_chain.is_none());
+ assert!(hook.post_chain.is_none());
+ assert!(hook.pre_render.is_none());
+ assert!(hook.post_render.is_none());
+ assert!(hook.finish.is_none());
+ }
+
+ #[test]
+ fn test_hook_on_begin() {
+ static CALLED: AtomicBool = AtomicBool::new(false);
+ let hook = ProgramHook::<MockHookEnum>::empty().on_begin(|| {
+ CALLED.store(true, Ordering::SeqCst);
+ });
+ assert!(hook.begin.is_some());
+ (hook.begin.unwrap())();
+ assert!(CALLED.load(Ordering::SeqCst));
+ }
+
+ #[test]
+ fn test_hook_on_pre_dispatch() {
+ static CALLED: AtomicBool = AtomicBool::new(false);
+ let hook = ProgramHook::<MockHookEnum>::empty().on_pre_dispatch(|args| {
+ assert_eq!(args, &["a", "b"]);
+ CALLED.store(true, Ordering::SeqCst);
+ });
+ assert!(hook.pre_dispatch.is_some());
+ (hook.pre_dispatch.unwrap())(&["a".to_string(), "b".to_string()]);
+ assert!(CALLED.load(Ordering::SeqCst));
+ }
+
+ #[test]
+ fn test_hook_on_post_dispatch() {
+ static CALLED: AtomicBool = AtomicBool::new(false);
+ let hook = ProgramHook::<MockHookEnum>::empty().on_post_dispatch(|entry| {
+ assert_eq!(*entry, MockHookEnum::A);
+ CALLED.store(true, Ordering::SeqCst);
+ });
+ assert!(hook.post_dispatch.is_some());
+ (hook.post_dispatch.unwrap())(&MockHookEnum::A);
+ assert!(CALLED.load(Ordering::SeqCst));
+ }
+
+ #[test]
+ fn test_hook_on_pre_chain() {
+ static CALLED: AtomicBool = AtomicBool::new(false);
+ let hook = ProgramHook::<MockHookEnum>::empty().on_pre_chain(
+ |input: &MockHookEnum, _raw: &dyn Any| {
+ assert_eq!(*input, MockHookEnum::A);
+ CALLED.store(true, Ordering::SeqCst);
+ },
+ );
+ assert!(hook.pre_chain.is_some());
+ (hook.pre_chain.unwrap())(&MockHookEnum::A, &42);
+ assert!(CALLED.load(Ordering::SeqCst));
+ }
+
+ #[test]
+ fn test_hook_on_post_chain() {
+ static CALLED: AtomicBool = AtomicBool::new(false);
+ let hook = ProgramHook::<MockHookEnum>::empty().on_post_chain(
+ |_output: &AnyOutput<MockHookEnum>| {
+ CALLED.store(true, Ordering::SeqCst);
+ },
+ );
+ assert!(hook.post_chain.is_some());
+ let output = AnyOutput::new(MockHookEnum::A);
+ (hook.post_chain.unwrap())(&output);
+ assert!(CALLED.load(Ordering::SeqCst));
+ }
+
+ #[test]
+ fn test_hook_on_pre_render() {
+ static CALLED: AtomicBool = AtomicBool::new(false);
+ let hook = ProgramHook::<MockHookEnum>::empty().on_pre_render(
+ |input: &MockHookEnum, _raw: &dyn Any| {
+ assert_eq!(*input, MockHookEnum::A);
+ CALLED.store(true, Ordering::SeqCst);
+ },
+ );
+ assert!(hook.pre_render.is_some());
+ (hook.pre_render.unwrap())(&MockHookEnum::A, &42);
+ assert!(CALLED.load(Ordering::SeqCst));
+ }
+
+ #[test]
+ fn test_hook_on_post_render() {
+ static CALLED: AtomicBool = AtomicBool::new(false);
+ let hook = ProgramHook::<MockHookEnum>::empty().on_post_render(|_result: &RenderResult| {
+ CALLED.store(true, Ordering::SeqCst);
+ });
+ assert!(hook.post_render.is_some());
+ let result = RenderResult::default();
+ (hook.post_render.unwrap())(&result);
+ assert!(CALLED.load(Ordering::SeqCst));
+ }
+
+ #[test]
+ fn test_hook_on_finish() {
+ let hook = ProgramHook::<MockHookEnum>::empty().on_finish(|| 42);
+ assert!(hook.finish.is_some());
+ assert_eq!((hook.finish.unwrap())(), 42);
+ }
+
+ #[test]
+ fn test_hook_builder_chaining() {
+ let hook = ProgramHook::<MockHookEnum>::empty()
+ .on_begin(|| {})
+ .on_pre_dispatch(|_| {})
+ .on_post_dispatch(|_| {})
+ .on_pre_chain(|_, _| {})
+ .on_post_chain(|_| {})
+ .on_pre_render(|_, _| {})
+ .on_post_render(|_| {})
+ .on_finish(|| 0);
+ assert!(hook.begin.is_some());
+ assert!(hook.pre_dispatch.is_some());
+ assert!(hook.post_dispatch.is_some());
+ assert!(hook.pre_chain.is_some());
+ assert!(hook.post_chain.is_some());
+ assert!(hook.pre_render.is_some());
+ assert!(hook.post_render.is_some());
+ assert!(hook.finish.is_some());
+ }
+}
diff --git a/mingling_core/src/program/setup.rs b/mingling_core/src/program/setup.rs
index fa9d0eb..2bfced1 100644
--- a/mingling_core/src/program/setup.rs
+++ b/mingling_core/src/program/setup.rs
@@ -16,3 +16,85 @@ where
S::setup(setup, self);
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::{AnyOutput, ChainProcess, Groupped, RenderResult};
+
+ /// Minimal mock collector that satisfies `C: ProgramCollect<Enum = C>`
+ /// by setting `Enum = Self`.
+ #[derive(Debug, Clone, PartialEq)]
+ struct MockCollect;
+
+ impl Groupped<MockCollect> for MockCollect {
+ fn member_id() -> MockCollect {
+ MockCollect
+ }
+ }
+
+ impl ProgramCollect for MockCollect {
+ type Enum = MockCollect;
+ type ErrorDispatcherNotFound = MockCollect;
+ type ErrorRendererNotFound = MockCollect;
+ type ResultEmpty = MockCollect;
+
+ fn build_renderer_not_found(_member_id: MockCollect) -> AnyOutput<MockCollect> {
+ unimplemented!()
+ }
+ fn build_dispatcher_not_found(_args: Vec<String>) -> AnyOutput<MockCollect> {
+ unimplemented!()
+ }
+ fn build_empty_result() -> AnyOutput<MockCollect> {
+ unimplemented!()
+ }
+ fn render(_any: AnyOutput<MockCollect>, _r: &mut RenderResult) {
+ unimplemented!()
+ }
+ fn render_help(_any: AnyOutput<MockCollect>, _r: &mut RenderResult) {
+ unimplemented!()
+ }
+ fn do_chain(_any: AnyOutput<MockCollect>) -> ChainProcess<MockCollect> {
+ unimplemented!()
+ }
+ #[cfg(feature = "comp")]
+ fn do_comp(_any: &AnyOutput<MockCollect>, _ctx: &crate::ShellContext) -> crate::Suggest {
+ unimplemented!()
+ }
+ fn has_renderer(_any: &AnyOutput<MockCollect>) -> bool {
+ unimplemented!()
+ }
+ fn has_chain(_any: &AnyOutput<MockCollect>) -> bool {
+ unimplemented!()
+ }
+
+ #[cfg(feature = "general_renderer")]
+ fn general_render(
+ _any: AnyOutput<MockCollect>,
+ _setting: &crate::GeneralRendererSetting,
+ ) -> Result<RenderResult, crate::error::GeneralRendererSerializeError> {
+ unimplemented!()
+ }
+ }
+
+ struct TestSetup {
+ called: std::rc::Rc<std::cell::Cell<bool>>,
+ }
+
+ impl ProgramSetup<MockCollect> for TestSetup {
+ fn setup(self, _program: &mut Program<MockCollect>) {
+ self.called.set(true);
+ }
+ }
+
+ #[test]
+ fn test_with_setup_calls_setup() {
+ let called = std::rc::Rc::new(std::cell::Cell::new(false));
+ let setup = TestSetup {
+ called: std::rc::Rc::clone(&called),
+ };
+ let mut program: Program<MockCollect> = Program::new_with_args(["test"]);
+ program.with_setup(setup);
+ assert!(called.get());
+ }
+}
diff --git a/mingling_core/src/program/single_instance.rs b/mingling_core/src/program/single_instance.rs
index d16bcf5..8b165bf 100644
--- a/mingling_core/src/program/single_instance.rs
+++ b/mingling_core/src/program/single_instance.rs
@@ -110,3 +110,49 @@ where
{
THIS_PROGRAM.get_raw()?.downcast_ref::<Program<C>>()
}
+
+#[cfg(test)]
+mod tests {
+ use super::ProgramCell;
+
+ #[test]
+ fn test_program_cell_set_and_get_raw() {
+ let cell = ProgramCell::new();
+ cell.set(Box::new(42_i32));
+ let val = cell.get_raw();
+ assert!(val.is_some());
+ assert_eq!(*val.unwrap().downcast_ref::<i32>().unwrap(), 42);
+ }
+
+ #[test]
+ fn test_program_cell_get_raw_uninitialized() {
+ let cell = ProgramCell::new();
+ assert!(cell.get_raw().is_none());
+ }
+
+ #[test]
+ #[should_panic(expected = "ProgramCell already initialized")]
+ fn test_program_cell_set_twice_panics() {
+ let cell = ProgramCell::new();
+ cell.set(Box::new(1_i32));
+ cell.set(Box::new(2_i32));
+ }
+
+ #[test]
+ fn test_program_cell_take() {
+ let cell = ProgramCell::new();
+ cell.set(Box::new(99_i32));
+
+ // SAFETY: test-local cell, no outstanding references.
+ let taken = unsafe { cell.take() };
+ assert!(taken.is_some());
+ assert_eq!(*taken.unwrap().downcast_ref::<i32>().unwrap(), 99);
+
+ // After take, get_raw returns None.
+ assert!(cell.get_raw().is_none());
+
+ // Calling take again returns None.
+ let taken_again = unsafe { cell.take() };
+ assert!(taken_again.is_none());
+ }
+}
diff --git a/mingling_core/src/program/string_vec.rs b/mingling_core/src/program/string_vec.rs
index fd0e2cb..1ccedf4 100644
--- a/mingling_core/src/program/string_vec.rs
+++ b/mingling_core/src/program/string_vec.rs
@@ -55,3 +55,62 @@ impl From<Vec<&str>> for StringVec {
}
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_string_vec_from_array() {
+ let sv = StringVec::from(["a", "b", "c"]);
+ assert_eq!(sv.vec, vec!["a", "b", "c"]);
+ }
+
+ #[test]
+ fn test_string_vec_from_slice_ref() {
+ let arr = ["x", "y"];
+ let sv = StringVec::from(&arr[..]);
+ assert_eq!(sv.vec, vec!["x", "y"]);
+ }
+
+ #[test]
+ fn test_string_vec_from_vec_string() {
+ let original = vec!["one".to_string(), "two".to_string()];
+ let sv = StringVec::from(original.clone());
+ assert_eq!(sv.vec, original);
+ }
+
+ #[test]
+ fn test_string_vec_from_slice_string() {
+ let original = vec!["a".to_string(), "b".to_string()];
+ let sv = StringVec::from(&original[..]);
+ assert_eq!(sv.vec, original);
+ }
+
+ #[test]
+ fn test_string_vec_from_vec_str() {
+ let sv = StringVec::from(vec!["hello", "world"]);
+ assert_eq!(sv.vec, vec!["hello", "world"]);
+ }
+
+ #[test]
+ fn test_string_vec_deref() {
+ let sv = StringVec::from(["alpha", "beta"]);
+ let inner: &Vec<String> = &*sv;
+ assert_eq!(inner.len(), 2);
+ assert_eq!(inner[0], "alpha");
+ }
+
+ #[test]
+ fn test_string_vec_into_vec() {
+ let sv = StringVec::from(["foo", "bar"]);
+ let v: Vec<String> = sv.into();
+ assert_eq!(v, vec!["foo", "bar"]);
+ }
+
+ #[test]
+ fn test_string_vec_empty_from_empty_array() {
+ let sv = StringVec::from([] as [&str; 0]);
+ assert!(sv.vec.is_empty());
+ }
+}