use std::process::Command;
fn cha_binary() -> String {
let mut path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.pop(); path.push("target/release/cha");
path.to_string_lossy().to_string()
}
fn fixture(name: &str) -> String {
let mut path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("tests/fixtures");
path.push(name);
path.to_string_lossy().to_string()
}
fn run_analyze(file: &str, format: &str) -> (i32, String) {
let output = Command::new(cha_binary())
.args(["analyze", file, "--format", format])
.output()
.expect("failed to run cha");
let code = output.status.code().unwrap_or(-1);
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
(code, stdout)
}
#[test]
fn smelly_file_detects_naming_convention() {
let (_, out) = run_analyze(&fixture("smelly.ts"), "terminal");
assert!(
out.contains("[naming_convention]"),
"expected naming_convention finding"
);
assert!(out.contains("badClass"), "expected badClass in output");
}
#[test]
fn smelly_file_detects_long_parameter_list() {
let (_, out) = run_analyze(&fixture("smelly.ts"), "terminal");
assert!(
out.contains("[long_parameter_list]"),
"expected long_parameter_list finding"
);
}
#[test]
fn smelly_file_detects_high_complexity() {
let (_, out) = run_analyze(&fixture("smelly.ts"), "terminal");
assert!(
out.contains("[high_complexity]"),
"expected high_complexity finding"
);
}
#[test]
fn clean_file_has_no_warnings() {
let (code, out) = run_analyze(&fixture("clean.ts"), "terminal");
assert!(!out.contains("⚠"), "expected no warnings");
assert!(!out.contains("✖"), "expected no errors");
assert_eq!(code, 0);
}
#[test]
fn json_output_is_valid() {
let (_, out) = run_analyze(&fixture("smelly.ts"), "json");
let parsed: serde_json::Value = serde_json::from_str(&out).expect("invalid JSON output");
assert!(parsed.is_object(), "JSON output should be an object");
let findings = parsed.get("findings").expect("missing findings key");
assert!(findings.is_array());
let arr = findings.as_array().unwrap();
assert!(!arr.is_empty(), "expected at least one finding");
let first = &arr[0];
assert!(first.get("smell_name").is_some());
assert!(first.get("severity").is_some());
assert!(first.get("message").is_some());
let scores = parsed
.get("health_scores")
.expect("missing health_scores key");
assert!(scores.is_array());
}
#[test]
fn sarif_output_is_valid() {
let (_, out) = run_analyze(&fixture("smelly.ts"), "sarif");
let parsed: serde_json::Value = serde_json::from_str(&out).expect("invalid SARIF output");
assert_eq!(parsed["version"], "2.1.0");
assert!(parsed["runs"].is_array());
}
#[test]
fn fail_on_warning_exits_nonzero() {
let output = Command::new(cha_binary())
.args(["analyze", &fixture("smelly.ts"), "--fail-on", "warning"])
.output()
.expect("failed to run cha");
assert_ne!(
output.status.code().unwrap_or(0),
0,
"expected nonzero exit"
);
}
#[test]
fn fail_on_error_exits_zero_for_warnings_only() {
let output = Command::new(cha_binary())
.args(["analyze", &fixture("clean.ts"), "--fail-on", "error"])
.output()
.expect("failed to run cha");
assert_eq!(output.status.code().unwrap_or(-1), 0);
}
#[test]
fn plugin_filter_limits_output() {
let output = Command::new(cha_binary())
.args([
"analyze",
&fixture("smelly.ts"),
"--plugin",
"naming",
"--format",
"json",
])
.output()
.expect("failed to run cha");
let out = String::from_utf8_lossy(&output.stdout);
let parsed: serde_json::Value = serde_json::from_str(&out).expect("invalid JSON");
let findings = parsed["findings"].as_array().expect("missing findings");
for f in findings {
let name = f["smell_name"].as_str().unwrap_or("");
assert!(
name.starts_with("naming"),
"unexpected finding from non-naming plugin: {name}"
);
}
}
#[test]
fn plugin_filter_unknown_plugin_produces_no_findings() {
let output = Command::new(cha_binary())
.args([
"analyze",
&fixture("smelly.ts"),
"--plugin",
"nonexistent_plugin_xyz",
"--format",
"json",
])
.output()
.expect("failed to run cha");
let out = String::from_utf8_lossy(&output.stdout);
let parsed: serde_json::Value = serde_json::from_str(&out).expect("invalid JSON");
let findings = parsed["findings"].as_array().expect("missing findings");
assert!(
findings.is_empty(),
"expected no findings for unknown plugin"
);
}
#[test]
fn llm_output_contains_findings_section() {
let (_, out) = run_analyze(&fixture("smelly.ts"), "llm");
assert!(
out.contains("finding") || out.contains("smell") || out.contains("issue"),
"LLM output missing expected content"
);
}
#[test]
fn clean_file_exits_zero_with_fail_on_warning() {
let output = Command::new(cha_binary())
.args(["analyze", &fixture("clean.ts"), "--fail-on", "warning"])
.output()
.expect("failed to run cha");
assert_eq!(output.status.code().unwrap_or(-1), 0);
}
fn run_deps(args: &[&str]) -> String {
let output = Command::new(cha_binary())
.arg("deps")
.args(args)
.output()
.expect("failed to run cha deps");
String::from_utf8_lossy(&output.stdout).to_string()
}
#[test]
fn deps_direction_out_shows_only_outgoing() {
let dir = fixture_dir("c_oop");
let out = run_deps(&[
&dir,
"--type",
"imports",
"--filter",
"widget.c",
"--exact",
"--direction",
"out",
]);
assert!(
out.contains("widget.h"),
"direction=out should show widget.c → widget.h"
);
}
#[test]
fn deps_direction_in_shows_only_incoming() {
let dir = fixture_dir("c_oop");
let out = run_deps(&[
&dir,
"--type",
"imports",
"--filter",
"widget.h",
"--exact",
"--direction",
"in",
]);
assert!(
out.contains("widget.c"),
"direction=in should show widget.c → widget.h"
);
}
#[test]
fn deps_format_plantuml_has_startuml() {
let dir = fixture_dir("c_oop");
let out = run_deps(&[&dir, "--type", "imports", "--format", "plantuml"]);
assert!(
out.starts_with("@startuml"),
"plantuml output should start with @startuml"
);
assert!(
out.contains("@enduml"),
"plantuml output should end with @enduml"
);
}
#[test]
fn c_oop_filter_suppresses_lazy_class_for_struct_with_methods() {
let dir = fixture_dir("c_oop");
let (_, out) = run_analyze(&dir, "json");
assert!(
!out.contains("\"lazy_class\""),
"Widget should not be flagged as lazy_class because it has cross-file methods"
);
}
fn fixture_dir(name: &str) -> String {
let mut path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("tests/fixtures");
path.push(name);
path.to_string_lossy().to_string()
}