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 --- mingling_core/src/comp/comp_ctx.rs | 80 +++++++++++ mingling_core/src/comp/shell_ctx.rs | 125 +++++++++++++++++ mingling_core/src/comp/suggest.rs | 263 ++++++++++++++++++++++++++++++++++++ 3 files changed, 468 insertions(+) (limited to 'mingling_core/src/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"), + } + } +} -- cgit