diff options
| author | Weicao-CatilGrass <1992414357@qq.com> | 2026-06-09 21:08:20 +0800 |
|---|---|---|
| committer | Weicao-CatilGrass <1992414357@qq.com> | 2026-06-09 22:23:16 +0800 |
| commit | 514929c3b8ee0d4f540be5eb4bc8c1a10e62095d (patch) | |
| tree | 8faeeb71075a695354496af38eb527085bb37f92 /mingling_core/src/asset | |
| parent | 92cccd9517e764508dfa0342ae2ea254661d0a8f (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.rs | 83 | ||||
| -rw-r--r-- | mingling_core/src/asset/dispatcher.rs | 148 | ||||
| -rw-r--r-- | mingling_core/src/asset/global_resource.rs | 66 | ||||
| -rw-r--r-- | mingling_core/src/asset/lazy_resource.rs | 345 | ||||
| -rw-r--r-- | mingling_core/src/asset/node.rs | 74 |
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"); + } +} |
