1use crate::analyzer::BehaviorInferrer;
2use crate::config::load_config;
3use crate::error::Result;
4use crate::types::{
5 Assertion, CliAnalysis, CliOption, CliTestConfig, NoArgsBehavior, OptionType, TestCase,
6 TestCategory, TestPriority,
7};
8use crate::utils::{choose_strategy, ParallelStrategy, Workload};
9use rayon::prelude::*;
10use std::path::Path;
11
12pub struct TestGenerator {
14 analysis: CliAnalysis,
16
17 categories: Vec<TestCategory>,
19
20 config: Option<CliTestConfig>,
22}
23
24impl TestGenerator {
25 pub fn new(analysis: CliAnalysis, categories: Vec<TestCategory>) -> Self {
27 Self {
28 analysis,
29 categories,
30 config: None,
31 }
32 }
33
34 pub fn with_config(
36 analysis: CliAnalysis,
37 categories: Vec<TestCategory>,
38 config_path: Option<&Path>,
39 ) -> Result<Self> {
40 let config = load_config(config_path)?;
41 Ok(Self {
42 analysis,
43 categories,
44 config,
45 })
46 }
47
48 pub fn generate(&self) -> Result<Vec<TestCase>> {
77 log::info!("Generating tests for {} categories", self.categories.len());
78
79 let mut all_tests = Vec::new();
80
81 for category in &self.categories {
82 let tests = match category {
83 TestCategory::Basic => self.generate_basic_tests()?,
84 TestCategory::Help => self.generate_help_tests()?,
85 TestCategory::Security => self.generate_security_tests()?,
86 TestCategory::Path => self.generate_path_tests()?,
87 TestCategory::InputValidation => self.generate_input_validation_tests()?,
88 TestCategory::DestructiveOps => self.generate_destructive_ops_tests()?,
89 TestCategory::DirectoryTraversal => self.generate_directory_traversal_tests()?,
90 TestCategory::Performance => self.generate_performance_tests()?,
91 TestCategory::MultiShell => self.generate_multi_shell_tests()?,
92 };
93
94 log::info!("Generated {} tests for {:?}", tests.len(), category);
95 all_tests.extend(tests);
96 }
97
98 log::info!("Total tests generated: {}", all_tests.len());
99 Ok(all_tests)
100 }
101
102 pub fn generate_parallel(&self) -> Result<Vec<TestCase>> {
129 log::info!(
130 "Generating tests in parallel for {} categories",
131 self.categories.len()
132 );
133
134 let results: Result<Vec<Vec<TestCase>>> = self
135 .categories
136 .par_iter()
137 .map(|category| match category {
138 TestCategory::Basic => self.generate_basic_tests(),
139 TestCategory::Help => self.generate_help_tests(),
140 TestCategory::Security => self.generate_security_tests(),
141 TestCategory::Path => self.generate_path_tests(),
142 TestCategory::InputValidation => self.generate_input_validation_tests(),
143 TestCategory::DestructiveOps => self.generate_destructive_ops_tests(),
144 TestCategory::DirectoryTraversal => self.generate_directory_traversal_tests(),
145 TestCategory::Performance => self.generate_performance_tests(),
146 TestCategory::MultiShell => self.generate_multi_shell_tests(),
147 })
148 .collect();
149
150 let all_tests: Vec<TestCase> = results?.into_iter().flatten().collect();
151
152 log::info!("Total tests generated (parallel): {}", all_tests.len());
153 Ok(all_tests)
154 }
155
156 pub fn generate_with_strategy(&self) -> Result<Vec<TestCase>> {
177 let workload = Workload::new(
179 &self.categories,
180 self.analysis.global_options.len(),
181 self.analysis.subcommands.len(),
182 );
183
184 let strategy = choose_strategy(&workload);
186
187 log::info!(
188 "Auto-selected strategy: {:?} (categories={}, global_options={}, subcommands={}, estimated_tests={})",
189 strategy,
190 workload.num_categories,
191 self.analysis.global_options.len(),
192 self.analysis.subcommands.len(),
193 workload.total_estimated_tests()
194 );
195
196 match strategy {
198 ParallelStrategy::Sequential => {
199 log::debug!("Using sequential generation");
200 self.generate()
201 }
202 ParallelStrategy::CategoryLevel => {
203 log::debug!("Using category-level parallel generation");
204 self.generate_parallel()
205 }
206 ParallelStrategy::TestLevel => {
207 log::debug!("Using test-level parallel generation");
208 self.generate_parallel()
212 }
213 }
214 }
215
216 fn generate_basic_tests(&self) -> Result<Vec<TestCase>> {
218 let mut tests = Vec::new();
219
220 tests.push(
222 TestCase::new(
223 "basic-001".to_string(),
224 "Display help with --help flag".to_string(),
225 TestCategory::Basic,
226 "\"$CLI_BINARY\" --help".to_string(),
227 )
228 .with_exit_code(0)
229 .with_assertion(Assertion::OutputContains("Usage:".to_string()))
230 .with_tag("help".to_string()),
231 );
232
233 tests.push(
235 TestCase::new(
236 "basic-002".to_string(),
237 "Display help with -h flag".to_string(),
238 TestCategory::Basic,
239 "\"$CLI_BINARY\" -h".to_string(),
240 )
241 .with_exit_code(0)
242 .with_assertion(Assertion::OutputContains("Usage:".to_string()))
243 .with_tag("help".to_string()),
244 );
245
246 if self.analysis.version.is_some() {
248 tests.push(
249 TestCase::new(
250 "basic-003".to_string(),
251 "Display version with --version flag".to_string(),
252 TestCategory::Basic,
253 "\"$CLI_BINARY\" --version".to_string(),
254 )
255 .with_exit_code(0)
256 .with_tag("version".to_string()),
257 );
258 }
259
260 tests.push(
267 TestCase::new(
268 "basic-004".to_string(),
269 "Reject invalid option".to_string(),
270 TestCategory::Basic,
271 "\"$CLI_BINARY\" --invalid-option-xyz".to_string(),
272 )
273 .expect_nonzero_exit() .with_assertion(Assertion::OutputMatches(
279 "(error|Error|unknown|unrecognized|invalid)".to_string(),
280 ))
281 .with_tag("error-handling".to_string()),
282 );
283
284 let inferrer = BehaviorInferrer::new();
286 let no_args_behavior = inferrer.infer_no_args_behavior(&self.analysis);
287
288 match no_args_behavior {
289 NoArgsBehavior::ShowHelp => {
290 tests.push(
291 TestCase::new(
292 "basic-005".to_string(),
293 "Show help when invoked without arguments".to_string(),
294 TestCategory::Basic,
295 "\"$CLI_BINARY\"".to_string(),
296 )
297 .with_exit_code(0)
298 .with_priority(TestPriority::Important)
301 .with_tag("no-args".to_string())
302 .with_tag("show-help".to_string()),
303 );
304 }
305
306 NoArgsBehavior::RequireSubcommand => {
307 tests.push(
308 TestCase::new(
309 "basic-005".to_string(),
310 "Require subcommand when invoked without arguments".to_string(),
311 TestCategory::Basic,
312 "\"$CLI_BINARY\"".to_string(),
313 )
314 .expect_nonzero_exit() .with_priority(TestPriority::Important)
318 .with_tag("no-args".to_string())
319 .with_tag("require-subcommand".to_string()),
320 );
321 }
322
323 NoArgsBehavior::Interactive => {
324 tests.push(
325 TestCase::new(
326 "basic-005".to_string(),
327 "Enter interactive mode when invoked without arguments".to_string(),
328 TestCategory::Basic,
329 "echo '' | \"$CLI_BINARY\"".to_string(), )
331 .with_exit_code(0)
332 .with_priority(TestPriority::Important)
333 .with_tag("no-args".to_string())
334 .with_tag("interactive".to_string()),
335 );
336 }
337 }
338
339 Ok(tests)
340 }
341
342 fn generate_help_tests(&self) -> Result<Vec<TestCase>> {
344 if self.analysis.subcommands.len() < 10 {
347 let mut tests = Vec::new();
348
349 for (idx, subcommand) in self.analysis.subcommands.iter().enumerate() {
350 if subcommand.name.to_lowercase() == "help" {
352 log::debug!("Skipping help test for meta-command 'help'");
353 continue;
354 }
355
356 tests.push(
357 TestCase::new(
358 format!("help-{:03}", idx + 1),
359 format!("Display help for subcommand '{}'", subcommand.name),
360 TestCategory::Help,
361 format!("\"$CLI_BINARY\" {} --help", subcommand.name),
362 )
363 .with_exit_code(0)
364 .with_assertion(Assertion::OutputContains("Usage:".to_string()))
365 .with_tag("subcommand-help".to_string())
366 .with_tag(subcommand.name.clone()),
367 );
368 }
369
370 return Ok(tests);
371 }
372
373 let tests: Vec<TestCase> = self
375 .analysis
376 .subcommands
377 .par_iter()
378 .enumerate()
379 .filter_map(|(idx, subcommand)| {
380 if subcommand.name.to_lowercase() == "help" {
382 log::debug!("Skipping help test for meta-command 'help'");
383 return None;
384 }
385
386 Some(
387 TestCase::new(
388 format!("help-{:03}", idx + 1),
389 format!("Display help for subcommand '{}'", subcommand.name),
390 TestCategory::Help,
391 format!("\"$CLI_BINARY\" {} --help", subcommand.name),
392 )
393 .with_exit_code(0)
394 .with_assertion(Assertion::OutputContains("Usage:".to_string()))
395 .with_tag("subcommand-help".to_string())
396 .with_tag(subcommand.name.clone()),
397 )
398 })
399 .collect();
400
401 Ok(tests)
402 }
403
404 fn generate_security_tests(&self) -> Result<Vec<TestCase>> {
425 let mut tests = Vec::new();
426
427 let skip_options: Vec<String> = self
429 .config
430 .as_ref()
431 .and_then(|c| {
432 c.test_adjustments
433 .security
434 .as_ref()
435 .map(|s| s.skip_options.iter().map(|opt| opt.name.clone()).collect())
436 })
437 .unwrap_or_default();
438
439 let string_option = self
442 .analysis
443 .global_options
444 .iter()
445 .find(|opt| {
446 matches!(opt.option_type, OptionType::String | OptionType::Path)
447 && opt.long.is_some()
448 && !skip_options.iter().any(|skip_name| {
449 opt.long
450 .as_ref()
451 .is_some_and(|long| long.trim_start_matches("--") == skip_name)
452 })
453 })
454 .and_then(|opt| opt.long.as_ref())
455 .unwrap_or(&"--invalid-option".to_string())
456 .clone();
457
458 tests.push(
461 TestCase::new(
462 "security-001".to_string(),
463 "Reject command injection in option value".to_string(),
464 TestCategory::Security,
465 format!("\"$CLI_BINARY\" {} 'test; rm -rf /'", string_option),
466 )
467 .expect_nonzero_exit() .with_priority(TestPriority::SecurityCheck)
469 .with_tag("injection".to_string())
470 .with_tag("critical".to_string()),
471 );
472
473 tests.push(
476 TestCase::new(
477 "security-002".to_string(),
478 "Reject null byte in option value".to_string(),
479 TestCategory::Security,
480 format!(
481 r#""$CLI_BINARY" {} $'/tmp/test\x00malicious'"#,
482 string_option
483 ),
484 )
485 .expect_nonzero_exit() .with_priority(TestPriority::SecurityCheck)
487 .with_tag("injection".to_string())
488 .with_tag("critical".to_string()),
489 );
490
491 tests.push(
494 TestCase::new(
495 "security-003".to_string(),
496 "Reject path traversal attempt".to_string(),
497 TestCategory::Security,
498 format!("\"$CLI_BINARY\" {} ../../../etc/passwd", string_option),
499 )
500 .expect_nonzero_exit() .with_priority(TestPriority::SecurityCheck)
502 .with_tag("path-traversal".to_string())
503 .with_tag("critical".to_string()),
504 );
505
506 if let Some(config) = &self.config {
537 if let Some(security_config) = &config.test_adjustments.security {
538 for (idx, custom_test) in security_config.custom_tests.iter().enumerate() {
539 tests.push(
540 TestCase::new(
541 format!("security-custom-{:03}", idx + 1),
542 custom_test.description.clone(),
543 TestCategory::Security,
544 custom_test.command.clone(),
545 )
546 .with_exit_code(custom_test.expected_exit_code)
547 .with_priority(TestPriority::SecurityCheck)
548 .with_tag("custom".to_string())
549 .with_tag(custom_test.name.clone()),
550 );
551 }
552 }
553 }
554
555 Ok(tests)
556 }
557
558 fn generate_path_tests(&self) -> Result<Vec<TestCase>> {
560 let mut tests = Vec::new();
561
562 let path_options: Vec<&CliOption> = self
564 .analysis
565 .global_options
566 .iter()
567 .filter(|opt| matches!(opt.option_type, OptionType::Path))
568 .collect();
569
570 if path_options.is_empty() {
571 log::debug!("No path options found, generating generic path tests");
572 return Ok(tests);
573 }
574
575 for (idx, option) in path_options.iter().enumerate() {
576 let flag = option.long.as_ref().or(option.short.as_ref()).unwrap();
577
578 tests.push(
580 TestCase::new(
581 format!("path-{:03}-spaces", idx + 1),
582 format!("Handle path with spaces for {}", flag),
583 TestCategory::Path,
584 format!("\"$CLI_BINARY\" {} '/tmp/test dir/file.txt'", flag),
585 )
586 .with_tag("spaces".to_string()),
587 );
588
589 tests.push(
591 TestCase::new(
592 format!("path-{:03}-unicode", idx + 1),
593 format!("Handle Unicode in path for {}", flag),
594 TestCategory::Path,
595 format!("\"$CLI_BINARY\" {} '/tmp/ใในใ/file.txt'", flag),
596 )
597 .with_tag("unicode".to_string()),
598 );
599
600 tests.push(
602 TestCase::new(
603 format!("path-{:03}-symlink", idx + 1),
604 format!("Handle symbolic links for {}", flag),
605 TestCategory::Path,
606 format!("\"$CLI_BINARY\" {} '/tmp/test-symlink'", flag),
607 )
608 .with_tag("symlink".to_string()),
609 );
610 }
611
612 Ok(tests)
613 }
614
615 fn generate_input_validation_tests(&self) -> Result<Vec<TestCase>> {
617 let mut tests = Vec::new();
618
619 let numeric_options: Vec<&CliOption> = self
621 .analysis
622 .global_options
623 .iter()
624 .filter(|opt| matches!(opt.option_type, OptionType::Numeric { .. }))
625 .collect();
626
627 for (idx, option) in numeric_options.iter().enumerate() {
628 let flag = option.long.as_ref().or(option.short.as_ref()).unwrap();
629
630 tests.push(
632 TestCase::new(
633 format!("input-{:03}-valid", idx + 1),
634 format!("Accept valid numeric value for {}", flag),
635 TestCategory::InputValidation,
636 format!("\"$CLI_BINARY\" {} 10", flag),
637 )
638 .with_tag("numeric".to_string()),
639 );
640
641 tests.push(
643 TestCase::new(
644 format!("input-{:03}-invalid", idx + 1),
645 format!("Reject non-numeric value for {}", flag),
646 TestCategory::InputValidation,
647 format!("\"$CLI_BINARY\" {} 'not-a-number'", flag),
648 )
649 .with_exit_code(1)
650 .with_tag("numeric".to_string())
651 .with_tag("validation".to_string()),
652 );
653
654 if let OptionType::Numeric {
656 min: Some(min_val), ..
657 } = &option.option_type
658 {
659 if *min_val >= 0 {
660 tests.push(
661 TestCase::new(
662 format!("input-{:03}-negative", idx + 1),
663 format!("Reject negative value for {}", flag),
664 TestCategory::InputValidation,
665 format!("\"$CLI_BINARY\" {} -1", flag),
666 )
667 .with_exit_code(1)
668 .with_tag("numeric".to_string())
669 .with_tag("validation".to_string()),
670 );
671 }
672 }
673 }
674
675 let enum_options: Vec<&CliOption> = self
677 .analysis
678 .global_options
679 .iter()
680 .filter(|opt| matches!(opt.option_type, OptionType::Enum { .. }))
681 .collect();
682
683 for (idx, option) in enum_options.iter().enumerate() {
684 let flag = option.long.as_ref().or(option.short.as_ref()).unwrap();
685
686 if let OptionType::Enum { values } = &option.option_type {
687 if let Some(first_value) = values.first() {
688 tests.push(
690 TestCase::new(
691 format!("enum-{:03}-valid", idx + 1),
692 format!("Accept valid enum value for {}", flag),
693 TestCategory::InputValidation,
694 format!("\"$CLI_BINARY\" {} {}", flag, first_value),
695 )
696 .with_tag("enum".to_string()),
697 );
698 }
699
700 tests.push(
702 TestCase::new(
703 format!("enum-{:03}-invalid", idx + 1),
704 format!("Reject invalid enum value for {}", flag),
705 TestCategory::InputValidation,
706 format!("\"$CLI_BINARY\" {} 'invalid-value-xyz'", flag),
707 )
708 .with_exit_code(1)
709 .with_tag("enum".to_string())
710 .with_tag("validation".to_string()),
711 );
712 }
713 }
714
715 Ok(tests)
716 }
717
718 fn generate_destructive_ops_tests(&self) -> Result<Vec<TestCase>> {
720 let mut tests = Vec::new();
721
722 let env_vars = self
724 .config
725 .as_ref()
726 .and_then(|c| c.test_adjustments.destructive_ops.as_ref())
727 .map(|d| d.env_vars.clone())
728 .unwrap_or_default();
729
730 let cancel_exit_code = self
731 .config
732 .as_ref()
733 .and_then(|c| c.test_adjustments.destructive_ops.as_ref())
734 .map(|d| d.cancel_exit_code)
735 .unwrap_or(1); let destructive_keywords = ["delete", "remove", "clean", "destroy", "purge", "drop"];
739
740 for subcommand in &self.analysis.subcommands {
741 let is_destructive = destructive_keywords
742 .iter()
743 .any(|keyword| subcommand.name.to_lowercase().contains(keyword));
744
745 if is_destructive {
746 let dummy_args = subcommand
748 .required_args
749 .iter()
750 .map(|arg| {
751 match arg.to_lowercase().as_str() {
753 "id" | "name" => "test-id",
754 "file" | "path" => "/tmp/test-file",
755 "dir" | "directory" => "/tmp/test-dir",
756 _ => "test-value",
757 }
758 })
759 .collect::<Vec<_>>()
760 .join(" ");
761
762 let args_part = if dummy_args.is_empty() {
763 String::new()
764 } else {
765 format!(" {}", dummy_args)
766 };
767
768 let env_prefix = if env_vars.is_empty() {
770 String::new()
771 } else {
772 env_vars
773 .iter()
774 .map(|(k, v)| format!("{}=\"{}\"", k, v))
775 .collect::<Vec<_>>()
776 .join(" ")
777 + " "
778 };
779
780 let test_command = if env_vars.is_empty() {
782 format!(
784 "echo 'n' | \"$CLI_BINARY\" {}{}",
785 subcommand.name, args_part
786 )
787 } else {
788 format!(
790 "{}\"$CLI_BINARY\" {}{}",
791 env_prefix, subcommand.name, args_part
792 )
793 };
794
795 let mut test = TestCase::new(
796 format!("destructive-{}-001", subcommand.name),
797 if env_vars.is_empty() {
798 format!("Subcommand '{}' requires confirmation", subcommand.name)
799 } else {
800 format!(
801 "Subcommand '{}' auto-confirms with env vars",
802 subcommand.name
803 )
804 },
805 TestCategory::DestructiveOps,
806 test_command,
807 )
808 .with_tag("confirmation".to_string())
809 .with_tag(subcommand.name.clone());
810
811 if env_vars.is_empty() {
813 test = test.with_exit_code(cancel_exit_code);
815 }
816 tests.push(test);
819
820 let has_yes_flag = subcommand.options.iter().any(|opt| {
822 opt.long
823 .as_ref()
824 .is_some_and(|l| l.contains("yes") || l.contains("force"))
825 });
826
827 if has_yes_flag {
828 tests.push(
829 TestCase::new(
830 format!("destructive-{}-002", subcommand.name),
831 format!("Subcommand '{}' accepts --yes flag", subcommand.name),
832 TestCategory::DestructiveOps,
833 format!("\"$CLI_BINARY\" {}{} --yes", subcommand.name, args_part),
834 )
835 .with_tag("force".to_string())
836 .with_tag(subcommand.name.clone()),
837 );
838 }
839 }
840 }
841
842 if tests.is_empty() {
844 log::debug!("No destructive subcommands detected");
845 }
846
847 Ok(tests)
848 }
849
850 fn generate_directory_traversal_tests(&self) -> Result<Vec<TestCase>> {
852 let mut tests = Vec::new();
853
854 let test_directories = self
856 .config
857 .as_ref()
858 .and_then(|c| c.test_adjustments.directory_traversal.as_ref())
859 .and_then(|dt| {
860 if dt.test_directories.is_empty() {
861 None
862 } else {
863 Some(dt.test_directories.clone())
864 }
865 });
866
867 if let Some(test_dirs) = test_directories {
868 for (idx, test_dir) in test_dirs.iter().enumerate() {
870 let description = if let Some(file_count) = test_dir.file_count {
871 format!(
872 "Handle directory with {} files at {}",
873 file_count, test_dir.path
874 )
875 } else if let Some(depth) = test_dir.depth {
876 format!(
877 "Handle deeply nested directory (depth {}) at {}",
878 depth, test_dir.path
879 )
880 } else {
881 format!("Handle directory at {}", test_dir.path)
882 };
883
884 tests.push(
885 TestCase::new(
886 format!("dir-traversal-{:03}", idx + 1),
887 description,
888 TestCategory::DirectoryTraversal,
889 format!("\"$CLI_BINARY\" {}", test_dir.path),
890 )
891 .with_tag("configured".to_string())
892 .with_tag(if test_dir.file_count.is_some() {
893 "large-dir".to_string()
894 } else if test_dir.depth.is_some() {
895 "deep-nesting".to_string()
896 } else {
897 "basic".to_string()
898 }),
899 );
900 }
901 } else {
902 tests = vec![
904 TestCase::new(
906 "dir-traversal-001".to_string(),
907 "Handle directory with 1000 files".to_string(),
908 TestCategory::DirectoryTraversal,
909 "\"$CLI_BINARY\" /tmp/test-large-dir".to_string(),
910 )
911 .with_tag("performance".to_string())
912 .with_tag("large-dir".to_string()),
913 TestCase::new(
915 "dir-traversal-002".to_string(),
916 "Handle deeply nested directory (50 levels)".to_string(),
917 TestCategory::DirectoryTraversal,
918 "\"$CLI_BINARY\" /tmp/test-deep-dir".to_string(),
919 )
920 .with_tag("performance".to_string())
921 .with_tag("deep-nesting".to_string()),
922 TestCase::new(
924 "dir-traversal-003".to_string(),
925 "Detect and handle symlink loops".to_string(),
926 TestCategory::DirectoryTraversal,
927 "\"$CLI_BINARY\" /tmp/test-symlink-loop".to_string(),
928 )
929 .with_tag("symlink".to_string())
930 .with_tag("loop-detection".to_string()),
931 ];
932 }
933
934 Ok(tests)
935 }
936
937 fn generate_performance_tests(&self) -> Result<Vec<TestCase>> {
939 let tests = vec![
940 TestCase::new(
942 "perf-001".to_string(),
943 "Startup time for --help < 100ms".to_string(),
944 TestCategory::Performance,
945 "\"$CLI_BINARY\" --help".to_string(),
946 )
947 .with_exit_code(0)
948 .with_tag("startup".to_string())
949 .with_tag("benchmark".to_string()),
950 TestCase::new(
952 "perf-002".to_string(),
953 "Memory usage stays within reasonable limits".to_string(),
954 TestCategory::Performance,
955 "\"$CLI_BINARY\" --help".to_string(),
956 )
957 .with_exit_code(0)
958 .with_tag("memory".to_string())
959 .with_tag("benchmark".to_string()),
960 ];
961
962 Ok(tests)
963 }
964
965 fn generate_multi_shell_tests(&self) -> Result<Vec<TestCase>> {
967 let mut tests = Vec::new();
968
969 for shell in &["bash", "zsh", "sh"] {
972 tests.push(
973 TestCase::new(
974 format!("multi-shell-{}", shell),
975 format!("Run --help in {}", shell),
976 TestCategory::MultiShell,
977 format!("{} -c \"\\\"$CLI_BINARY\\\" --help\"", shell),
978 )
979 .with_exit_code(0)
980 .with_tag(shell.to_string()),
981 );
982 }
983
984 Ok(tests)
985 }
986}
987
988#[cfg(test)]
989mod tests {
990 use super::*;
991 use crate::types::Subcommand;
992 use std::path::PathBuf;
993
994 fn create_test_analysis() -> CliAnalysis {
995 let mut analysis = CliAnalysis::new(
996 PathBuf::from("/usr/bin/test-cli"),
997 "test-cli".to_string(),
998 "Test CLI help output".to_string(),
999 );
1000
1001 analysis.version = Some("1.0.0".to_string());
1002
1003 analysis.global_options.push(CliOption {
1005 short: Some("-t".to_string()),
1006 long: Some("--timeout".to_string()),
1007 description: Some("Timeout in seconds".to_string()),
1008 option_type: OptionType::Numeric {
1009 min: Some(0),
1010 max: Some(3600),
1011 },
1012 required: false,
1013 default_value: Some("30".to_string()),
1014 });
1015
1016 analysis.global_options.push(CliOption {
1018 short: Some("-f".to_string()),
1019 long: Some("--file".to_string()),
1020 description: Some("Input file".to_string()),
1021 option_type: OptionType::Path,
1022 required: false,
1023 default_value: None,
1024 });
1025
1026 analysis.global_options.push(CliOption {
1028 short: None,
1029 long: Some("--format".to_string()),
1030 description: Some("Output format".to_string()),
1031 option_type: OptionType::Enum {
1032 values: vec!["json".to_string(), "yaml".to_string(), "text".to_string()],
1033 },
1034 required: false,
1035 default_value: Some("text".to_string()),
1036 });
1037
1038 analysis.subcommands.push(Subcommand {
1040 name: "delete".to_string(),
1041 description: Some("Delete resources".to_string()),
1042 options: vec![CliOption {
1043 short: None,
1044 long: Some("--yes".to_string()),
1045 description: Some("Skip confirmation".to_string()),
1046 option_type: OptionType::Flag,
1047 required: false,
1048 default_value: None,
1049 }],
1050 required_args: vec![],
1051 subcommands: vec![],
1052 depth: 0,
1053 });
1054
1055 analysis
1056 }
1057
1058 #[test]
1059 fn test_generator_creation() {
1060 let analysis = create_test_analysis();
1061 let categories = vec![TestCategory::Basic];
1062 let generator = TestGenerator::new(analysis, categories);
1063
1064 assert_eq!(generator.categories.len(), 1);
1065 }
1066
1067 #[test]
1068 fn test_generate_basic_tests() {
1069 let analysis = create_test_analysis();
1070 let generator = TestGenerator::new(analysis, vec![]);
1071
1072 let tests = generator.generate_basic_tests().unwrap();
1073
1074 assert!(!tests.is_empty());
1075 assert!(tests.iter().any(|t| t.command.contains("--help")));
1076 assert!(tests.iter().any(|t| t.command.contains("--version")));
1077 }
1078
1079 #[test]
1080 fn test_generate_security_tests() {
1081 let analysis = create_test_analysis();
1082 let generator = TestGenerator::new(analysis, vec![]);
1083
1084 let tests = generator.generate_security_tests().unwrap();
1085
1086 assert!(!tests.is_empty());
1087 assert!(tests
1088 .iter()
1089 .any(|t| t.tags.contains(&"injection".to_string())));
1090 }
1091
1092 #[test]
1093 fn test_generate_input_validation_tests() {
1094 let analysis = create_test_analysis();
1095 let generator = TestGenerator::new(analysis, vec![]);
1096
1097 let tests = generator.generate_input_validation_tests().unwrap();
1098
1099 assert!(!tests.is_empty());
1100 assert!(tests
1102 .iter()
1103 .any(|t| t.tags.contains(&"numeric".to_string())));
1104 assert!(tests.iter().any(|t| t.tags.contains(&"enum".to_string())));
1105 }
1106
1107 #[test]
1108 fn test_generate_destructive_ops_tests() {
1109 let analysis = create_test_analysis();
1110 let generator = TestGenerator::new(analysis, vec![]);
1111
1112 let tests = generator.generate_destructive_ops_tests().unwrap();
1113
1114 assert!(!tests.is_empty());
1115 assert!(tests.iter().any(|t| t.command.contains("delete")));
1116 }
1117
1118 #[test]
1119 fn test_generate_all_categories() {
1120 let analysis = create_test_analysis();
1121 let categories = vec![
1122 TestCategory::Basic,
1123 TestCategory::Security,
1124 TestCategory::InputValidation,
1125 ];
1126 let generator = TestGenerator::new(analysis, categories);
1127
1128 let tests = generator.generate().unwrap();
1129
1130 assert!(!tests.is_empty());
1131 assert!(tests.iter().any(|t| t.category == TestCategory::Basic));
1132 assert!(tests.iter().any(|t| t.category == TestCategory::Security));
1133 assert!(tests
1134 .iter()
1135 .any(|t| t.category == TestCategory::InputValidation));
1136 }
1137
1138 #[test]
1139 fn test_generate_parallel() {
1140 let analysis = create_test_analysis();
1141 let categories = vec![
1142 TestCategory::Basic,
1143 TestCategory::Security,
1144 TestCategory::Performance,
1145 ];
1146 let generator = TestGenerator::new(analysis, categories);
1147
1148 let tests = generator.generate_parallel().unwrap();
1149
1150 assert!(!tests.is_empty());
1151 }
1152}