use std::collections::HashMap;
use std::path::PathBuf;
use std::process::Command;
use tempfile::TempDir;
#[derive(Debug, Clone)]
struct CommandResult {
command: String,
args: Vec<String>,
success: bool,
stdout: String,
stderr: String,
exit_code: i32,
validation_errors: Vec<String>,
}
type Validator = fn(&str) -> Result<(), String>;
#[derive(Clone)]
struct CommandSpec {
command: String,
subcommand: Option<String>,
args: Vec<String>,
options: HashMap<String, String>,
should_succeed: bool,
output_validators: Vec<Validator>,
}
struct CliTestHarness {
binary_path: PathBuf,
test_dir: TempDir,
results: Vec<CommandResult>,
}
impl CliTestHarness {
fn new() -> Self {
let binary_path = PathBuf::from(env!("CARGO_BIN_EXE_pmat"));
let test_dir = TempDir::new().expect("Failed to create temp dir");
Self {
binary_path,
test_dir,
results: Vec::new(),
}
}
fn execute_command(&mut self, spec: CommandSpec) -> CommandResult {
let mut cmd = Command::new(&self.binary_path);
cmd.arg(&spec.command);
if let Some(subcmd) = &spec.subcommand {
cmd.arg(subcmd);
}
for arg in &spec.args {
cmd.arg(arg);
}
for (key, value) in &spec.options {
cmd.arg(format!("--{}", key));
if !value.is_empty() {
cmd.arg(value);
}
}
let cmd_child = cmd.spawn().expect("Failed to spawn command");
let output = match cmd_child.wait_with_output() {
Ok(output) => output,
Err(e) => panic!("Command failed to complete: {}", e),
};
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let exit_code = output.status.code().unwrap_or(-1);
let mut validation_errors = Vec::new();
if spec.should_succeed && !output.status.success() {
validation_errors.push(format!(
"Command should succeed but failed with exit code {}",
exit_code
));
}
for validator in &spec.output_validators {
if let Err(e) = validator(&stdout) {
validation_errors.push(e);
}
}
let result = CommandResult {
command: format!(
"{} {}",
spec.command,
spec.subcommand.as_ref().unwrap_or(&String::new())
),
args: spec.args.clone(),
success: output.status.success(),
stdout: stdout.clone(),
stderr: stderr.clone(),
exit_code,
validation_errors: validation_errors.clone(),
};
self.results.push(result.clone());
result
}
fn generate_report(&self) -> String {
let total = self.results.len();
let passed = self
.results
.iter()
.filter(|r| r.validation_errors.is_empty())
.count();
let failed = total - passed;
let mut report = "# CLI Functional Test Report\n\n".to_string();
report.push_str(&format!("Total Commands Tested: {}\n", total));
report.push_str(&format!("✅ Passed: {}\n", passed));
report.push_str(&format!("❌ Failed: {}\n\n", failed));
if failed > 0 {
report.push_str("## Failed Commands\n\n");
for result in &self.results {
if !result.validation_errors.is_empty() {
report.push_str(&format!("### Command: `{}`\n", result.command));
report.push_str(&format!("Args: {:?}\n", result.args));
report.push_str(&format!("Exit Code: {}\n", result.exit_code));
report.push_str("Errors:\n");
for error in &result.validation_errors {
report.push_str(&format!("- {}\n", error));
}
if !result.stderr.is_empty() {
report.push_str(&format!("Stderr:\n```\n{}\n```\n", result.stderr));
}
report.push('\n');
}
}
}
report.push_str("## Working Commands\n\n");
for result in &self.results {
if result.validation_errors.is_empty() {
report.push_str(&format!("✅ `pmat {}`", result.command));
if !result.args.is_empty() {
report.push_str(&format!(" {}", result.args.join(" ")));
}
report.push('\n');
}
}
report
}
}
fn validate_help_has_usage(output: &str) -> Result<(), String> {
if output.contains("Usage:") {
Ok(())
} else {
Err("Help output missing 'Usage:' section".to_string())
}
}
fn validate_help_has_commands(output: &str) -> Result<(), String> {
if output.contains("Commands:") {
Ok(())
} else {
Err("Help output missing 'Commands:' section".to_string())
}
}
fn validate_has_version(output: &str) -> Result<(), String> {
if output.contains("pmat") {
Ok(())
} else {
Err("Version output doesn't contain 'pmat'".to_string())
}
}
fn validate_complexity_output(output: &str) -> Result<(), String> {
if output.contains("Complexity Analysis") || output.contains("Files analyzed") {
Ok(())
} else {
Err("Complexity output missing expected sections".to_string())
}
}
fn validate_satd_output(output: &str) -> Result<(), String> {
if output.contains("SATD Analysis") || output.contains("Files analyzed") {
Ok(())
} else {
Err("SATD output missing expected sections".to_string())
}
}
fn validate_dead_code_output(output: &str) -> Result<(), String> {
if output.contains("Dead Code Analysis") || output.contains("Files analyzed") {
Ok(())
} else {
Err("Dead code output missing expected sections".to_string())
}
}
fn validate_quality_gate_output(output: &str) -> Result<(), String> {
if output.contains("Quality Gate") || output.contains("Checking") {
Ok(())
} else {
Err("Quality gate output missing expected sections".to_string())
}
}
fn generate_command_specs() -> Vec<CommandSpec> {
let mut specs = Vec::new();
specs.push(CommandSpec {
command: "--help".to_string(),
subcommand: None,
args: vec![],
options: HashMap::new(),
should_succeed: true,
output_validators: vec![validate_help_has_usage, validate_help_has_commands],
});
specs.push(CommandSpec {
command: "--version".to_string(),
subcommand: None,
args: vec![],
options: HashMap::new(),
should_succeed: true,
output_validators: vec![validate_has_version],
});
specs.push(CommandSpec {
command: "analyze".to_string(),
subcommand: Some("complexity".to_string()),
args: vec![],
options: HashMap::new(),
should_succeed: true,
output_validators: vec![validate_complexity_output],
});
specs.push(CommandSpec {
command: "analyze".to_string(),
subcommand: Some("complexity".to_string()),
args: vec![],
options: {
let mut opts = HashMap::new();
opts.insert("file".to_string(), "src/lib.rs".to_string());
opts
},
should_succeed: true,
output_validators: vec![],
});
specs.push(CommandSpec {
command: "analyze".to_string(),
subcommand: Some("satd".to_string()),
args: vec![],
options: HashMap::new(),
should_succeed: true,
output_validators: vec![validate_satd_output],
});
specs.push(CommandSpec {
command: "analyze".to_string(),
subcommand: Some("dead-code".to_string()),
args: vec![],
options: {
let mut opts = HashMap::new();
opts.insert("path".to_string(), ".".to_string());
opts
},
should_succeed: true,
output_validators: vec![validate_dead_code_output],
});
specs.push(CommandSpec {
command: "quality-gate".to_string(),
subcommand: None,
args: vec![],
options: HashMap::new(),
should_succeed: true, output_validators: vec![validate_quality_gate_output],
});
specs.push(CommandSpec {
command: "demo".to_string(),
subcommand: None,
args: vec!["--help".to_string()],
options: HashMap::new(),
should_succeed: true,
output_validators: vec![],
});
specs.push(CommandSpec {
command: "agent".to_string(),
subcommand: None,
args: vec!["--help".to_string()],
options: HashMap::new(),
should_succeed: true,
output_validators: vec![],
});
specs.push(CommandSpec {
command: "analyze".to_string(),
subcommand: Some("tdg".to_string()),
args: vec![],
options: HashMap::new(),
should_succeed: true,
output_validators: vec![],
});
specs.push(CommandSpec {
command: "context".to_string(),
subcommand: None,
args: vec![],
options: HashMap::new(),
should_succeed: true,
output_validators: vec![],
});
specs.push(CommandSpec {
command: "refactor".to_string(),
subcommand: None,
args: vec!["--help".to_string()],
options: HashMap::new(),
should_succeed: true,
output_validators: vec![],
});
specs
}
#[test]
fn test_all_cli_commands_work() {
let mut harness = CliTestHarness::new();
let specs = generate_command_specs();
println!("Testing {} command variations...", specs.len());
let mut failed = 0;
for spec in specs {
let result = harness.execute_command(spec.clone());
if !result.validation_errors.is_empty() {
failed += 1;
eprintln!(
"❌ Failed: {} {}",
spec.command,
spec.subcommand.unwrap_or_default()
);
for error in &result.validation_errors {
eprintln!(" {}", error);
}
} else {
println!(
"✅ Passed: {} {}",
spec.command,
spec.subcommand.unwrap_or_default()
);
}
}
let report = harness.generate_report();
std::fs::write("cli_test_report.md", &report).expect("Failed to write report");
if failed > 0 {
panic!(
"{} commands failed! See cli_test_report.md for details",
failed
);
}
}
#[test]
fn test_help_is_actually_helpful() {
let output = Command::new(env!("CARGO_BIN_EXE_pmat"))
.arg("--help")
.output()
.expect("Failed to run help");
let help_text = String::from_utf8_lossy(&output.stdout);
assert!(help_text.contains("Usage:"), "Help should show usage");
assert!(help_text.contains("Commands:"), "Help should list commands");
assert!(help_text.contains("analyze"), "Should show analyze command");
assert!(
help_text.contains("quality-gate"),
"Should show quality-gate command"
);
}
#[test]
fn test_analyze_complexity_actually_finds_files() {
let output = Command::new(env!("CARGO_BIN_EXE_pmat"))
.arg("analyze")
.arg("complexity")
.arg("--project-path")
.arg(".")
.output()
.expect("Failed to run complexity analysis");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
!stdout.contains("Files analyzed: 0"),
"Complexity analysis should find files, but got:\n{}",
stdout
);
}
#[test]
fn test_error_messages_are_helpful() {
let output = Command::new(env!("CARGO_BIN_EXE_pmat"))
.arg("agent")
.arg("analyze") .output()
.expect("Failed to run command");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("unrecognized") || stderr.contains("error"),
"Should show clear error message"
);
}