aboutsummaryrefslogtreecommitdiff
path: root/mingling_core/src/asset
diff options
context:
space:
mode:
authorWeicao-CatilGrass <1992414357@qq.com>2026-06-09 21:08:20 +0800
committerWeicao-CatilGrass <1992414357@qq.com>2026-06-09 22:23:16 +0800
commit514929c3b8ee0d4f540be5eb4bc8c1a10e62095d (patch)
tree8faeeb71075a695354496af38eb527085bb37f92 /mingling_core/src/asset
parent92cccd9517e764508dfa0342ae2ea254661d0a8f (diff)
Add unit and integration tests for mingling_core
Diffstat (limited to 'mingling_core/src/asset')
-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
5 files changed, 716 insertions, 0 deletions
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");
+ }
+}