aboutsummaryrefslogtreecommitdiff
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
parent92cccd9517e764508dfa0342ae2ea254661d0a8f (diff)
Add unit and integration tests for mingling_core
-rw-r--r--CHANGELOG.md37
-rw-r--r--Cargo.toml12
-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
-rw-r--r--mingling_core/tests/test-all/Cargo.lock465
-rw-r--r--mingling_core/tests/test-all/Cargo.toml20
-rw-r--r--mingling_core/tests/test-all/tests/integration.rs235
-rw-r--r--mingling_core/tests/test-basic/Cargo.lock76
-rw-r--r--mingling_core/tests/test-basic/Cargo.toml10
-rw-r--r--mingling_core/tests/test-basic/tests/integration.rs93
-rw-r--r--mingling_core/tests/test-comp/Cargo.lock86
-rw-r--r--mingling_core/tests/test-comp/Cargo.toml10
-rw-r--r--mingling_core/tests/test-comp/tests/integration.rs123
-rw-r--r--mingling_core/tests/test-dispatch-tree/Cargo.lock76
-rw-r--r--mingling_core/tests/test-dispatch-tree/Cargo.toml10
-rw-r--r--mingling_core/tests/test-dispatch-tree/tests/integration.rs93
-rw-r--r--mingling_core/tests/test-general-renderer/Cargo.lock287
-rw-r--r--mingling_core/tests/test-general-renderer/Cargo.toml11
-rw-r--r--mingling_core/tests/test-general-renderer/tests/integration.rs77
-rw-r--r--mingling_core/tests/test-repl/Cargo.lock76
-rw-r--r--mingling_core/tests/test-repl/Cargo.toml10
-rw-r--r--mingling_core/tests/test-repl/tests/integration.rs63
41 files changed, 4410 insertions, 0 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b8e7cf7..ca57668 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,43 @@
### Release 0.2.0 (Unreleased)
+#### Tests:
+
+1. **\[core\] Added complete unit test coverage**, adding `#[cfg(test)]` test modules for 23 modules in `mingling_core` that previously lacked tests, covering:
+
+ - **Core types** (`any.rs`): `AnyOutput` creation, downcast, type judgment, route routing, restore deserialization; `ChainProcess` type conversion; `NextProcess` formatting
+ - **Dispatcher** (`dispatcher.rs`): Conversion of `Dispatchers` from 1~7 tuples, Vec, Box; Deref dereferencing; clone behavior
+ - **Node** (`node.rs`): Construction, join, kebab-case conversion, equality comparison, sorting
+ - **Global resource** (`global_resource.rs`): `GlobalResource` new, Deref, AsRef; three default implementations of `ResourceMarker`
+ - **Lazy resource** (`lazy_resource.rs`): Coverage of all 18+ methods of `LazyRes`, including initialization triggering, get_ref/get_mut/get_clone, into_inner/unwrap, Drop callback, `ResourceMarker` integration
+ - **Error types** (`chain/error.rs`, `program/error.rs`): All Display, Error source, From conversions
+ - **Configuration structs** (`config.rs`): Default values for `ProgramStdoutSetting`, `ProgramUserContext`; FromStr parsing and Display output of `GeneralRendererSetting` (feature-gated)
+ - **Flag system** (`flag.rs`): Added 8 From conversions, Deref, AsRef for `Flag`
+ - **String wrapper** (`string_vec.rs`): 6 From conversions, Deref, Into\<Vec\>
+ - **Render result** (`render_result.rs`): print/println/clear/is_empty, Write trait, Display, Deref, From conversions
+ - **Render error** (`general/error.rs`): Construction, From, Deref, Into\<String\>
+ - **General renderer** (`general.rs`): Rendering in Disable/JSON/YAML/TOML/RON formats (feature-gated)
+ - **Completion suggestions** (`suggest.rs`): All construction, access, modification, sorting, and conversion methods for `Suggest` and `SuggestItem`
+ - **Shell context** (`shell_ctx.rs`): Added `filling_argument`, `filling_argument_first`, `typing_argument`, `strip_typed_argument`, `get_typed_arguments`
+ - **Hook system** (`hook.rs`): `ProgramHook::empty` and all 8 builder methods
+ - **Singleton management** (`single_instance.rs`): `ProgramCell` set/get_raw/take/double-set-panic
+ - **Program setup** (`setup.rs`): Verification of `with_setup` invocation
+ - **Completion detection** (`comp_ctx.rs`): Three scenarios for `is_completing`
+ - **Build script** (`builds/comp.rs`): `get_tmpl` for four Shells and Other fallback
+
+2. **\[core\] Added 6 integration test crates**, testing public APIs under different feature combinations:
+
+ - `test-basic`: Basic type tests with default features (Node, Flag, RenderResult, NextProcess, StringVec)
+ - `test-comp`: ShellContext, Suggest, SuggestItem, is_completing with `comp + builds` features
+ - `test-general-renderer`: GeneralRenderer output in various formats with `general_renderer_full + parser` features
+ - `test-repl`: ResREPL and basic types with `repl + extra_macros` features
+ - `test-dispatch-tree`: Basic types with `dispatch_tree` feature
+ - `test-all`: Comprehensive testing with all feature combinations (ShellContext, Suggest, ResREPL, GeneralRenderer, Hooks, basic types, etc.)
+
+ These crates are located in `mingling_core/tests/test-*/`, each marked as an independent workspace via `[workspace]`, isolated from the main workspace.
+
+3. **\[workspace\] Added workspace exclude rules for the 6 test crates in the root `Cargo.toml`**, ensuring that integration test crates are not captured by the workspace's implicit member rules.
+
#### Fixes:
1. **\[core:comp\]** Fixed `default_completion` incorrectly handling multi-level subcommand suggestions when the cursor is after a trailing space. `all_words.get(1..word_index)` could go out of bounds because Zsh's `$CURRENT` (`word_index`) may exceed `all_words.len()` when trailing whitespace is present. The range end is now capped with `.min(all_words.len())`
diff --git a/Cargo.toml b/Cargo.toml
index 5b7726f..47cbe6c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -2,6 +2,7 @@
resolver = "2"
members = ["mingling", "mingling_core", "mingling_macros", "mling"]
exclude = [
+ # Examples
"examples/example-argument-parse",
"examples/example-async-support",
"examples/example-basic",
@@ -23,7 +24,18 @@ exclude = [
"examples/example-setup",
"examples/example-unit-test",
"examples/full-todolist",
+
+ # Dev Tools
"dev_tools",
+
+ # Tests
+ "mingling_core/tests/test-basic",
+ "mingling_core/tests/test-comp",
+ "mingling_core/tests/test-general-renderer",
+ "mingling_core/tests/test-repl",
+ "mingling_core/tests/test-dispatch-tree",
+ "mingling_core/tests/test-async",
+ "mingling_core/tests/test-all",
]
[workspace.dependencies]
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());
+ }
+}
diff --git a/mingling_core/tests/test-all/Cargo.lock b/mingling_core/tests/test-all/Cargo.lock
new file mode 100644
index 0000000..9fe2a28
--- /dev/null
+++ b/mingling_core/tests/test-all/Cargo.lock
@@ -0,0 +1,465 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "bitflags"
+version = "2.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "bytes"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+
+[[package]]
+name = "indexmap"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "just_fmt"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e"
+
+[[package]]
+name = "just_template"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db3edb658c34b10b69c4b3b58f7ba989cd09c82c0621dee1eef51843c2327225"
+dependencies = [
+ "just_fmt",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.186"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
+
+[[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "memchr"
+version = "2.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
+
+[[package]]
+name = "mingling"
+version = "0.2.0"
+dependencies = [
+ "mingling_core",
+ "mingling_macros",
+ "serde",
+ "size",
+]
+
+[[package]]
+name = "mingling_core"
+version = "0.2.0"
+dependencies = [
+ "just_fmt",
+ "just_template",
+ "ron",
+ "serde",
+ "serde_json",
+ "serde_yaml",
+ "toml",
+]
+
+[[package]]
+name = "mingling_macros"
+version = "0.2.0"
+dependencies = [
+ "just_fmt",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "mio"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-link",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "ron"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4147b952f3f819eca0e99527022f7d6a8d05f111aeb0a62960c74eb283bec8fc"
+dependencies = [
+ "bitflags",
+ "once_cell",
+ "serde",
+ "serde_derive",
+ "typeid",
+ "unicode-ident",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.150"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "serde_yaml"
+version = "0.9.34+deprecated"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
+dependencies = [
+ "indexmap",
+ "itoa",
+ "ryu",
+ "serde",
+ "unsafe-libyaml",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
+dependencies = [
+ "errno",
+ "libc",
+]
+
+[[package]]
+name = "size"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b6709c7b6754dca1311b3c73e79fcce40dd414c782c66d88e8823030093b02b"
+
+[[package]]
+name = "smallvec"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+
+[[package]]
+name = "socket2"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
+dependencies = [
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "test-all"
+version = "0.1.0"
+dependencies = [
+ "mingling",
+ "serde",
+ "tokio",
+]
+
+[[package]]
+name = "tokio"
+version = "1.52.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
+dependencies = [
+ "bytes",
+ "libc",
+ "mio",
+ "parking_lot",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "socket2",
+ "tokio-macros",
+ "windows-sys",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "toml"
+version = "1.1.2+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee"
+dependencies = [
+ "indexmap",
+ "serde_core",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_parser",
+ "toml_writer",
+ "winnow",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "1.1.1+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "toml_parser"
+version = "1.1.2+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
+dependencies = [
+ "winnow",
+]
+
+[[package]]
+name = "toml_writer"
+version = "1.1.1+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db"
+
+[[package]]
+name = "typeid"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unsafe-libyaml"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "winnow"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1"
+
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
diff --git a/mingling_core/tests/test-all/Cargo.toml b/mingling_core/tests/test-all/Cargo.toml
new file mode 100644
index 0000000..9eea2de
--- /dev/null
+++ b/mingling_core/tests/test-all/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "test-all"
+version = "0.1.0"
+edition = "2024"
+publish = false
+
+[workspace]
+
+[dependencies]
+mingling = { path = "../../../mingling", features = [
+ "general_renderer_full",
+ "comp",
+ "builds",
+ "repl",
+ "dispatch_tree",
+ "parser",
+ "extra_macros",
+] }
+tokio = { version = "1", features = ["full"] }
+serde = { version = "1", features = ["derive"] }
diff --git a/mingling_core/tests/test-all/tests/integration.rs b/mingling_core/tests/test-all/tests/integration.rs
new file mode 100644
index 0000000..f910729
--- /dev/null
+++ b/mingling_core/tests/test-all/tests/integration.rs
@@ -0,0 +1,235 @@
+use mingling::Flag;
+use mingling::GeneralRenderer;
+use mingling::GeneralRendererSetting;
+use mingling::Groupped;
+use mingling::NextProcess;
+use mingling::Node;
+use mingling::Program;
+use mingling::ProgramCollect;
+use mingling::RenderResult;
+use mingling::StringVec;
+use mingling::comp::{ShellContext, ShellFlag, Suggest};
+use mingling::core_res::ResREPL;
+use mingling::hook::ProgramHook;
+use serde::Serialize;
+use std::sync::atomic::{AtomicBool, Ordering};
+
+// MockCollect for is_completing tests
+
+#[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) -> mingling::AnyOutput<MockCollect> {
+ unimplemented!()
+ }
+ fn build_dispatcher_not_found(_args: Vec<String>) -> mingling::AnyOutput<MockCollect> {
+ unimplemented!()
+ }
+ fn build_empty_result() -> mingling::AnyOutput<MockCollect> {
+ unimplemented!()
+ }
+ fn render(_any: mingling::AnyOutput<MockCollect>, _r: &mut RenderResult) {
+ unimplemented!()
+ }
+ fn render_help(_any: mingling::AnyOutput<MockCollect>, _r: &mut RenderResult) {
+ unimplemented!()
+ }
+ fn do_chain(_any: mingling::AnyOutput<MockCollect>) -> mingling::ChainProcess<MockCollect> {
+ unimplemented!()
+ }
+ fn do_comp(_any: &mingling::AnyOutput<MockCollect>, _ctx: &ShellContext) -> Suggest {
+ unimplemented!()
+ }
+ fn has_renderer(_any: &mingling::AnyOutput<MockCollect>) -> bool {
+ unimplemented!()
+ }
+ fn has_chain(_any: &mingling::AnyOutput<MockCollect>) -> bool {
+ unimplemented!()
+ }
+
+ fn dispatch_args_trie(
+ _raw: &[String],
+ ) -> Result<mingling::AnyOutput<MockCollect>, mingling::error::ProgramInternalExecuteError>
+ {
+ unimplemented!()
+ }
+
+ fn get_nodes() -> Vec<(
+ String,
+ &'static (dyn mingling::Dispatcher<MockCollect> + Send + Sync),
+ )> {
+ unimplemented!()
+ }
+
+ fn general_render(
+ _any: mingling::AnyOutput<MockCollect>,
+ _setting: &GeneralRendererSetting,
+ ) -> Result<RenderResult, mingling::error::GeneralRendererSerializeError> {
+ unimplemented!()
+ }
+}
+
+// ShellContext
+
+#[test]
+fn test_shell_context_from_args() {
+ let ctx = ShellContext::try_from(vec![
+ "-f".to_string(),
+ "app greet".to_string(),
+ "-F".to_string(),
+ "zsh".to_string(),
+ ])
+ .unwrap();
+ assert!(matches!(ctx.shell_flag, ShellFlag::Zsh));
+ assert_eq!(ctx.all_words, vec!["app", "greet"]);
+}
+
+// Suggest
+
+#[test]
+fn test_suggest_creation() {
+ let s: Suggest = vec!["--help".to_string()].into();
+ assert!(matches!(s, Suggest::Suggest(_)));
+}
+
+// ResREPL
+
+#[test]
+fn test_res_repl_default() {
+ let res = ResREPL::default();
+ assert!(!res.exit);
+}
+
+// Node
+
+#[test]
+fn test_node_creation() {
+ let node = Node::from("a.b.c");
+ assert_eq!(node.to_string(), "a.b.c");
+}
+
+#[test]
+fn test_node_kebab() {
+ let node = Node::from("HelloWorld.FooBar");
+ assert_eq!(node.to_string(), "hello-world.foo-bar");
+}
+
+// Flag
+
+#[test]
+fn test_flag_conversion() {
+ let flag = Flag::from(["-h", "--help"]);
+ assert_eq!(flag.as_ref(), &["-h", "--help"]);
+}
+
+#[test]
+fn test_flag_empty() {
+ let flag = Flag::from(());
+ assert!(flag.is_empty());
+}
+
+// RenderResult
+
+#[test]
+fn test_render_result_default() {
+ let r = RenderResult::default();
+ assert!(r.is_empty());
+ assert_eq!(r.exit_code, 0);
+}
+
+#[test]
+fn test_render_result_print() {
+ let mut r = RenderResult::default();
+ r.print("hello");
+ assert_eq!(&*r, "hello");
+}
+
+// GeneralRenderer
+
+#[derive(Debug, Clone, PartialEq, Serialize)]
+struct TestData {
+ name: String,
+ value: i32,
+}
+
+#[test]
+fn test_general_renderer_disable() {
+ let data = TestData {
+ name: "test".into(),
+ value: 42,
+ };
+ let mut r = RenderResult::default();
+ let result = GeneralRenderer::render(&data, &GeneralRendererSetting::Disable, &mut r);
+ assert!(result.is_ok());
+ assert!(r.is_empty());
+}
+
+#[test]
+fn test_general_renderer_json() {
+ let data = TestData {
+ name: "test".into(),
+ value: 42,
+ };
+ let mut r = RenderResult::default();
+ let result = GeneralRenderer::render(&data, &GeneralRendererSetting::Json, &mut r);
+ assert!(result.is_ok());
+ assert!(!r.is_empty());
+}
+
+// is_completing
+
+#[test]
+fn test_is_completing() {
+ let program: Program<MockCollect> = Program::new_with_args(["app", "__comp"]);
+ assert!(program.is_completing());
+}
+
+#[test]
+fn test_is_not_completing() {
+ let program: Program<MockCollect> = Program::new_with_args(["app", "greet"]);
+ assert!(!program.is_completing());
+}
+
+// Hooks
+
+#[test]
+fn test_hook_setup() {
+ static CALLED: AtomicBool = AtomicBool::new(false);
+
+ let hook = ProgramHook::<MockCollect>::empty().on_begin(|| {
+ CALLED.store(true, Ordering::SeqCst);
+ });
+
+ assert!(hook.begin.is_some());
+ (hook.begin.unwrap())();
+ assert!(CALLED.load(Ordering::SeqCst));
+}
+
+// NextProcess
+
+#[test]
+fn test_next_process_display() {
+ assert_eq!(format!("{}", NextProcess::Chain), "Chain");
+ assert_eq!(format!("{}", NextProcess::Renderer), "Renderer");
+}
+
+// StringVec
+
+#[test]
+fn test_string_vec_from_array() {
+ let sv = StringVec::from(["a", "b", "c"]);
+ let v: Vec<String> = sv.into();
+ assert_eq!(v, vec!["a", "b", "c"]);
+}
diff --git a/mingling_core/tests/test-basic/Cargo.lock b/mingling_core/tests/test-basic/Cargo.lock
new file mode 100644
index 0000000..a5d9ab9
--- /dev/null
+++ b/mingling_core/tests/test-basic/Cargo.lock
@@ -0,0 +1,76 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "just_fmt"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e"
+
+[[package]]
+name = "mingling"
+version = "0.2.0"
+dependencies = [
+ "mingling_core",
+ "mingling_macros",
+]
+
+[[package]]
+name = "mingling_core"
+version = "0.2.0"
+dependencies = [
+ "just_fmt",
+]
+
+[[package]]
+name = "mingling_macros"
+version = "0.2.0"
+dependencies = [
+ "just_fmt",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "test-basic"
+version = "0.1.0"
+dependencies = [
+ "mingling",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
diff --git a/mingling_core/tests/test-basic/Cargo.toml b/mingling_core/tests/test-basic/Cargo.toml
new file mode 100644
index 0000000..7b326c1
--- /dev/null
+++ b/mingling_core/tests/test-basic/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "test-basic"
+version = "0.1.0"
+edition = "2024"
+publish = false
+
+[workspace]
+
+[dependencies]
+mingling = { path = "../../../mingling" }
diff --git a/mingling_core/tests/test-basic/tests/integration.rs b/mingling_core/tests/test-basic/tests/integration.rs
new file mode 100644
index 0000000..7cd7b8c
--- /dev/null
+++ b/mingling_core/tests/test-basic/tests/integration.rs
@@ -0,0 +1,93 @@
+use mingling::Flag;
+use mingling::NextProcess;
+use mingling::Node;
+use mingling::RenderResult;
+use mingling::StringVec;
+
+#[test]
+fn test_node_from_str() {
+ let node = Node::from("a.b.c");
+ assert_eq!(node.to_string(), "a.b.c");
+}
+
+#[test]
+fn test_node_kebab_case() {
+ let node = Node::from("HelloWorld.FooBar");
+ assert_eq!(node.to_string(), "hello-world.foo-bar");
+}
+
+#[test]
+fn test_node_join() {
+ let node = Node::from("base").join("sub");
+ assert_eq!(node.to_string(), "base.sub");
+}
+
+#[test]
+fn test_node_eq() {
+ let a = Node::from("x.y");
+ let b = Node::from("x.y");
+ let c = Node::from("x.z");
+ assert_eq!(a, b);
+ assert_ne!(a, c);
+}
+
+#[test]
+fn test_flag_from_static_str() {
+ let flag = Flag::from("-h");
+ assert_eq!(flag.as_ref(), &["-h"]);
+}
+
+#[test]
+fn test_flag_from_array() {
+ let flag = Flag::from(["-h", "--help"]);
+ assert_eq!(flag.as_ref(), &["-h", "--help"]);
+}
+
+#[test]
+fn test_flag_empty() {
+ let flag = Flag::from(());
+ assert!(flag.is_empty());
+}
+
+#[test]
+fn test_render_result_default() {
+ let r = RenderResult::default();
+ assert!(r.is_empty());
+ assert_eq!(r.exit_code, 0);
+}
+
+#[test]
+fn test_render_result_print() {
+ let mut r = RenderResult::default();
+ r.print("hello");
+ assert_eq!(&*r, "hello");
+}
+
+#[test]
+fn test_render_result_clear() {
+ let mut r = RenderResult::default();
+ r.print("data");
+ assert!(!r.is_empty());
+ r.clear();
+ assert!(r.is_empty());
+}
+
+#[test]
+fn test_next_process_display() {
+ assert_eq!(format!("{}", NextProcess::Chain), "Chain");
+ assert_eq!(format!("{}", NextProcess::Renderer), "Renderer");
+}
+
+#[test]
+fn test_string_vec_from_array() {
+ let sv = StringVec::from(["a", "b", "c"]);
+ let v: Vec<String> = sv.into();
+ assert_eq!(v, vec!["a", "b", "c"]);
+}
+
+#[test]
+fn test_string_vec_from_vec() {
+ let original = vec!["x".to_string(), "y".to_string()];
+ let sv = StringVec::from(original.clone());
+ assert_eq!(*sv, original);
+}
diff --git a/mingling_core/tests/test-comp/Cargo.lock b/mingling_core/tests/test-comp/Cargo.lock
new file mode 100644
index 0000000..ad384fe
--- /dev/null
+++ b/mingling_core/tests/test-comp/Cargo.lock
@@ -0,0 +1,86 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "just_fmt"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e"
+
+[[package]]
+name = "just_template"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db3edb658c34b10b69c4b3b58f7ba989cd09c82c0621dee1eef51843c2327225"
+dependencies = [
+ "just_fmt",
+]
+
+[[package]]
+name = "mingling"
+version = "0.2.0"
+dependencies = [
+ "mingling_core",
+ "mingling_macros",
+]
+
+[[package]]
+name = "mingling_core"
+version = "0.2.0"
+dependencies = [
+ "just_fmt",
+ "just_template",
+]
+
+[[package]]
+name = "mingling_macros"
+version = "0.2.0"
+dependencies = [
+ "just_fmt",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "test-comp"
+version = "0.1.0"
+dependencies = [
+ "mingling",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
diff --git a/mingling_core/tests/test-comp/Cargo.toml b/mingling_core/tests/test-comp/Cargo.toml
new file mode 100644
index 0000000..9ceca3e
--- /dev/null
+++ b/mingling_core/tests/test-comp/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "test-comp"
+version = "0.1.0"
+edition = "2024"
+publish = false
+
+[workspace]
+
+[dependencies]
+mingling = { path = "../../../mingling", features = ["comp", "builds"] }
diff --git a/mingling_core/tests/test-comp/tests/integration.rs b/mingling_core/tests/test-comp/tests/integration.rs
new file mode 100644
index 0000000..4e6455e
--- /dev/null
+++ b/mingling_core/tests/test-comp/tests/integration.rs
@@ -0,0 +1,123 @@
+use mingling::Groupped;
+use mingling::Program;
+use mingling::ProgramCollect;
+use mingling::RenderResult;
+use mingling::comp::{ShellContext, ShellFlag, Suggest, SuggestItem};
+
+/// 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) -> mingling::AnyOutput<MockCollect> {
+ unimplemented!()
+ }
+ fn build_dispatcher_not_found(_args: Vec<String>) -> mingling::AnyOutput<MockCollect> {
+ unimplemented!()
+ }
+ fn build_empty_result() -> mingling::AnyOutput<MockCollect> {
+ unimplemented!()
+ }
+ fn render(_any: mingling::AnyOutput<MockCollect>, _r: &mut RenderResult) {
+ unimplemented!()
+ }
+ fn render_help(_any: mingling::AnyOutput<MockCollect>, _r: &mut RenderResult) {
+ unimplemented!()
+ }
+ fn do_chain(_any: mingling::AnyOutput<MockCollect>) -> mingling::ChainProcess<MockCollect> {
+ unimplemented!()
+ }
+ fn do_comp(_any: &mingling::AnyOutput<MockCollect>, _ctx: &ShellContext) -> Suggest {
+ unimplemented!()
+ }
+ fn has_renderer(_any: &mingling::AnyOutput<MockCollect>) -> bool {
+ unimplemented!()
+ }
+ fn has_chain(_any: &mingling::AnyOutput<MockCollect>) -> bool {
+ unimplemented!()
+ }
+}
+
+#[test]
+fn test_shell_context_parsing_full() {
+ let args = vec![
+ "-f".to_string(),
+ "myapp hello ^world".to_string(),
+ "-C".to_string(),
+ "14".to_string(),
+ "-w".to_string(),
+ "hello".to_string(),
+ "-p".to_string(),
+ "myapp".to_string(),
+ "-c".to_string(),
+ "myapp".to_string(),
+ "-i".to_string(),
+ "1".to_string(),
+ "-F".to_string(),
+ "bash".to_string(),
+ ];
+ let ctx = ShellContext::try_from(args).unwrap();
+ assert_eq!(ctx.command_line, "myapp hello -world");
+ assert_eq!(ctx.cursor_position, 14);
+ assert_eq!(ctx.current_word, "hello");
+ assert_eq!(ctx.previous_word, "myapp");
+ assert_eq!(ctx.command_name, "myapp");
+ assert_eq!(ctx.word_index, 1);
+ assert!(matches!(ctx.shell_flag, ShellFlag::Bash));
+}
+
+#[test]
+fn test_shell_context_parsing_empty() {
+ let ctx = ShellContext::try_from(vec![]).unwrap();
+ assert!(ctx.all_words.is_empty());
+ assert!(matches!(ctx.shell_flag, ShellFlag::Other(_)));
+}
+
+#[test]
+fn test_suggest_from_vec() {
+ let s: Suggest = vec!["--help".to_string(), "--version".to_string()].into();
+ match &s {
+ Suggest::Suggest(items) => {
+ assert_eq!(items.len(), 2);
+ }
+ _ => panic!("expected Suggest::Suggest"),
+ }
+}
+
+#[test]
+fn test_suggest_item_new() {
+ let item = SuggestItem::new("hello".to_string());
+ assert_eq!(item.suggest(), "hello");
+ assert!(item.description().is_none());
+}
+
+#[test]
+fn test_suggest_item_with_description() {
+ let item = SuggestItem::new_with_desc("hello".to_string(), "a greeting".to_string());
+ assert_eq!(item.suggest(), "hello");
+ assert_eq!(item.description(), Some(&"a greeting".to_string()));
+}
+
+#[test]
+fn test_program_is_completing() {
+ let program: Program<MockCollect> = Program::new_with_args(["myapp", "__comp", "hello", ""]);
+ assert!(program.is_completing());
+}
+
+#[test]
+fn test_program_is_not_completing() {
+ let program: Program<MockCollect> = Program::new_with_args(["myapp", "hello"]);
+ assert!(!program.is_completing());
+}
diff --git a/mingling_core/tests/test-dispatch-tree/Cargo.lock b/mingling_core/tests/test-dispatch-tree/Cargo.lock
new file mode 100644
index 0000000..be5b922
--- /dev/null
+++ b/mingling_core/tests/test-dispatch-tree/Cargo.lock
@@ -0,0 +1,76 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "just_fmt"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e"
+
+[[package]]
+name = "mingling"
+version = "0.2.0"
+dependencies = [
+ "mingling_core",
+ "mingling_macros",
+]
+
+[[package]]
+name = "mingling_core"
+version = "0.2.0"
+dependencies = [
+ "just_fmt",
+]
+
+[[package]]
+name = "mingling_macros"
+version = "0.2.0"
+dependencies = [
+ "just_fmt",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "test-dispatch-tree"
+version = "0.1.0"
+dependencies = [
+ "mingling",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
diff --git a/mingling_core/tests/test-dispatch-tree/Cargo.toml b/mingling_core/tests/test-dispatch-tree/Cargo.toml
new file mode 100644
index 0000000..bb94292
--- /dev/null
+++ b/mingling_core/tests/test-dispatch-tree/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "test-dispatch-tree"
+version = "0.1.0"
+edition = "2024"
+publish = false
+
+[workspace]
+
+[dependencies]
+mingling = { path = "../../../mingling", features = ["dispatch_tree"] }
diff --git a/mingling_core/tests/test-dispatch-tree/tests/integration.rs b/mingling_core/tests/test-dispatch-tree/tests/integration.rs
new file mode 100644
index 0000000..7cd7b8c
--- /dev/null
+++ b/mingling_core/tests/test-dispatch-tree/tests/integration.rs
@@ -0,0 +1,93 @@
+use mingling::Flag;
+use mingling::NextProcess;
+use mingling::Node;
+use mingling::RenderResult;
+use mingling::StringVec;
+
+#[test]
+fn test_node_from_str() {
+ let node = Node::from("a.b.c");
+ assert_eq!(node.to_string(), "a.b.c");
+}
+
+#[test]
+fn test_node_kebab_case() {
+ let node = Node::from("HelloWorld.FooBar");
+ assert_eq!(node.to_string(), "hello-world.foo-bar");
+}
+
+#[test]
+fn test_node_join() {
+ let node = Node::from("base").join("sub");
+ assert_eq!(node.to_string(), "base.sub");
+}
+
+#[test]
+fn test_node_eq() {
+ let a = Node::from("x.y");
+ let b = Node::from("x.y");
+ let c = Node::from("x.z");
+ assert_eq!(a, b);
+ assert_ne!(a, c);
+}
+
+#[test]
+fn test_flag_from_static_str() {
+ let flag = Flag::from("-h");
+ assert_eq!(flag.as_ref(), &["-h"]);
+}
+
+#[test]
+fn test_flag_from_array() {
+ let flag = Flag::from(["-h", "--help"]);
+ assert_eq!(flag.as_ref(), &["-h", "--help"]);
+}
+
+#[test]
+fn test_flag_empty() {
+ let flag = Flag::from(());
+ assert!(flag.is_empty());
+}
+
+#[test]
+fn test_render_result_default() {
+ let r = RenderResult::default();
+ assert!(r.is_empty());
+ assert_eq!(r.exit_code, 0);
+}
+
+#[test]
+fn test_render_result_print() {
+ let mut r = RenderResult::default();
+ r.print("hello");
+ assert_eq!(&*r, "hello");
+}
+
+#[test]
+fn test_render_result_clear() {
+ let mut r = RenderResult::default();
+ r.print("data");
+ assert!(!r.is_empty());
+ r.clear();
+ assert!(r.is_empty());
+}
+
+#[test]
+fn test_next_process_display() {
+ assert_eq!(format!("{}", NextProcess::Chain), "Chain");
+ assert_eq!(format!("{}", NextProcess::Renderer), "Renderer");
+}
+
+#[test]
+fn test_string_vec_from_array() {
+ let sv = StringVec::from(["a", "b", "c"]);
+ let v: Vec<String> = sv.into();
+ assert_eq!(v, vec!["a", "b", "c"]);
+}
+
+#[test]
+fn test_string_vec_from_vec() {
+ let original = vec!["x".to_string(), "y".to_string()];
+ let sv = StringVec::from(original.clone());
+ assert_eq!(*sv, original);
+}
diff --git a/mingling_core/tests/test-general-renderer/Cargo.lock b/mingling_core/tests/test-general-renderer/Cargo.lock
new file mode 100644
index 0000000..7c4b628
--- /dev/null
+++ b/mingling_core/tests/test-general-renderer/Cargo.lock
@@ -0,0 +1,287 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "bitflags"
+version = "2.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "hashbrown"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+
+[[package]]
+name = "indexmap"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "just_fmt"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e"
+
+[[package]]
+name = "memchr"
+version = "2.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
+
+[[package]]
+name = "mingling"
+version = "0.2.0"
+dependencies = [
+ "mingling_core",
+ "mingling_macros",
+ "serde",
+ "size",
+]
+
+[[package]]
+name = "mingling_core"
+version = "0.2.0"
+dependencies = [
+ "just_fmt",
+ "ron",
+ "serde",
+ "serde_json",
+ "serde_yaml",
+ "toml",
+]
+
+[[package]]
+name = "mingling_macros"
+version = "0.2.0"
+dependencies = [
+ "just_fmt",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "ron"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4147b952f3f819eca0e99527022f7d6a8d05f111aeb0a62960c74eb283bec8fc"
+dependencies = [
+ "bitflags",
+ "once_cell",
+ "serde",
+ "serde_derive",
+ "typeid",
+ "unicode-ident",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.150"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "serde_yaml"
+version = "0.9.34+deprecated"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
+dependencies = [
+ "indexmap",
+ "itoa",
+ "ryu",
+ "serde",
+ "unsafe-libyaml",
+]
+
+[[package]]
+name = "size"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b6709c7b6754dca1311b3c73e79fcce40dd414c782c66d88e8823030093b02b"
+
+[[package]]
+name = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "test-general-renderer"
+version = "0.1.0"
+dependencies = [
+ "mingling",
+ "serde",
+]
+
+[[package]]
+name = "toml"
+version = "1.1.2+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee"
+dependencies = [
+ "indexmap",
+ "serde_core",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_parser",
+ "toml_writer",
+ "winnow",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "1.1.1+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "toml_parser"
+version = "1.1.2+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
+dependencies = [
+ "winnow",
+]
+
+[[package]]
+name = "toml_writer"
+version = "1.1.1+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db"
+
+[[package]]
+name = "typeid"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unsafe-libyaml"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
+
+[[package]]
+name = "winnow"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1"
+
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
diff --git a/mingling_core/tests/test-general-renderer/Cargo.toml b/mingling_core/tests/test-general-renderer/Cargo.toml
new file mode 100644
index 0000000..3d038aa
--- /dev/null
+++ b/mingling_core/tests/test-general-renderer/Cargo.toml
@@ -0,0 +1,11 @@
+[package]
+name = "test-general-renderer"
+version = "0.1.0"
+edition = "2024"
+publish = false
+
+[workspace]
+
+[dependencies]
+mingling = { path = "../../../mingling", features = ["general_renderer_full", "parser"] }
+serde = { version = "1", features = ["derive"] }
diff --git a/mingling_core/tests/test-general-renderer/tests/integration.rs b/mingling_core/tests/test-general-renderer/tests/integration.rs
new file mode 100644
index 0000000..0fcc38d
--- /dev/null
+++ b/mingling_core/tests/test-general-renderer/tests/integration.rs
@@ -0,0 +1,77 @@
+use mingling::GeneralRenderer;
+use mingling::GeneralRendererSetting;
+use mingling::RenderResult;
+use serde::Serialize;
+
+#[derive(Debug, Clone, PartialEq, Serialize)]
+struct TestData {
+ name: String,
+ value: i32,
+}
+
+fn test_data() -> TestData {
+ TestData {
+ name: "test".into(),
+ value: 42,
+ }
+}
+
+#[test]
+fn test_render_disable() {
+ let mut r = RenderResult::default();
+ let result = GeneralRenderer::render(&test_data(), &GeneralRendererSetting::Disable, &mut r);
+ assert!(result.is_ok());
+ assert!(r.is_empty());
+}
+
+#[test]
+fn test_render_json() {
+ let mut r = RenderResult::default();
+ let result = GeneralRenderer::render(&test_data(), &GeneralRendererSetting::Json, &mut r);
+ assert!(result.is_ok());
+ assert!(!r.is_empty());
+ let output: String = r.into();
+ assert!(output.contains("\"name\""));
+ assert!(output.contains("\"test\""));
+ assert!(output.contains("\"value\""));
+ assert!(output.contains("42"));
+}
+
+#[test]
+fn test_render_yaml() {
+ let mut r = RenderResult::default();
+ let result = GeneralRenderer::render(&test_data(), &GeneralRendererSetting::Yaml, &mut r);
+ assert!(result.is_ok());
+ assert!(!r.is_empty());
+ let output: String = r.into();
+ assert!(output.contains("name:"));
+ assert!(output.contains("test"));
+ assert!(output.contains("value:"));
+ assert!(output.contains("42"));
+}
+
+#[test]
+fn test_render_toml() {
+ let mut r = RenderResult::default();
+ let result = GeneralRenderer::render(&test_data(), &GeneralRendererSetting::Toml, &mut r);
+ assert!(result.is_ok());
+ assert!(!r.is_empty());
+ let output: String = r.into();
+ assert!(output.contains("name = "));
+ assert!(output.contains("test"));
+ assert!(output.contains("value = "));
+ assert!(output.contains("42"));
+}
+
+#[test]
+fn test_render_ron() {
+ let mut r = RenderResult::default();
+ let result = GeneralRenderer::render(&test_data(), &GeneralRendererSetting::Ron, &mut r);
+ assert!(result.is_ok());
+ assert!(!r.is_empty());
+ let output: String = r.into();
+ assert!(output.contains("name:"));
+ assert!(output.contains("\"test\""));
+ assert!(output.contains("value:"));
+ assert!(output.contains("42"));
+}
diff --git a/mingling_core/tests/test-repl/Cargo.lock b/mingling_core/tests/test-repl/Cargo.lock
new file mode 100644
index 0000000..0950248
--- /dev/null
+++ b/mingling_core/tests/test-repl/Cargo.lock
@@ -0,0 +1,76 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "just_fmt"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e"
+
+[[package]]
+name = "mingling"
+version = "0.2.0"
+dependencies = [
+ "mingling_core",
+ "mingling_macros",
+]
+
+[[package]]
+name = "mingling_core"
+version = "0.2.0"
+dependencies = [
+ "just_fmt",
+]
+
+[[package]]
+name = "mingling_macros"
+version = "0.2.0"
+dependencies = [
+ "just_fmt",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "test-repl"
+version = "0.1.0"
+dependencies = [
+ "mingling",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
diff --git a/mingling_core/tests/test-repl/Cargo.toml b/mingling_core/tests/test-repl/Cargo.toml
new file mode 100644
index 0000000..c4c83ce
--- /dev/null
+++ b/mingling_core/tests/test-repl/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "test-repl"
+version = "0.1.0"
+edition = "2024"
+publish = false
+
+[workspace]
+
+[dependencies]
+mingling = { path = "../../../mingling", features = ["repl", "extra_macros"] }
diff --git a/mingling_core/tests/test-repl/tests/integration.rs b/mingling_core/tests/test-repl/tests/integration.rs
new file mode 100644
index 0000000..1792525
--- /dev/null
+++ b/mingling_core/tests/test-repl/tests/integration.rs
@@ -0,0 +1,63 @@
+use mingling::Flag;
+use mingling::Node;
+use mingling::RenderResult;
+use mingling::core_res::ResREPL;
+
+// ResREPL tests
+
+#[test]
+fn test_res_repl_default_exit_false() {
+ let res = ResREPL::default();
+ assert!(!res.exit);
+}
+
+#[test]
+fn test_res_repl_exit_true() {
+ let mut res = ResREPL::default();
+ res.exit = true;
+ assert!(res.exit);
+}
+
+// Node tests
+
+#[test]
+fn test_node_from_str() {
+ let node = Node::from("a.b.c");
+ assert_eq!(node.to_string(), "a.b.c");
+}
+
+#[test]
+fn test_node_kebab_case() {
+ let node = Node::from("HelloWorld.FooBar");
+ assert_eq!(node.to_string(), "hello-world.foo-bar");
+}
+
+// Flag tests
+
+#[test]
+fn test_flag_from_static_str() {
+ let flag = Flag::from("-h");
+ assert_eq!(flag.as_ref(), &["-h"]);
+}
+
+#[test]
+fn test_flag_empty() {
+ let flag = Flag::from(());
+ assert!(flag.is_empty());
+}
+
+// RenderResult tests
+
+#[test]
+fn test_render_result_default() {
+ let r = RenderResult::default();
+ assert!(r.is_empty());
+ assert_eq!(r.exit_code, 0);
+}
+
+#[test]
+fn test_render_result_print() {
+ let mut r = RenderResult::default();
+ r.print("hello");
+ assert_eq!(&*r, "hello");
+}