From 514929c3b8ee0d4f540be5eb4bc8c1a10e62095d Mon Sep 17 00:00:00 2001 From: Weicao-CatilGrass <1992414357@qq.com> Date: Tue, 9 Jun 2026 21:08:20 +0800 Subject: Add unit and integration tests for mingling_core --- CHANGELOG.md | 37 ++ Cargo.toml | 12 + mingling_core/src/any.rs | 255 +++++++++++ mingling_core/src/asset/chain/error.rs | 83 ++++ mingling_core/src/asset/dispatcher.rs | 148 +++++++ mingling_core/src/asset/global_resource.rs | 66 +++ mingling_core/src/asset/lazy_resource.rs | 345 +++++++++++++++ mingling_core/src/asset/node.rs | 74 ++++ mingling_core/src/builds/comp.rs | 41 ++ mingling_core/src/comp.rs | 10 + mingling_core/src/comp/comp_ctx.rs | 80 ++++ mingling_core/src/comp/shell_ctx.rs | 125 ++++++ mingling_core/src/comp/suggest.rs | 263 ++++++++++++ mingling_core/src/program/config.rs | 142 +++++++ mingling_core/src/program/error.rs | 222 ++++++++++ mingling_core/src/program/flag.rs | 53 +++ mingling_core/src/program/hook.rs | 208 +++++++++ mingling_core/src/program/setup.rs | 82 ++++ mingling_core/src/program/single_instance.rs | 46 ++ mingling_core/src/program/string_vec.rs | 59 +++ mingling_core/src/renderer/general.rs | 112 +++++ mingling_core/src/renderer/general/error.rs | 32 ++ mingling_core/src/renderer/render_result.rs | 94 +++++ mingling_core/tests/test-all/Cargo.lock | 465 +++++++++++++++++++++ mingling_core/tests/test-all/Cargo.toml | 20 + mingling_core/tests/test-all/tests/integration.rs | 235 +++++++++++ mingling_core/tests/test-basic/Cargo.lock | 76 ++++ mingling_core/tests/test-basic/Cargo.toml | 10 + .../tests/test-basic/tests/integration.rs | 93 +++++ mingling_core/tests/test-comp/Cargo.lock | 86 ++++ mingling_core/tests/test-comp/Cargo.toml | 10 + mingling_core/tests/test-comp/tests/integration.rs | 123 ++++++ mingling_core/tests/test-dispatch-tree/Cargo.lock | 76 ++++ mingling_core/tests/test-dispatch-tree/Cargo.toml | 10 + .../tests/test-dispatch-tree/tests/integration.rs | 93 +++++ .../tests/test-general-renderer/Cargo.lock | 287 +++++++++++++ .../tests/test-general-renderer/Cargo.toml | 11 + .../test-general-renderer/tests/integration.rs | 77 ++++ mingling_core/tests/test-repl/Cargo.lock | 76 ++++ mingling_core/tests/test-repl/Cargo.toml | 10 + mingling_core/tests/test-repl/tests/integration.rs | 63 +++ 41 files changed, 4410 insertions(+) create mode 100644 mingling_core/tests/test-all/Cargo.lock create mode 100644 mingling_core/tests/test-all/Cargo.toml create mode 100644 mingling_core/tests/test-all/tests/integration.rs create mode 100644 mingling_core/tests/test-basic/Cargo.lock create mode 100644 mingling_core/tests/test-basic/Cargo.toml create mode 100644 mingling_core/tests/test-basic/tests/integration.rs create mode 100644 mingling_core/tests/test-comp/Cargo.lock create mode 100644 mingling_core/tests/test-comp/Cargo.toml create mode 100644 mingling_core/tests/test-comp/tests/integration.rs create mode 100644 mingling_core/tests/test-dispatch-tree/Cargo.lock create mode 100644 mingling_core/tests/test-dispatch-tree/Cargo.toml create mode 100644 mingling_core/tests/test-dispatch-tree/tests/integration.rs create mode 100644 mingling_core/tests/test-general-renderer/Cargo.lock create mode 100644 mingling_core/tests/test-general-renderer/Cargo.toml create mode 100644 mingling_core/tests/test-general-renderer/tests/integration.rs create mode 100644 mingling_core/tests/test-repl/Cargo.lock create mode 100644 mingling_core/tests/test-repl/Cargo.toml create mode 100644 mingling_core/tests/test-repl/tests/integration.rs 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\ + - **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\ + - **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 From> for ChainProcess { 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 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 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 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::()); + 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 = output.downcast::(); + 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 = output.downcast::(); + 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::()); + } + + #[test] + fn test_any_output_is_false_for_non_matching_type() { + let data = AlphaData { value: 7 }; + let output = AnyOutput::new(data); + + assert!(!output.is::()); + } + + // 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::().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::() { + v.value = 100; + } + + let result: Result = output.downcast::(); + assert_eq!(result.unwrap().value, 100); + } + + // ChainProcess::From + + #[test] + fn test_chain_process_from_any_output() { + let data = AlphaData { value: 3 }; + let output = AnyOutput::new(data); + + let cp: ChainProcess = 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 for SerData { + fn member_id() -> MockGroup { + MockGroup::Gamma + } + } + + let data = SerData { x: 42 }; + let output = AnyOutput::new(data); + let restored: Option = output.restore::(); + 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 for SerA { + fn member_id() -> MockGroup { + MockGroup::Alpha + } + } + + impl Groupped for SerB { + fn member_id() -> MockGroup { + MockGroup::Beta + } + } + + let data = SerA { a: 1 }; + let output = AnyOutput::new(data); + let restored: Option = output.restore::(); + 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 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 From> for Vec + 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 Dispatcher for MockDispatcher { + fn node(&self) -> crate::asset::node::Node { + self.name.into() + } + + fn begin(&self, _args: Vec) -> ChainProcess { + unimplemented!("not used in these tests") + } + + fn clone_dispatcher(&self) -> Box> { + 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 + Send + Sync> = Box::new(MockDispatcher { name: "a" }); + let d2: Box + Send + Sync> = Box::new(MockDispatcher { name: "b" }); + let dispatchers: Dispatchers = vec![d1, d2].into(); + assert_eq!(dispatchers.dispatcher.len(), 2); + } + + #[test] + fn test_dispatchers_from_single_boxed() { + let d: Box + Send + Sync> = Box::new(MockDispatcher { name: "x" }); + let dispatchers: Dispatchers = d.into(); + assert_eq!(dispatchers.dispatcher.len(), 1); + } + + #[test] + fn test_dispatchers_deref() { + let disp = MockDispatcher { name: "test" }; + let dispatchers: Dispatchers = Dispatchers::from((disp,)); + let inner: &Vec + Send + Sync + 'static>> = &*dispatchers; + assert_eq!(inner.len(), 1); + } + + #[test] + fn test_dispatchers_into_vec() { + let disp = MockDispatcher { name: "foo" }; + let dispatchers: Dispatchers = Dispatchers::from((disp,)); + let vec: Vec + Send + Sync + 'static>> = dispatchers.into(); + assert_eq!(vec.len(), 1); + } + + #[test] + fn test_box_clone_dispatcher() { + let disp: Box> = 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 ResourceMarker for T { this::().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!(::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!(::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 = vec![]; + assert_eq!( 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::().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`. + struct DropFlag(Arc); + + 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 = 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 = 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 = LazyRes::new(|| 13); + r.unwrap(); + } + + // LazyRes::unwrap_or returns default if uninitialized + #[test] + fn unwrap_or_uninitialized_returns_default() { + let r: LazyRes = 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 = 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 = LazyRes::default(); + assert!(!r.is_initialized()); + } + + #[test] + fn default_factory_produces_t_default() { + let mut r: LazyRes = LazyRes::default(); + assert_eq!(*r.get_ref(), 0); + } + + // From for LazyRes creates initialized value + #[test] + fn from_t_creates_initialized() { + let r: LazyRes = LazyRes::from("hello".to_string()); + assert!(r.is_initialized()); + } + + #[test] + fn from_t_contains_correct_value() { + let r: LazyRes = 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::lazy_default(); + assert!(!r.is_initialized()); + } + + #[test] + fn lazy_default_factory_returns_default() { + let mut r: LazyRes = 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::lazy_init(|| 77); + assert!(!r.is_initialized()); + } + + #[test] + fn lazy_init_factory_produces_correct_value() { + let mut r: LazyRes = 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> = 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 = LazyRes::::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 = 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` + /// by setting `Enum = Self`. + #[derive(Debug, Clone, PartialEq)] + struct MockCollect; + + impl Groupped 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 { + unimplemented!() + } + fn build_dispatcher_not_found(_args: Vec) -> AnyOutput { + unimplemented!() + } + fn build_empty_result() -> AnyOutput { + unimplemented!() + } + fn render(_any: AnyOutput, _r: &mut RenderResult) { + unimplemented!() + } + fn render_help(_any: AnyOutput, _r: &mut RenderResult) { + unimplemented!() + } + fn do_chain(_any: AnyOutput) -> ChainProcess { + unimplemented!() + } + #[cfg(feature = "comp")] + fn do_comp(_any: &AnyOutput, _ctx: &crate::ShellContext) -> crate::Suggest { + unimplemented!() + } + fn has_renderer(_any: &AnyOutput) -> bool { + unimplemented!() + } + fn has_chain(_any: &AnyOutput) -> bool { + unimplemented!() + } + + #[cfg(feature = "general_renderer")] + fn general_render( + _any: AnyOutput, + _setting: &crate::GeneralRendererSetting, + ) -> Result { + unimplemented!() + } + } + + #[test] + fn test_is_completing_with_comp_subcommand() { + let program: Program = + Program::new_with_args(["program", "__comp", "some", "args"]); + assert!(program.is_completing()); + } + + #[test] + fn test_is_completing_with_normal_subcommand() { + let program: Program = Program::new_with_args(["program", "normal", "cmd"]); + assert!(!program.is_completing()); + } + + #[test] + fn test_is_completing_with_no_args() { + let program: Program = 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 = 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 = &*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 = "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 = >::from("json"); + assert!( + matches!(val, GeneralRendererSetting::Disable) + || matches!(val, GeneralRendererSetting::Json) + ); + + let val = >::from("invalid"); + assert!(matches!(val, GeneralRendererSetting::Disable)); + } + + #[test] + fn from_string() { + let val = >::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 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 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 Program 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 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 { + unreachable!() + } + + fn build_dispatcher_not_found(_args: Vec) -> AnyOutput { + unreachable!() + } + + fn build_empty_result() -> AnyOutput { + unreachable!() + } + + fn render(_any: AnyOutput, _r: &mut RenderResult) { + unreachable!() + } + + fn render_help(_any: AnyOutput, _r: &mut RenderResult) { + unreachable!() + } + + fn do_chain(_any: AnyOutput) -> crate::ChainProcess { + unreachable!() + } + + fn has_renderer(_any: &AnyOutput) -> bool { + unreachable!() + } + + fn has_chain(_any: &AnyOutput) -> bool { + unreachable!() + } + + #[cfg(feature = "comp")] + fn do_comp(_any: &AnyOutput, _ctx: &crate::ShellContext) -> crate::Suggest { + unreachable!() + } + + #[cfg(feature = "general_renderer")] + fn general_render( + _any: AnyOutput, + _setting: &crate::GeneralRendererSetting, + ) -> Result { + unreachable!() + } + } + + #[test] + fn test_hook_empty() { + let hook = ProgramHook::::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::::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::::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::::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::::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::::empty().on_post_chain( + |_output: &AnyOutput| { + 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::::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::::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::::empty().on_finish(|| 42); + assert!(hook.finish.is_some()); + assert_eq!((hook.finish.unwrap())(), 42); + } + + #[test] + fn test_hook_builder_chaining() { + let hook = ProgramHook::::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` + /// by setting `Enum = Self`. + #[derive(Debug, Clone, PartialEq)] + struct MockCollect; + + impl Groupped 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 { + unimplemented!() + } + fn build_dispatcher_not_found(_args: Vec) -> AnyOutput { + unimplemented!() + } + fn build_empty_result() -> AnyOutput { + unimplemented!() + } + fn render(_any: AnyOutput, _r: &mut RenderResult) { + unimplemented!() + } + fn render_help(_any: AnyOutput, _r: &mut RenderResult) { + unimplemented!() + } + fn do_chain(_any: AnyOutput) -> ChainProcess { + unimplemented!() + } + #[cfg(feature = "comp")] + fn do_comp(_any: &AnyOutput, _ctx: &crate::ShellContext) -> crate::Suggest { + unimplemented!() + } + fn has_renderer(_any: &AnyOutput) -> bool { + unimplemented!() + } + fn has_chain(_any: &AnyOutput) -> bool { + unimplemented!() + } + + #[cfg(feature = "general_renderer")] + fn general_render( + _any: AnyOutput, + _setting: &crate::GeneralRendererSetting, + ) -> Result { + unimplemented!() + } + } + + struct TestSetup { + called: std::rc::Rc>, + } + + impl ProgramSetup for TestSetup { + fn setup(self, _program: &mut Program) { + 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 = 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::>() } + +#[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::().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::().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> 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 = &*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 = 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 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 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 { + unimplemented!() + } + fn build_dispatcher_not_found(_args: Vec) -> mingling::AnyOutput { + unimplemented!() + } + fn build_empty_result() -> mingling::AnyOutput { + unimplemented!() + } + fn render(_any: mingling::AnyOutput, _r: &mut RenderResult) { + unimplemented!() + } + fn render_help(_any: mingling::AnyOutput, _r: &mut RenderResult) { + unimplemented!() + } + fn do_chain(_any: mingling::AnyOutput) -> mingling::ChainProcess { + unimplemented!() + } + fn do_comp(_any: &mingling::AnyOutput, _ctx: &ShellContext) -> Suggest { + unimplemented!() + } + fn has_renderer(_any: &mingling::AnyOutput) -> bool { + unimplemented!() + } + fn has_chain(_any: &mingling::AnyOutput) -> bool { + unimplemented!() + } + + fn dispatch_args_trie( + _raw: &[String], + ) -> Result, mingling::error::ProgramInternalExecuteError> + { + unimplemented!() + } + + fn get_nodes() -> Vec<( + String, + &'static (dyn mingling::Dispatcher + Send + Sync), + )> { + unimplemented!() + } + + fn general_render( + _any: mingling::AnyOutput, + _setting: &GeneralRendererSetting, + ) -> Result { + 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 = Program::new_with_args(["app", "__comp"]); + assert!(program.is_completing()); +} + +#[test] +fn test_is_not_completing() { + let program: Program = 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::::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 = 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 = 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` +/// by setting `Enum = Self`. +#[derive(Debug, Clone, PartialEq)] +struct MockCollect; + +impl Groupped 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 { + unimplemented!() + } + fn build_dispatcher_not_found(_args: Vec) -> mingling::AnyOutput { + unimplemented!() + } + fn build_empty_result() -> mingling::AnyOutput { + unimplemented!() + } + fn render(_any: mingling::AnyOutput, _r: &mut RenderResult) { + unimplemented!() + } + fn render_help(_any: mingling::AnyOutput, _r: &mut RenderResult) { + unimplemented!() + } + fn do_chain(_any: mingling::AnyOutput) -> mingling::ChainProcess { + unimplemented!() + } + fn do_comp(_any: &mingling::AnyOutput, _ctx: &ShellContext) -> Suggest { + unimplemented!() + } + fn has_renderer(_any: &mingling::AnyOutput) -> bool { + unimplemented!() + } + fn has_chain(_any: &mingling::AnyOutput) -> 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 = Program::new_with_args(["myapp", "__comp", "hello", ""]); + assert!(program.is_completing()); +} + +#[test] +fn test_program_is_not_completing() { + let program: Program = 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 = 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"); +} -- cgit