aboutsummaryrefslogtreecommitdiff
path: root/mingling_core/src
diff options
context:
space:
mode:
Diffstat (limited to 'mingling_core/src')
-rw-r--r--mingling_core/src/any.rs255
-rw-r--r--mingling_core/src/asset/chain/error.rs83
-rw-r--r--mingling_core/src/asset/dispatcher.rs148
-rw-r--r--mingling_core/src/asset/global_resource.rs66
-rw-r--r--mingling_core/src/asset/lazy_resource.rs345
-rw-r--r--mingling_core/src/asset/node.rs74
-rw-r--r--mingling_core/src/builds/comp.rs41
-rw-r--r--mingling_core/src/comp.rs10
-rw-r--r--mingling_core/src/comp/comp_ctx.rs80
-rw-r--r--mingling_core/src/comp/shell_ctx.rs125
-rw-r--r--mingling_core/src/comp/suggest.rs263
-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
-rw-r--r--mingling_core/src/renderer/general.rs112
-rw-r--r--mingling_core/src/renderer/general/error.rs32
-rw-r--r--mingling_core/src/renderer/render_result.rs94
21 files changed, 2540 insertions, 0 deletions
diff --git a/mingling_core/src/any.rs b/mingling_core/src/any.rs
index 46ebced..8ee07f5 100644
--- a/mingling_core/src/any.rs
+++ b/mingling_core/src/any.rs
@@ -146,3 +146,258 @@ impl<G> From<AnyOutput<G>> for ChainProcess<G> {
ChainProcess::Ok((value, NextProcess::Chain))
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::Groupped;
+
+ /// Mock enum for testing AnyOutput
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
+ #[allow(dead_code)]
+ enum MockGroup {
+ Alpha,
+ Beta,
+ Gamma,
+ }
+
+ impl std::fmt::Display for MockGroup {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ MockGroup::Alpha => write!(f, "Alpha"),
+ MockGroup::Beta => write!(f, "Beta"),
+ MockGroup::Gamma => write!(f, "Gamma"),
+ }
+ }
+ }
+
+ #[derive(Debug, Clone, PartialEq)]
+ #[cfg_attr(feature = "general_renderer", derive(serde::Serialize))]
+ struct AlphaData {
+ value: i32,
+ }
+
+ impl Groupped<MockGroup> for AlphaData {
+ fn member_id() -> MockGroup {
+ MockGroup::Alpha
+ }
+ }
+
+ #[derive(Debug, Clone, PartialEq)]
+ #[cfg_attr(feature = "general_renderer", derive(serde::Serialize))]
+ struct BetaData {
+ name: String,
+ }
+
+ impl Groupped<MockGroup> for BetaData {
+ fn member_id() -> MockGroup {
+ MockGroup::Beta
+ }
+ }
+
+ #[derive(Debug, Clone, PartialEq)]
+ #[allow(dead_code)]
+ #[cfg_attr(feature = "general_renderer", derive(serde::Serialize))]
+ struct GammaData;
+
+ impl Groupped<MockGroup> for GammaData {
+ fn member_id() -> MockGroup {
+ MockGroup::Gamma
+ }
+ }
+
+ // AnyOutput::new
+
+ #[test]
+ fn test_any_output_new_stores_type_id_and_member_id() {
+ let data = AlphaData { value: 42 };
+ let output = AnyOutput::new(data);
+
+ assert_eq!(output.type_id, std::any::TypeId::of::<AlphaData>());
+ assert_eq!(output.member_id, MockGroup::Alpha);
+ }
+
+ // AnyOutput::downcast
+
+ #[test]
+ fn test_any_output_downcast_success() {
+ let data = AlphaData { value: 99 };
+ let output = AnyOutput::new(data);
+
+ let result: Result<AlphaData, _> = output.downcast::<AlphaData>();
+ assert!(result.is_ok());
+ assert_eq!(result.unwrap().value, 99);
+ }
+
+ #[test]
+ fn test_any_output_downcast_failure() {
+ let data = AlphaData { value: 10 };
+ let output = AnyOutput::new(data);
+
+ let result: Result<BetaData, _> = output.downcast::<BetaData>();
+ assert!(result.is_err());
+ }
+
+ // AnyOutput::is
+
+ #[test]
+ fn test_any_output_is_true_for_matching_type() {
+ let data = AlphaData { value: 7 };
+ let output = AnyOutput::new(data);
+
+ assert!(output.is::<AlphaData>());
+ }
+
+ #[test]
+ fn test_any_output_is_false_for_non_matching_type() {
+ let data = AlphaData { value: 7 };
+ let output = AnyOutput::new(data);
+
+ assert!(!output.is::<BetaData>());
+ }
+
+ // AnyOutput::route_chain
+
+ #[test]
+ fn test_route_chain_returns_ok_with_chain_next() {
+ let data = AlphaData { value: 1 };
+ let output = AnyOutput::new(data);
+
+ let result = output.route_chain();
+ match result {
+ ChainProcess::Ok((any, next)) => {
+ assert_eq!(any.member_id, MockGroup::Alpha);
+ assert_eq!(next, NextProcess::Chain);
+ }
+ _ => panic!("Expected ChainProcess::Ok"),
+ }
+ }
+
+ // AnyOutput::route_renderer
+
+ #[test]
+ fn test_route_renderer_returns_ok_with_renderer_next() {
+ let data = AlphaData { value: 2 };
+ let output = AnyOutput::new(data);
+
+ let result = output.route_renderer();
+ match result {
+ ChainProcess::Ok((any, next)) => {
+ assert_eq!(any.member_id, MockGroup::Alpha);
+ assert_eq!(next, NextProcess::Renderer);
+ }
+ _ => panic!("Expected ChainProcess::Ok"),
+ }
+ }
+
+ // AnyOutput: Deref / DerefMut
+
+ #[test]
+ fn test_any_output_deref_accesses_inner_any() {
+ let data = AlphaData { value: 5 };
+ let output = AnyOutput::new(data);
+
+ let inner: &dyn std::any::Any = &*output;
+ assert!(inner.downcast_ref::<AlphaData>().is_some());
+ }
+
+ #[test]
+ fn test_any_output_deref_mut_allows_modification() {
+ let data = AlphaData { value: 0 };
+ let mut output = AnyOutput::new(data);
+
+ let inner: &mut dyn std::any::Any = &mut *output;
+ if let Some(ref mut v) = inner.downcast_mut::<AlphaData>() {
+ v.value = 100;
+ }
+
+ let result: Result<AlphaData, _> = output.downcast::<AlphaData>();
+ assert_eq!(result.unwrap().value, 100);
+ }
+
+ // ChainProcess::From<AnyOutput>
+
+ #[test]
+ fn test_chain_process_from_any_output() {
+ let data = AlphaData { value: 3 };
+ let output = AnyOutput::new(data);
+
+ let cp: ChainProcess<MockGroup> = output.into();
+ match cp {
+ ChainProcess::Ok((any, next)) => {
+ assert_eq!(any.member_id, MockGroup::Alpha);
+ assert_eq!(next, NextProcess::Chain);
+ }
+ _ => panic!("Expected ChainProcess::Ok"),
+ }
+ }
+
+ // NextProcess::Display
+
+ #[test]
+ fn test_next_process_display_chain() {
+ assert_eq!(format!("{}", NextProcess::Chain), "Chain");
+ }
+
+ #[test]
+ fn test_next_process_display_renderer() {
+ assert_eq!(format!("{}", NextProcess::Renderer), "Renderer");
+ }
+
+ // AnyOutput::restore general_renderer feature only
+
+ #[cfg(feature = "general_renderer")]
+ #[test]
+ fn test_any_output_restore_success() {
+ use serde::Serialize;
+
+ #[derive(Debug, Clone, PartialEq, Serialize)]
+ struct SerData {
+ x: i32,
+ }
+
+ impl Groupped<MockGroup> for SerData {
+ fn member_id() -> MockGroup {
+ MockGroup::Gamma
+ }
+ }
+
+ let data = SerData { x: 42 };
+ let output = AnyOutput::new(data);
+ let restored: Option<SerData> = output.restore::<SerData>();
+ assert_eq!(restored, Some(SerData { x: 42 }));
+ }
+
+ #[cfg(feature = "general_renderer")]
+ #[test]
+ fn test_any_output_restore_type_mismatch() {
+ use serde::Serialize;
+
+ #[derive(Debug, Clone, PartialEq, Serialize)]
+ struct SerA {
+ a: i32,
+ }
+
+ #[derive(Debug, Clone, PartialEq, Serialize)]
+ struct SerB {
+ b: String,
+ }
+
+ impl Groupped<MockGroup> for SerA {
+ fn member_id() -> MockGroup {
+ MockGroup::Alpha
+ }
+ }
+
+ impl Groupped<MockGroup> for SerB {
+ fn member_id() -> MockGroup {
+ MockGroup::Beta
+ }
+ }
+
+ let data = SerA { a: 1 };
+ let output = AnyOutput::new(data);
+ let restored: Option<SerB> = output.restore::<SerB>();
+ assert_eq!(restored, None);
+ }
+}
diff --git a/mingling_core/src/asset/chain/error.rs b/mingling_core/src/asset/chain/error.rs
index 29abba1..ad64195 100644
--- a/mingling_core/src/asset/chain/error.rs
+++ b/mingling_core/src/asset/chain/error.rs
@@ -53,3 +53,86 @@ impl From<ProgramInternalExecuteError> for ChainProcessError {
}
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::error::{ProgramInternalExecuteError, ProgramPanic};
+ use std::error::Error;
+
+ #[test]
+ fn test_chain_process_error_display_other() {
+ let err = ChainProcessError::Other("something went wrong".into());
+ assert_eq!(format!("{err}"), "Other error: something went wrong");
+ }
+
+ #[test]
+ fn test_chain_process_error_display_io() {
+ let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
+ let err = ChainProcessError::IO(io_err);
+ let display = format!("{err}");
+ assert!(display.contains("IO error"));
+ assert!(display.contains("file not found"));
+ }
+
+ #[test]
+ fn test_chain_process_error_source_io() {
+ let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied");
+ let err = ChainProcessError::IO(io_err);
+ assert!(err.source().is_some());
+ }
+
+ #[test]
+ fn test_chain_process_error_source_other() {
+ let err = ChainProcessError::Other("msg".into());
+ assert!(err.source().is_none());
+ }
+
+ #[test]
+ fn test_from_io_error_into_chain_process_error() {
+ let io_err = std::io::Error::new(std::io::ErrorKind::TimedOut, "timeout");
+ let err: ChainProcessError = io_err.into();
+ assert!(matches!(err, ChainProcessError::IO(_)));
+ }
+
+ #[test]
+ fn test_from_program_internal_execute_error_dispatcher_not_found() {
+ let internal = ProgramInternalExecuteError::DispatcherNotFound;
+ let err: ChainProcessError = internal.into();
+ assert!(matches!(err, ChainProcessError::Other(_)));
+ assert_eq!(format!("{err}"), "Other error: DispatcherNotFound");
+ }
+
+ #[test]
+ fn test_from_program_internal_execute_error_renderer_not_found() {
+ let internal = ProgramInternalExecuteError::RendererNotFound("json".into());
+ let err: ChainProcessError = internal.into();
+ assert_eq!(format!("{err}"), "Other error: RendererNotFound: json");
+ }
+
+ #[test]
+ fn test_from_program_internal_execute_error_other() {
+ let internal = ProgramInternalExecuteError::Other("custom error".into());
+ let err: ChainProcessError = internal.into();
+ assert_eq!(format!("{err}"), "Other error: custom error");
+ }
+
+ #[test]
+ fn test_from_program_internal_execute_error_io() {
+ let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused");
+ let internal = ProgramInternalExecuteError::IO(io_err);
+ let err: ChainProcessError = internal.into();
+ let display = format!("{err}");
+ assert!(display.contains("IOError"));
+ }
+
+ #[test]
+ fn test_from_program_internal_execute_error_repl_panic() {
+ let panic_payload = ProgramPanic {
+ payload: Box::new("repl crash"),
+ };
+ let internal = ProgramInternalExecuteError::REPLPanic(panic_payload);
+ let err: ChainProcessError = internal.into();
+ assert!(format!("{err}").contains("REPLPanic"));
+ }
+}
diff --git a/mingling_core/src/asset/dispatcher.rs b/mingling_core/src/asset/dispatcher.rs
index b62a0d0..1652ced 100644
--- a/mingling_core/src/asset/dispatcher.rs
+++ b/mingling_core/src/asset/dispatcher.rs
@@ -227,3 +227,151 @@ impl<G> From<Dispatchers<G>> for Vec<Box<dyn Dispatcher<G> + Send + Sync + 'stat
val.dispatcher
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::ChainProcess;
+ use std::fmt::Display;
+
+ /// A minimal mock Dispatcher for testing Dispatchers conversions.
+ #[derive(Clone)]
+ struct MockDispatcher {
+ name: &'static str,
+ }
+
+ impl<C: Display> Dispatcher<C> for MockDispatcher {
+ fn node(&self) -> crate::asset::node::Node {
+ self.name.into()
+ }
+
+ fn begin(&self, _args: Vec<String>) -> ChainProcess<C> {
+ unimplemented!("not used in these tests")
+ }
+
+ fn clone_dispatcher(&self) -> Box<dyn Dispatcher<C>> {
+ Box::new(self.clone())
+ }
+ }
+
+ /// Minimal mock group for Dispatchers tests
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
+ #[allow(dead_code)]
+ enum MockG {
+ A,
+ }
+
+ impl Display for MockG {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "A")
+ }
+ }
+
+ #[test]
+ fn test_dispatchers_from_single_tuple() {
+ let disp = MockDispatcher { name: "foo" };
+ let dispatchers: Dispatchers<MockG> = Dispatchers::from((disp,));
+ assert_eq!(dispatchers.dispatcher.len(), 1);
+ }
+
+ #[test]
+ fn test_dispatchers_from_two_tuple() {
+ let d1 = MockDispatcher { name: "a" };
+ let d2 = MockDispatcher { name: "b" };
+ let dispatchers: Dispatchers<MockG> = Dispatchers::from((d1, d2));
+ assert_eq!(dispatchers.dispatcher.len(), 2);
+ }
+
+ #[test]
+ fn test_dispatchers_from_three_tuple() {
+ let d1 = MockDispatcher { name: "x" };
+ let d2 = MockDispatcher { name: "y" };
+ let d3 = MockDispatcher { name: "z" };
+ let dispatchers: Dispatchers<MockG> = Dispatchers::from((d1, d2, d3));
+ assert_eq!(dispatchers.dispatcher.len(), 3);
+ }
+
+ #[test]
+ fn test_dispatchers_from_four_tuple() {
+ let d1 = MockDispatcher { name: "1" };
+ let d2 = MockDispatcher { name: "2" };
+ let d3 = MockDispatcher { name: "3" };
+ let d4 = MockDispatcher { name: "4" };
+ let dispatchers: Dispatchers<MockG> = Dispatchers::from((d1, d2, d3, d4));
+ assert_eq!(dispatchers.dispatcher.len(), 4);
+ }
+
+ #[test]
+ fn test_dispatchers_from_five_tuple() {
+ let d1 = MockDispatcher { name: "a" };
+ let d2 = MockDispatcher { name: "b" };
+ let d3 = MockDispatcher { name: "c" };
+ let d4 = MockDispatcher { name: "d" };
+ let d5 = MockDispatcher { name: "e" };
+ let dispatchers: Dispatchers<MockG> = Dispatchers::from((d1, d2, d3, d4, d5));
+ assert_eq!(dispatchers.dispatcher.len(), 5);
+ }
+
+ #[test]
+ fn test_dispatchers_from_six_tuple() {
+ let d1 = MockDispatcher { name: "a" };
+ let d2 = MockDispatcher { name: "b" };
+ let d3 = MockDispatcher { name: "c" };
+ let d4 = MockDispatcher { name: "d" };
+ let d5 = MockDispatcher { name: "e" };
+ let d6 = MockDispatcher { name: "f" };
+ let dispatchers: Dispatchers<MockG> = Dispatchers::from((d1, d2, d3, d4, d5, d6));
+ assert_eq!(dispatchers.dispatcher.len(), 6);
+ }
+
+ #[test]
+ fn test_dispatchers_from_seven_tuple() {
+ let d1 = MockDispatcher { name: "a" };
+ let d2 = MockDispatcher { name: "b" };
+ let d3 = MockDispatcher { name: "c" };
+ let d4 = MockDispatcher { name: "d" };
+ let d5 = MockDispatcher { name: "e" };
+ let d6 = MockDispatcher { name: "f" };
+ let d7 = MockDispatcher { name: "g" };
+ let dispatchers: Dispatchers<MockG> = Dispatchers::from((d1, d2, d3, d4, d5, d6, d7));
+ assert_eq!(dispatchers.dispatcher.len(), 7);
+ }
+
+ #[test]
+ fn test_dispatchers_from_vec_of_boxed() {
+ let d1: Box<dyn Dispatcher<MockG> + Send + Sync> = Box::new(MockDispatcher { name: "a" });
+ let d2: Box<dyn Dispatcher<MockG> + Send + Sync> = Box::new(MockDispatcher { name: "b" });
+ let dispatchers: Dispatchers<MockG> = vec![d1, d2].into();
+ assert_eq!(dispatchers.dispatcher.len(), 2);
+ }
+
+ #[test]
+ fn test_dispatchers_from_single_boxed() {
+ let d: Box<dyn Dispatcher<MockG> + Send + Sync> = Box::new(MockDispatcher { name: "x" });
+ let dispatchers: Dispatchers<MockG> = d.into();
+ assert_eq!(dispatchers.dispatcher.len(), 1);
+ }
+
+ #[test]
+ fn test_dispatchers_deref() {
+ let disp = MockDispatcher { name: "test" };
+ let dispatchers: Dispatchers<MockG> = Dispatchers::from((disp,));
+ let inner: &Vec<Box<dyn Dispatcher<MockG> + Send + Sync + 'static>> = &*dispatchers;
+ assert_eq!(inner.len(), 1);
+ }
+
+ #[test]
+ fn test_dispatchers_into_vec() {
+ let disp = MockDispatcher { name: "foo" };
+ let dispatchers: Dispatchers<MockG> = Dispatchers::from((disp,));
+ let vec: Vec<Box<dyn Dispatcher<MockG> + Send + Sync + 'static>> = dispatchers.into();
+ assert_eq!(vec.len(), 1);
+ }
+
+ #[test]
+ fn test_box_clone_dispatcher() {
+ let disp: Box<dyn Dispatcher<MockG>> = Box::new(MockDispatcher { name: "clonable" });
+ let cloned = disp.clone_dispatcher();
+ assert_eq!(cloned.node().to_string(), "clonable");
+ }
+}
diff --git a/mingling_core/src/asset/global_resource.rs b/mingling_core/src/asset/global_resource.rs
index d03c6ea..83a779d 100644
--- a/mingling_core/src/asset/global_resource.rs
+++ b/mingling_core/src/asset/global_resource.rs
@@ -164,3 +164,69 @@ impl<T: Default + Clone + Send + Sync + 'static> ResourceMarker for T {
this::<C>().modify_res(f);
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn global_resource_new_and_deref() {
+ let res = GlobalResource::new(42i32);
+ assert_eq!(*res, 42);
+ }
+
+ #[test]
+ fn global_resource_from_arc() {
+ let arc = Arc::new(42i32);
+ let res = GlobalResource::from(arc);
+ assert_eq!(*res, 42);
+ }
+
+ #[test]
+ fn global_resource_as_ref() {
+ let res = GlobalResource::new(42i32);
+ assert_eq!(res.as_ref(), &42);
+ }
+
+ #[test]
+ fn resource_marker_i32_res_clone() {
+ let val = 42i32;
+ let cloned = val.res_clone();
+ assert_eq!(cloned, 42);
+ }
+
+ #[test]
+ fn resource_marker_i32_res_default() {
+ assert_eq!(<i32 as ResourceMarker>::res_default(), 0i32);
+ }
+
+ #[test]
+ fn resource_marker_string_res_clone() {
+ let val = "hello".to_string();
+ let cloned = val.res_clone();
+ assert_eq!(cloned, "hello");
+ }
+
+ #[test]
+ fn resource_marker_string_res_default() {
+ assert_eq!(<String as ResourceMarker>::res_default(), "");
+ }
+
+ #[test]
+ fn resource_marker_vec_res_clone() {
+ let val = vec![1, 2, 3];
+ let cloned = val.res_clone();
+ assert_eq!(cloned, vec![1, 2, 3]);
+ }
+
+ #[test]
+ fn resource_marker_vec_res_default() {
+ let empty: Vec<i32> = vec![];
+ assert_eq!(<Vec<i32> as ResourceMarker>::res_default(), empty);
+ }
+
+ // Note: Tests for Program::with_resource, res(), res_or_route(), res_or_default(),
+ // and modify_res() require a concrete ProgramCollect implementation, which is
+ // complex and outside the scope of these unit tests.
+ // Those are better covered by integration tests.
+}
diff --git a/mingling_core/src/asset/lazy_resource.rs b/mingling_core/src/asset/lazy_resource.rs
index 6f1bd81..918aeb2 100644
--- a/mingling_core/src/asset/lazy_resource.rs
+++ b/mingling_core/src/asset/lazy_resource.rs
@@ -305,3 +305,348 @@ where
this::<C>().modify_res(f);
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::sync::Arc;
+ use std::sync::atomic::{AtomicBool, Ordering};
+
+ /// Helper for tracking drops via an `Arc<AtomicBool>`.
+ struct DropFlag(Arc<AtomicBool>);
+
+ impl Drop for DropFlag {
+ fn drop(&mut self) {
+ self.0.store(true, Ordering::SeqCst);
+ }
+ }
+
+ // LazyRes::new starts uninitialized
+ #[test]
+ fn new_returns_uninitialized() {
+ let r: LazyRes<i32> = LazyRes::new(|| 42);
+ assert!(!r.is_initialized());
+ }
+
+ // LazyRes::get_ref triggers init and returns correct value
+ #[test]
+ fn get_ref_triggers_initialization() {
+ let mut r = LazyRes::new(|| 42);
+ assert!(!r.is_initialized());
+ let val = r.get_ref();
+ assert_eq!(*val, 42);
+ assert!(r.is_initialized());
+ }
+
+ #[test]
+ fn get_ref_returns_same_value_on_subsequent_calls() {
+ let mut r = LazyRes::new(|| 42);
+ assert_eq!(*r.get_ref(), 42);
+ assert_eq!(*r.get_ref(), 42);
+ }
+
+ // LazyRes::get_mut triggers init and allows mutation
+ #[test]
+ fn get_mut_triggers_initialization() {
+ let mut r = LazyRes::new(|| 10);
+ assert!(!r.is_initialized());
+ *r.get_mut() = 20;
+ assert_eq!(*r.get_ref(), 20);
+ assert!(r.is_initialized());
+ }
+
+ // LazyRes::get_clone returns cloned value
+ #[test]
+ fn get_clone_returns_cloned_value() {
+ let mut r = LazyRes::new(|| "hello".to_string());
+ assert_eq!(r.get_clone(), "hello");
+ assert!(r.is_initialized());
+ }
+
+ // LazyRes::is_initialized is false before get_ref and true after
+ #[test]
+ fn is_initialized_false_before_true_after() {
+ let mut r = LazyRes::new(|| 99);
+ assert!(!r.is_initialized());
+ r.get_ref();
+ assert!(r.is_initialized());
+ }
+
+ // LazyRes::into_inner returns Some if initialized and None otherwise
+ #[test]
+ fn into_inner_initialized_returns_some() {
+ let mut r = LazyRes::new(|| 7);
+ r.get_ref(); // force init
+ assert_eq!(r.into_inner(), Some(7));
+ }
+
+ #[test]
+ fn into_inner_uninitialized_returns_none() {
+ let r: LazyRes<i32> = LazyRes::new(|| 7);
+ assert_eq!(r.into_inner(), None);
+ }
+
+ // LazyRes::unwrap returns value if initialized or panics otherwise
+ #[test]
+ fn unwrap_initialized_returns_value() {
+ let mut r = LazyRes::new(|| 13);
+ r.get_ref();
+ assert_eq!(r.unwrap(), 13);
+ }
+
+ #[test]
+ #[should_panic(expected = "uninitialized")]
+ fn unwrap_uninitialized_panics() {
+ let r: LazyRes<i32> = LazyRes::new(|| 13);
+ r.unwrap();
+ }
+
+ // LazyRes::unwrap_or returns default if uninitialized
+ #[test]
+ fn unwrap_or_uninitialized_returns_default() {
+ let r: LazyRes<i32> = LazyRes::new(|| 5);
+ assert_eq!(r.unwrap_or(100), 100);
+ }
+
+ #[test]
+ fn unwrap_or_initialized_returns_inner() {
+ let mut r = LazyRes::new(|| 5);
+ r.get_ref();
+ assert_eq!(r.unwrap_or(100), 5);
+ }
+
+ // LazyRes::unwrap_or_default returns T::default
+ #[test]
+ fn unwrap_or_default_uninitialized_returns_default() {
+ let r: LazyRes<i32> = LazyRes::new(|| 42);
+ assert_eq!(r.unwrap_or_default(), 0);
+ }
+
+ #[test]
+ fn unwrap_or_default_initialized_returns_inner() {
+ let mut r = LazyRes::new(|| 42);
+ r.get_ref();
+ assert_eq!(r.unwrap_or_default(), 42);
+ }
+
+ // LazyRes::Default creates uninitialized with T::default factory
+ #[test]
+ fn default_creates_uninitialized_with_default_factory() {
+ let r: LazyRes<i32> = LazyRes::default();
+ assert!(!r.is_initialized());
+ }
+
+ #[test]
+ fn default_factory_produces_t_default() {
+ let mut r: LazyRes<i32> = LazyRes::default();
+ assert_eq!(*r.get_ref(), 0);
+ }
+
+ // From<T> for LazyRes creates initialized value
+ #[test]
+ fn from_t_creates_initialized() {
+ let r: LazyRes<String> = LazyRes::from("hello".to_string());
+ assert!(r.is_initialized());
+ }
+
+ #[test]
+ fn from_t_contains_correct_value() {
+ let r: LazyRes<String> = LazyRes::from("world".to_string());
+ // Can only check via into_inner since get_ref needs &mut
+ assert_eq!(r.into_inner(), Some("world".to_string()));
+ }
+
+ // Drop callback via new_with_drop is called on drop
+ #[test]
+ fn new_with_drop_calls_callback_on_drop() {
+ let dropped = Arc::new(AtomicBool::new(false));
+ let dropped_clone = Arc::clone(&dropped);
+
+ let r = LazyRes::new_with_drop(
+ || 42,
+ move |val| {
+ assert_eq!(val, 42);
+ dropped_clone.store(true, Ordering::SeqCst);
+ },
+ );
+ // Force init first
+ drop(r);
+ // Not initialized, so the callback above was stored but never invoked.
+ // The initialized path is tested below.
+ }
+
+ #[test]
+ fn new_with_drop_calls_callback_on_drop_after_init() {
+ let dropped = Arc::new(AtomicBool::new(false));
+ let dropped_clone = Arc::clone(&dropped);
+
+ let mut r = LazyRes::new_with_drop(
+ || 42,
+ move |val| {
+ assert_eq!(val, 42);
+ dropped_clone.store(true, Ordering::SeqCst);
+ },
+ );
+ r.get_ref(); // initialize
+ assert!(r.is_initialized());
+ drop(r);
+ assert!(dropped.load(Ordering::SeqCst));
+ }
+
+ // Drop callback via with_on_drop uses chained builder style
+ #[test]
+ fn with_on_drop_calls_callback_on_drop() {
+ let dropped = Arc::new(AtomicBool::new(false));
+ let dropped_clone = Arc::clone(&dropped);
+
+ let r = LazyRes::new(|| 99).with_on_drop(move |val| {
+ assert_eq!(val, 99);
+ dropped_clone.store(true, Ordering::SeqCst);
+ });
+ drop(r); // not initialized, callback stored but won't fire
+ assert!(!dropped.load(Ordering::SeqCst));
+ }
+
+ #[test]
+ fn with_on_drop_calls_callback_on_drop_after_init() {
+ let dropped = Arc::new(AtomicBool::new(false));
+ let dropped_clone = Arc::clone(&dropped);
+
+ let mut r = LazyRes::new(|| 99).with_on_drop(move |val| {
+ assert_eq!(val, 99);
+ dropped_clone.store(true, Ordering::SeqCst);
+ });
+ r.get_ref();
+ drop(r);
+ assert!(dropped.load(Ordering::SeqCst));
+ }
+
+ // set_on_drop sets callback after construction
+ #[test]
+ fn set_on_drop_calls_callback_on_drop() {
+ let dropped = Arc::new(AtomicBool::new(false));
+ let dropped_clone = Arc::clone(&dropped);
+
+ let mut r = LazyRes::new(|| 55);
+ r.get_ref(); // init first
+ r.set_on_drop(move |val| {
+ assert_eq!(val, 55);
+ dropped_clone.store(true, Ordering::SeqCst);
+ });
+ drop(r);
+ assert!(dropped.load(Ordering::SeqCst));
+ }
+
+ #[test]
+ fn set_on_drop_before_init_stored_and_fires_after_init() {
+ let dropped = Arc::new(AtomicBool::new(false));
+ let dropped_clone = Arc::clone(&dropped);
+
+ let mut r = LazyRes::new(|| 55);
+ r.set_on_drop(move |val| {
+ assert_eq!(val, 55);
+ dropped_clone.store(true, Ordering::SeqCst);
+ });
+ r.get_ref(); // init after setting callback
+ drop(r);
+ assert!(dropped.load(Ordering::SeqCst));
+ }
+
+ // LazyInit::lazy_default trait method
+ #[test]
+ fn lazy_default_creates_uninitialized() {
+ let r: LazyRes<i32> = i32::lazy_default();
+ assert!(!r.is_initialized());
+ }
+
+ #[test]
+ fn lazy_default_factory_returns_default() {
+ let mut r: LazyRes<i32> = i32::lazy_default();
+ assert_eq!(*r.get_ref(), 0);
+ }
+
+ // LazyInit::lazy_init trait method with custom factory
+ #[test]
+ fn lazy_init_creates_uninitialized() {
+ let r: LazyRes<i32> = i32::lazy_init(|| 77);
+ assert!(!r.is_initialized());
+ }
+
+ #[test]
+ fn lazy_init_factory_produces_correct_value() {
+ let mut r: LazyRes<i32> = i32::lazy_init(|| 77);
+ assert_eq!(*r.get_ref(), 77);
+ }
+
+ // ResourceMarker for LazyRes res_clone clones initialized and res_default returns default
+ #[test]
+ fn res_clone_of_initialized_clones_value() {
+ let mut r = LazyRes::new(|| vec![1, 2, 3]);
+ r.get_ref();
+ let cloned = r.res_clone();
+ assert!(cloned.is_initialized());
+ assert_eq!(cloned.into_inner(), Some(vec![1, 2, 3]));
+ }
+
+ #[test]
+ fn res_clone_of_uninitialized_creates_default() {
+ let r: LazyRes<Vec<i32>> = LazyRes::new(|| vec![1, 2, 3]);
+ let cloned = r.res_clone();
+ // The source is uninitialized, so res_clone returns a default lazy
+ assert!(!cloned.is_initialized());
+ }
+
+ #[test]
+ fn res_default_returns_uninitialized() {
+ let r: LazyRes<i32> = LazyRes::<i32>::res_default();
+ assert!(!r.is_initialized());
+ }
+
+ // Factory is dropped after init via Arc flag
+ #[test]
+ fn factory_dropped_after_initialization() {
+ let factory_dropped = Arc::new(AtomicBool::new(false));
+ let flag = DropFlag(Arc::clone(&factory_dropped));
+
+ // The factory closure captures `flag`. When the closure (the factory) is
+ // consumed and dropped after init, the captured `flag` will be dropped,
+ // setting the atomic bool.
+ let factory = move || {
+ let _ = &flag;
+ 42
+ };
+
+ let mut r = LazyRes::new(factory);
+ assert!(
+ !factory_dropped.load(Ordering::SeqCst),
+ "factory not dropped yet"
+ );
+
+ r.get_ref(); // init — factory should be consumed and dropped
+ assert!(
+ factory_dropped.load(Ordering::SeqCst),
+ "factory should be dropped after initialization"
+ );
+
+ // Second access still works
+ assert_eq!(*r.get_ref(), 42);
+ }
+
+ #[test]
+ fn factory_dropped_even_when_not_initialized_and_dropped() {
+ let factory_dropped = Arc::new(AtomicBool::new(false));
+ let flag = DropFlag(Arc::clone(&factory_dropped));
+
+ let factory = move || {
+ let _ = &flag;
+ 42
+ };
+
+ let r: LazyRes<i32> = LazyRes::new(factory);
+ drop(r);
+ assert!(
+ factory_dropped.load(Ordering::SeqCst),
+ "factory should be dropped when LazyRes is dropped"
+ );
+ }
+}
diff --git a/mingling_core/src/asset/node.rs b/mingling_core/src/asset/node.rs
index 4dfdb48..caf34ec 100644
--- a/mingling_core/src/asset/node.rs
+++ b/mingling_core/src/asset/node.rs
@@ -58,3 +58,77 @@ impl std::fmt::Display for Node {
write!(f, "{}", self.node.join("."))
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_node_from_single_str() {
+ let node = Node::from("hello");
+ assert_eq!(node.node, vec!["hello"]);
+ assert_eq!(node.to_string(), "hello");
+ }
+
+ #[test]
+ fn test_node_from_dotted_str() {
+ let node = Node::from("a.b.c");
+ assert_eq!(node.node, vec!["a", "b", "c"]);
+ assert_eq!(node.to_string(), "a.b.c");
+ }
+
+ #[test]
+ fn test_node_kebab_case_conversion() {
+ let node = Node::from("HelloWorld.FooBar");
+ assert_eq!(node.node, vec!["hello-world", "foo-bar"]);
+ }
+
+ #[test]
+ fn test_node_from_string() {
+ let s = String::from("x.y");
+ let node = Node::from(s);
+ assert_eq!(node.node, vec!["x", "y"]);
+ }
+
+ #[test]
+ fn test_node_join() {
+ let node = Node::from("base").join("sub");
+ assert_eq!(node.node, vec!["base", "sub"]);
+ }
+
+ #[test]
+ fn test_node_join_multiple() {
+ let node = Node::from("a").join("b").join("c");
+ assert_eq!(node.to_string(), "a.b.c");
+ }
+
+ #[test]
+ fn test_node_default_empty() {
+ let node = Node::default();
+ assert!(node.node.is_empty());
+ assert_eq!(node.to_string(), "");
+ }
+
+ #[test]
+ fn test_node_partial_eq() {
+ let a = Node::from("a.b");
+ let b = Node::from("a.b");
+ let c = Node::from("a.c");
+ assert_eq!(a, b);
+ assert_ne!(a, c);
+ }
+
+ #[test]
+ fn test_node_ord() {
+ let a = Node::from("a");
+ let b = Node::from("b");
+ assert!(a < b);
+ }
+
+ #[test]
+ fn test_node_join_appends_part() {
+ let node = Node::from("existing");
+ let joined = node.join("new-part");
+ assert_eq!(joined.to_string(), "existing.new-part");
+ }
+}
diff --git a/mingling_core/src/builds/comp.rs b/mingling_core/src/builds/comp.rs
index aa08627..b826531 100644
--- a/mingling_core/src/builds/comp.rs
+++ b/mingling_core/src/builds/comp.rs
@@ -125,3 +125,44 @@ fn get_tmpl(shell_flag: &ShellFlag) -> (&'static str, &'static str) {
ShellFlag::Other(_) => (TMPL_COMP_BASH, ".sh"),
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::ShellFlag;
+
+ #[test]
+ fn get_tmpl_bash() {
+ let (tmpl, ext) = get_tmpl(&ShellFlag::Bash);
+ assert_eq!(ext, ".sh");
+ assert!(!tmpl.is_empty(), "bash template should not be empty");
+ }
+
+ #[test]
+ fn get_tmpl_zsh() {
+ let (tmpl, ext) = get_tmpl(&ShellFlag::Zsh);
+ assert_eq!(ext, ".zsh");
+ assert!(!tmpl.is_empty(), "zsh template should not be empty");
+ }
+
+ #[test]
+ fn get_tmpl_fish() {
+ let (tmpl, ext) = get_tmpl(&ShellFlag::Fish);
+ assert_eq!(ext, ".fish");
+ assert!(!tmpl.is_empty(), "fish template should not be empty");
+ }
+
+ #[test]
+ fn get_tmpl_powershell() {
+ let (tmpl, ext) = get_tmpl(&ShellFlag::Powershell);
+ assert_eq!(ext, ".ps1");
+ assert!(!tmpl.is_empty(), "powershell template should not be empty");
+ }
+
+ #[test]
+ fn get_tmpl_other() {
+ let (tmpl, ext) = get_tmpl(&ShellFlag::Other("custom".to_string()));
+ assert_eq!(ext, ".sh");
+ assert!(!tmpl.is_empty(), "fallback template should not be empty");
+ }
+}
diff --git a/mingling_core/src/comp.rs b/mingling_core/src/comp.rs
index bf7ab13..f6fecd1 100644
--- a/mingling_core/src/comp.rs
+++ b/mingling_core/src/comp.rs
@@ -371,3 +371,13 @@ fn trace_ctx(ctx: &ShellContext) {
trace!("shell_flag={:?}", ctx.shell_flag);
trace!("=== SHELL CTX END ===");
}
+
+#[cfg(test)]
+mod tests {
+ use super::COMPLETION_SUBCOMMAND;
+
+ #[test]
+ fn completion_subcommand_constant() {
+ assert_eq!(COMPLETION_SUBCOMMAND, "__comp");
+ }
+}
diff --git a/mingling_core/src/comp/comp_ctx.rs b/mingling_core/src/comp/comp_ctx.rs
index 02e79c5..b9f9020 100644
--- a/mingling_core/src/comp/comp_ctx.rs
+++ b/mingling_core/src/comp/comp_ctx.rs
@@ -17,3 +17,83 @@ where
.is_some_and(|arg| arg == COMPLETION_SUBCOMMAND)
}
}
+
+#[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!()
+ }
+ }
+
+ #[test]
+ fn test_is_completing_with_comp_subcommand() {
+ let program: Program<MockCollect> =
+ Program::new_with_args(["program", "__comp", "some", "args"]);
+ assert!(program.is_completing());
+ }
+
+ #[test]
+ fn test_is_completing_with_normal_subcommand() {
+ let program: Program<MockCollect> = Program::new_with_args(["program", "normal", "cmd"]);
+ assert!(!program.is_completing());
+ }
+
+ #[test]
+ fn test_is_completing_with_no_args() {
+ let program: Program<MockCollect> = Program::new_with_args(["program"]);
+ assert!(!program.is_completing());
+ }
+}
diff --git a/mingling_core/src/comp/shell_ctx.rs b/mingling_core/src/comp/shell_ctx.rs
index 35758e9..9d84aa7 100644
--- a/mingling_core/src/comp/shell_ctx.rs
+++ b/mingling_core/src/comp/shell_ctx.rs
@@ -237,6 +237,7 @@ impl ShellContext {
#[cfg(test)]
mod tests {
use super::*;
+ use crate::SuggestItem;
#[test]
fn test_try_from_full_args() {
@@ -315,4 +316,128 @@ mod tests {
let context = ShellContext::try_from(args).unwrap();
assert_eq!(context.all_words, vec!["cmd", "arg1", "arg2"]);
}
+
+ #[test]
+ fn test_filling_argument_first_true() {
+ let ctx = ShellContext {
+ previous_word: "--flag".to_string(),
+ current_word: "".to_string(),
+ all_words: vec!["cmd".to_string(), "--flag".to_string()],
+ ..Default::default()
+ };
+ assert!(ctx.filling_argument_first(&["--flag", "-f"][..]));
+ }
+
+ #[test]
+ fn test_filling_argument_first_false() {
+ let ctx = ShellContext {
+ previous_word: "--flag".to_string(),
+ current_word: "".to_string(),
+ all_words: vec![
+ "cmd".to_string(),
+ "--flag".to_string(),
+ "--flag".to_string(),
+ ],
+ ..Default::default()
+ };
+ assert!(!ctx.filling_argument_first(&["--flag", "-f"][..]));
+ }
+
+ #[test]
+ fn test_filling_argument_matches() {
+ let ctx = ShellContext {
+ previous_word: "--flag".to_string(),
+ current_word: "".to_string(),
+ all_words: vec!["cmd".to_string(), "--flag".to_string()],
+ ..Default::default()
+ };
+ assert!(ctx.filling_argument(&["--flag", "-f"][..]));
+ }
+
+ #[test]
+ fn test_filling_argument_no_match() {
+ let ctx = ShellContext {
+ previous_word: "other".to_string(),
+ current_word: "".to_string(),
+ all_words: vec!["cmd".to_string(), "other".to_string()],
+ ..Default::default()
+ };
+ assert!(!ctx.filling_argument(&["--flag", "-f"][..]));
+ }
+
+ #[test]
+ fn test_typing_argument_starts_with_dash() {
+ // On Windows typing_argument checks current_word.is_empty()
+ // On other platforms it checks current_word.starts_with("-")
+ let current_word = if cfg!(target_os = "windows") {
+ "".to_string()
+ } else {
+ "--verbose".to_string()
+ };
+ let ctx = ShellContext {
+ previous_word: "".to_string(),
+ current_word,
+ all_words: vec!["cmd".to_string(), "--verbose".to_string()],
+ ..Default::default()
+ };
+ assert!(ctx.typing_argument());
+ }
+
+ #[test]
+ fn test_typing_argument_no_dash() {
+ let ctx = ShellContext {
+ previous_word: "".to_string(),
+ current_word: "somefile".to_string(),
+ all_words: vec!["cmd".to_string(), "somefile".to_string()],
+ ..Default::default()
+ };
+ assert!(!ctx.typing_argument());
+ }
+
+ #[test]
+ fn test_strip_typed_argument_suggest() {
+ let ctx = ShellContext {
+ all_words: vec!["--flag".to_string()],
+ ..Default::default()
+ };
+ let suggest: Suggest = vec!["--flag", "--other"].into();
+ let stripped = ctx.strip_typed_argument(suggest);
+ match stripped {
+ Suggest::Suggest(set) => {
+ assert_eq!(set.len(), 1);
+ assert!(set.contains(&SuggestItem::new("--other".to_string())));
+ }
+ Suggest::FileCompletion => panic!("expected Suggest variant"),
+ }
+ }
+
+ #[test]
+ fn test_strip_typed_argument_file_completion() {
+ let ctx = ShellContext {
+ all_words: vec!["--flag".to_string()],
+ ..Default::default()
+ };
+ let stripped = ctx.strip_typed_argument(Suggest::FileCompletion);
+ assert_eq!(stripped, Suggest::FileCompletion);
+ }
+
+ #[test]
+ fn test_get_typed_arguments() {
+ let ctx = ShellContext {
+ previous_word: "".to_string(),
+ current_word: "".to_string(),
+ all_words: vec![
+ "cmd".to_string(),
+ "--flag".to_string(),
+ "--other".to_string(),
+ "file.txt".to_string(),
+ ],
+ ..Default::default()
+ };
+ let typed = ctx.get_typed_arguments();
+ let expected: HashSet<String> = vec!["--flag".to_string(), "--other".to_string()]
+ .into_iter()
+ .collect();
+ assert_eq!(typed, expected);
+ }
}
diff --git a/mingling_core/src/comp/suggest.rs b/mingling_core/src/comp/suggest.rs
index cd025a4..03842e1 100644
--- a/mingling_core/src/comp/suggest.rs
+++ b/mingling_core/src/comp/suggest.rs
@@ -183,3 +183,266 @@ impl From<(String, String)> for SuggestItem {
Self::new_with_desc(suggest, description)
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_suggest_new_creates_empty() {
+ let s = Suggest::new();
+ match s {
+ Suggest::Suggest(set) => assert!(set.is_empty(), "expected empty BTreeSet"),
+ Suggest::FileCompletion => panic!("expected Suggest variant"),
+ }
+ }
+
+ #[test]
+ fn test_suggest_file_comp() {
+ assert_eq!(Suggest::file_comp(), Suggest::FileCompletion);
+ }
+
+ #[test]
+ fn test_from_vec_string() {
+ let items = vec!["foo".to_string(), "bar".to_string()];
+ let suggest: Suggest = items.into();
+ match suggest {
+ Suggest::Suggest(set) => {
+ assert_eq!(set.len(), 2);
+ assert!(set.contains(&SuggestItem::new("foo".to_string())));
+ assert!(set.contains(&SuggestItem::new("bar".to_string())));
+ }
+ Suggest::FileCompletion => panic!("expected Suggest variant"),
+ }
+ }
+
+ #[test]
+ fn test_from_vec_str_ref() {
+ let items = vec!["a", "b", "c"];
+ let suggest: Suggest = items.into();
+ match suggest {
+ Suggest::Suggest(set) => {
+ assert_eq!(set.len(), 3);
+ }
+ Suggest::FileCompletion => panic!("expected Suggest variant"),
+ }
+ }
+
+ #[test]
+ fn test_from_array_str_ref() {
+ let items = ["x", "y", "z"];
+ let suggest: Suggest = items.into();
+ match suggest {
+ Suggest::Suggest(set) => {
+ assert_eq!(set.len(), 3);
+ }
+ Suggest::FileCompletion => panic!("expected Suggest variant"),
+ }
+ }
+
+ #[test]
+ fn test_deref_suggest() {
+ let s: Suggest = ["hello"].into();
+ let set: &BTreeSet<SuggestItem> = &*s;
+ assert_eq!(set.len(), 1);
+ }
+
+ #[test]
+ #[should_panic(expected = "Cannot deref FileCompletion variant")]
+ fn test_deref_file_completion_panics() {
+ let s = Suggest::FileCompletion;
+ let _ = &*s;
+ }
+
+ #[test]
+ fn test_deref_mut_suggest() {
+ let mut s = Suggest::Suggest(BTreeSet::new());
+ s.insert(SuggestItem::new("inserted".to_string()));
+ assert_eq!(s.len(), 1);
+ }
+
+ #[test]
+ #[should_panic(expected = "Cannot deref_mut FileCompletion variant")]
+ fn test_deref_mut_file_completion_panics() {
+ let mut s = Suggest::FileCompletion;
+ let _ = &mut *s;
+ }
+
+ #[test]
+ fn test_suggest_item_new() {
+ let item = SuggestItem::new("hello".to_string());
+ assert!(matches!(item, SuggestItem::Simple(ref s) if s == "hello"));
+ }
+
+ #[test]
+ fn test_suggest_item_new_with_desc() {
+ let item = SuggestItem::new_with_desc("hello".to_string(), "desc".to_string());
+ assert!(
+ matches!(item, SuggestItem::WithDescription(ref s, ref d) if s == "hello" && d == "desc")
+ );
+ }
+
+ #[test]
+ fn test_with_desc_replaces_existing() {
+ let item = SuggestItem::new_with_desc("foo".to_string(), "old".to_string())
+ .with_desc("new".to_string());
+ assert_eq!(item.description(), Some(&"new".to_string()));
+ }
+
+ #[test]
+ fn test_with_desc_on_simple() {
+ let item = SuggestItem::new("foo".to_string()).with_desc("added".to_string());
+ assert_eq!(item.description(), Some(&"added".to_string()));
+ }
+
+ #[test]
+ fn test_suggest_returns_text() {
+ let simple = SuggestItem::new("simple".to_string());
+ let desc = SuggestItem::new_with_desc("desc".to_string(), "d".to_string());
+ assert_eq!(simple.suggest(), &"simple".to_string());
+ assert_eq!(desc.suggest(), &"desc".to_string());
+ }
+
+ #[test]
+ fn test_description() {
+ let simple = SuggestItem::new("x".to_string());
+ assert_eq!(simple.description(), None);
+
+ let desc = SuggestItem::new_with_desc("x".to_string(), "y".to_string());
+ assert_eq!(desc.description(), Some(&"y".to_string()));
+ }
+
+ #[test]
+ fn test_set_suggest() {
+ let mut item = SuggestItem::new("old".to_string());
+ item.set_suggest("new".to_string());
+ assert_eq!(item.suggest(), &"new".to_string());
+
+ let mut item = SuggestItem::new_with_desc("old".to_string(), "d".to_string());
+ item.set_suggest("newer".to_string());
+ assert_eq!(item.suggest(), &"newer".to_string());
+ }
+
+ #[test]
+ fn test_set_description_on_simple() {
+ let mut item = SuggestItem::new("text".to_string());
+ item.set_description("added".to_string());
+ assert_eq!(item.description(), Some(&"added".to_string()));
+ }
+
+ #[test]
+ fn test_set_description_replaces_existing() {
+ let mut item = SuggestItem::new_with_desc("text".to_string(), "old".to_string());
+ item.set_description("new".to_string());
+ assert_eq!(item.description(), Some(&"new".to_string()));
+ }
+
+ #[test]
+ fn test_remove_desc_on_simple() {
+ let mut item = SuggestItem::new("text".to_string());
+ assert_eq!(item.remove_desc(), None);
+ assert!(matches!(item, SuggestItem::Simple(_)));
+ }
+
+ #[test]
+ fn test_remove_desc_on_with_description() {
+ let mut item = SuggestItem::new_with_desc("text".to_string(), "desc".to_string());
+ let desc = item.remove_desc();
+ assert_eq!(desc, Some("desc".to_string()));
+ assert!(matches!(item, SuggestItem::Simple(ref s) if s == "text"));
+ }
+
+ #[test]
+ fn test_ord_by_suggest_text() {
+ let mut items = vec![
+ SuggestItem::new("z".to_string()),
+ SuggestItem::new("a".to_string()),
+ SuggestItem::new("m".to_string()),
+ ];
+ items.sort();
+ assert_eq!(items[0].suggest(), &"a".to_string());
+ assert_eq!(items[1].suggest(), &"m".to_string());
+ assert_eq!(items[2].suggest(), &"z".to_string());
+ }
+
+ #[test]
+ fn test_ord_with_description() {
+ let mut items = vec![
+ SuggestItem::new_with_desc("z".to_string(), "zzz".to_string()),
+ SuggestItem::new("a".to_string()),
+ SuggestItem::new_with_desc("m".to_string(), "mmm".to_string()),
+ ];
+ items.sort();
+ assert_eq!(items[0].suggest(), &"a".to_string());
+ assert_eq!(items[1].suggest(), &"m".to_string());
+ assert_eq!(items[2].suggest(), &"z".to_string());
+ }
+
+ #[test]
+ fn test_from_string_for_suggest_item() {
+ let item: SuggestItem = "test".to_string().into();
+ assert!(matches!(item, SuggestItem::Simple(ref s) if s == "test"));
+ }
+
+ #[test]
+ fn test_from_tuple_for_suggest_item() {
+ let item: SuggestItem = ("key".to_string(), "val".to_string()).into();
+ assert!(
+ matches!(item, SuggestItem::WithDescription(ref s, ref d) if s == "key" && d == "val")
+ );
+ }
+
+ #[test]
+ fn test_default_suggest_item() {
+ let item = SuggestItem::default();
+ assert!(matches!(item, SuggestItem::Simple(ref s) if s.is_empty()));
+ }
+
+ #[test]
+ fn test_strip_typed_argument_removes_typed() {
+ let ctx = ShellContext {
+ all_words: vec!["--verbose".to_string(), "--help".to_string()],
+ ..ShellContext::default()
+ };
+
+ let suggest: Suggest = vec!["--verbose", "--output", "--help"].into();
+ let stripped = suggest.strip_typed_argument(&ctx);
+
+ match stripped {
+ Suggest::Suggest(set) => {
+ assert_eq!(set.len(), 1);
+ assert!(set.contains(&SuggestItem::new("--output".to_string())));
+ }
+ Suggest::FileCompletion => panic!("expected Suggest variant"),
+ }
+ }
+
+ #[test]
+ fn test_strip_typed_argument_passes_file_completion() {
+ let ctx = ShellContext {
+ all_words: vec!["--verbose".to_string()],
+ ..ShellContext::default()
+ };
+
+ let stripped = Suggest::FileCompletion.strip_typed_argument(&ctx);
+ assert_eq!(stripped, Suggest::FileCompletion);
+ }
+
+ #[test]
+ fn test_strip_typed_argument_keeps_untyped() {
+ let ctx = ShellContext {
+ all_words: vec!["--verbose".to_string()],
+ ..ShellContext::default()
+ };
+
+ let suggest: Suggest = vec!["--output", "--help"].into();
+ let stripped = suggest.strip_typed_argument(&ctx);
+
+ match stripped {
+ Suggest::Suggest(set) => {
+ assert_eq!(set.len(), 2);
+ }
+ Suggest::FileCompletion => panic!("expected Suggest variant"),
+ }
+ }
+}
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());
+ }
+}
diff --git a/mingling_core/src/renderer/general.rs b/mingling_core/src/renderer/general.rs
index 0ea82c1..1a9647b 100644
--- a/mingling_core/src/renderer/general.rs
+++ b/mingling_core/src/renderer/general.rs
@@ -142,3 +142,115 @@ impl GeneralRenderer {
Ok(())
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::RenderResult;
+ use serde::Serialize;
+
+ #[derive(Debug, Clone, PartialEq, Serialize)]
+ struct TestData {
+ name: String,
+ value: i32,
+ }
+
+ fn test_data() -> TestData {
+ TestData {
+ name: "hello".into(),
+ value: 42,
+ }
+ }
+
+ #[test]
+ fn test_render_disable_does_nothing() {
+ let mut r = RenderResult::default();
+ let result =
+ GeneralRenderer::render(&test_data(), &GeneralRendererSetting::Disable, &mut r);
+ assert!(result.is_ok());
+ assert!(r.is_empty());
+ }
+
+ #[cfg(feature = "json_serde_fmt")]
+ #[test]
+ fn test_render_to_json() {
+ let mut r = RenderResult::default();
+ let result = GeneralRenderer::render_to_json(&test_data(), &mut r);
+ assert!(result.is_ok());
+ assert!(!r.is_empty());
+ let output: String = r.into();
+ assert!(output.contains("\"name\""));
+ assert!(output.contains("\"hello\""));
+ assert!(output.contains("\"value\""));
+ assert!(output.contains("42"));
+ }
+
+ #[cfg(feature = "json_serde_fmt")]
+ #[test]
+ fn test_render_to_json_pretty() {
+ let mut r = RenderResult::default();
+ let result = GeneralRenderer::render_to_json_pretty(&test_data(), &mut r);
+ assert!(result.is_ok());
+ let output: String = r.into();
+ // Pretty JSON has newlines
+ assert!(output.contains('\n'));
+ }
+
+ #[cfg(feature = "yaml_serde_fmt")]
+ #[test]
+ fn test_render_to_yaml() {
+ let mut r = RenderResult::default();
+ let result = GeneralRenderer::render_to_yaml(&test_data(), &mut r);
+ assert!(result.is_ok());
+ assert!(!r.is_empty());
+ }
+
+ #[cfg(feature = "toml_serde_fmt")]
+ #[test]
+ fn test_render_to_toml() {
+ let mut r = RenderResult::default();
+ let result = GeneralRenderer::render_to_toml(&test_data(), &mut r);
+ assert!(result.is_ok());
+ assert!(!r.is_empty());
+ }
+
+ #[cfg(feature = "ron_serde_fmt")]
+ #[test]
+ fn test_render_to_ron() {
+ let mut r = RenderResult::default();
+ let result = GeneralRenderer::render_to_ron(&test_data(), &mut r);
+ assert!(result.is_ok());
+ assert!(!r.is_empty());
+ }
+
+ #[cfg(feature = "ron_serde_fmt")]
+ #[test]
+ fn test_render_to_ron_pretty() {
+ let mut r = RenderResult::default();
+ let result = GeneralRenderer::render_to_ron_pretty(&test_data(), &mut r);
+ assert!(result.is_ok());
+ let output: String = r.into();
+ assert!(output.contains('\n'));
+ }
+
+ #[test]
+ fn test_render_dispatches_correct_format() {
+ // Test that render dispatches to the right format handler
+ let mut r = RenderResult::default();
+
+ // Disable
+ let result =
+ GeneralRenderer::render(&test_data(), &GeneralRendererSetting::Disable, &mut r);
+ assert!(result.is_ok());
+ assert!(r.is_empty());
+ }
+
+ #[cfg(feature = "json_serde_fmt")]
+ #[test]
+ fn test_render_dispatches_json() {
+ let mut r = RenderResult::default();
+ let result = GeneralRenderer::render(&test_data(), &GeneralRendererSetting::Json, &mut r);
+ assert!(result.is_ok());
+ assert!(!r.is_empty());
+ }
+}
diff --git a/mingling_core/src/renderer/general/error.rs b/mingling_core/src/renderer/general/error.rs
index eb76a8b..8c89266 100644
--- a/mingling_core/src/renderer/general/error.rs
+++ b/mingling_core/src/renderer/general/error.rs
@@ -34,3 +34,35 @@ impl From<GeneralRendererSerializeError> for String {
val.error
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn new_creates_error_with_message() {
+ let msg = "serialization failed".to_string();
+ let err = GeneralRendererSerializeError::new(msg.clone());
+ assert_eq!(err.error, msg);
+ }
+
+ #[test]
+ fn from_str_creates_error_from_string_slice() {
+ let err: GeneralRendererSerializeError = "oops".into();
+ assert_eq!(err.error, "oops");
+ }
+
+ #[test]
+ fn deref_accesses_inner_error_string() {
+ let err = GeneralRendererSerializeError::new("inner message".to_string());
+ let derefed: &String = &*err;
+ assert_eq!(derefed, "inner message");
+ }
+
+ #[test]
+ fn into_string_extracts_message() {
+ let err = GeneralRendererSerializeError::new("extract me".to_string());
+ let s: String = err.into();
+ assert_eq!(s, "extract me");
+ }
+}
diff --git a/mingling_core/src/renderer/render_result.rs b/mingling_core/src/renderer/render_result.rs
index 3eb8929..5ef3120 100644
--- a/mingling_core/src/renderer/render_result.rs
+++ b/mingling_core/src/renderer/render_result.rs
@@ -105,3 +105,97 @@ impl RenderResult {
self.render_text.clear();
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::io::Write as IoWrite;
+
+ #[test]
+ fn default_creates_empty_text_with_exit_code_zero() {
+ let result = RenderResult::default();
+ assert!(result.is_empty());
+ assert_eq!(result.exit_code, 0);
+ }
+
+ #[test]
+ fn print_appends_text() {
+ let mut result = RenderResult::default();
+ result.print("Hello");
+ assert_eq!(result.deref(), "Hello");
+ }
+
+ #[test]
+ fn println_appends_text_with_newline() {
+ let mut result = RenderResult::default();
+ result.println("Hello");
+ assert_eq!(result.deref(), "Hello\n");
+ }
+
+ #[test]
+ fn clear_empties_content() {
+ let mut result = RenderResult::default();
+ result.print("something");
+ assert!(!result.is_empty());
+ result.clear();
+ assert!(result.is_empty());
+ }
+
+ #[test]
+ fn is_empty_returns_true_for_new_false_after_print() {
+ let mut result = RenderResult::default();
+ assert!(result.is_empty());
+ result.print("x");
+ assert!(!result.is_empty());
+ }
+
+ #[test]
+ fn write_appends_utf8_bytes() {
+ let mut result = RenderResult::default();
+ let n = IoWrite::write(&mut result, b"hello").unwrap();
+ assert_eq!(n, 5);
+ assert_eq!(result.deref(), "hello");
+ }
+
+ #[test]
+ fn write_with_invalid_utf8_returns_error() {
+ let mut result = RenderResult::default();
+ let err = IoWrite::write(&mut result, &[0xff, 0xfe]).unwrap_err();
+ assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
+ }
+
+ #[test]
+ fn display_trims_trailing_whitespace() {
+ let mut result = RenderResult::default();
+ result.print(" hello world \n");
+ let formatted = format!("{}", result);
+ assert_eq!(formatted, "hello world\n");
+ }
+
+ #[test]
+ fn deref_exposes_inner_text_as_str() {
+ let mut result = RenderResult::default();
+ result.print("test");
+
+ let s: &str = &result;
+ assert_eq!(s, "test");
+ }
+
+ #[test]
+ fn from_render_result_into_string_consumes() {
+ let mut result = RenderResult::default();
+ result.print("content");
+ let s: String = result.into();
+ assert_eq!(s, "content");
+ }
+
+ #[test]
+ fn from_ref_render_result_into_string_clones() {
+ let mut result = RenderResult::default();
+ result.print("content");
+ let s: String = String::from(&result);
+ assert_eq!(s, "content");
+ // original is still usable
+ assert!(!result.is_empty());
+ }
+}