use std::process::Command;
use assert_cmd::Command as AssertCommand;
use predicates::prelude::*;
use tempfile::TempDir;
const BIN: &str = env!("CARGO_BIN_EXE_cure");
fn cure() -> AssertCommand {
AssertCommand::new(BIN)
}
fn tmp_java(dir: &TempDir, name: &str, content: &str) -> std::path::PathBuf {
let p = dir.path().join(name);
std::fs::write(&p, content).unwrap();
p
}
#[test]
fn help_and_version() {
cure()
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("cure"));
cure()
.arg("--version")
.assert()
.success()
.stdout(predicate::str::contains("cure"));
}
#[test]
fn stdin_to_stdout() {
let src = "class A{int m(){int a=foo();int b=a;return b;}}";
cure()
.arg("-")
.write_stdin(src)
.assert()
.success()
.stdout(predicate::str::contains("return foo();"))
.stdout(predicate::str::contains("class A {"));
}
#[test]
fn file_to_stdout() {
let dir = TempDir::new().unwrap();
let p = tmp_java(
&dir,
"A.java",
r#"
class A {
boolean check(int v) {
boolean b = v > 10;
if (b) {
return true;
} else {
return false;
}
}
}
"#,
);
cure()
.arg(p)
.assert()
.success()
.stdout(predicate::str::contains("return v > 10;"));
}
#[test]
fn broken_source_still_succeeds_and_skips() {
let dir = TempDir::new().unwrap();
let p = tmp_java(
&dir,
"Bad.java",
r#"
class Bad {
void broken() {
int x = ;
}
int good() {
int a = foo();
int b = a;
return b;
}
}
"#,
);
cure()
.arg(p)
.assert()
.success()
.stdout(predicate::str::contains("return foo();"))
.stdout(predicate::str::contains("int x = ;"))
.stderr(predicate::str::contains("语法问题已跳过"));
}
#[test]
fn strict_mode_fails_on_errors() {
let dir = TempDir::new().unwrap();
let p = tmp_java(&dir, "Bad.java", "class {{{ totally broken");
cure()
.arg("--strict")
.arg(p)
.assert()
.failure()
.code(1);
}
#[test]
fn check_mode_exit_code() {
let dir = TempDir::new().unwrap();
let dirty = tmp_java(&dir, "Dirty.java", "class A{int m(){return 1+0;}}");
cure()
.arg("--check")
.arg(&dirty)
.assert()
.code(2);
let clean = tmp_java(&dir, "Clean.java", "class A {\n int m() {\n return 1;\n }\n}\n");
cure()
.arg("--check")
.arg(clean)
.assert()
.success();
}
#[test]
fn write_in_place() {
let dir = TempDir::new().unwrap();
let p = tmp_java(&dir, "A.java", "class A{int m(){int a=1;int b=a;return b;}}");
cure()
.arg("--write")
.arg(&p)
.assert()
.success();
let after = std::fs::read_to_string(&p).unwrap();
assert!(after.contains("return 1;"), "{after}");
}
#[test]
fn format_only() {
let dir = TempDir::new().unwrap();
let p = tmp_java(&dir, "F.java", "class F{int m(){return 1+0;}}");
cure()
.arg("--format-only")
.arg(p)
.assert()
.success()
.stdout(predicate::str::contains("return 1 + 0;"))
.stdout(predicate::str::contains("class F {"));
}
#[test]
fn disable_rule() {
let dir = TempDir::new().unwrap();
let p = tmp_java(&dir, "D.java", "class D{int m(){int a=5;return a;}}");
cure()
.arg("--disable")
.arg("local_propagation")
.arg(p)
.assert()
.success()
.stdout(predicate::str::contains("int a = 5;"));
}
#[test]
fn report_flag() {
let dir = TempDir::new().unwrap();
let p = tmp_java(&dir, "R.java", "class R{int m(){int a=5;return a;}}");
cure()
.arg("--report")
.arg(p)
.assert()
.success()
.stderr(predicate::str::contains("local_propagation"));
}
#[test]
fn invalid_option() {
cure()
.arg("--nope")
.assert()
.failure()
.code(2)
.stderr(predicate::str::contains("未知选项"));
}
#[test]
fn real_invocation_via_command() {
let out = Command::new(BIN)
.arg("--version")
.output()
.expect("spawn cure");
assert!(out.status.success());
assert!(String::from_utf8_lossy(&out.stdout).starts_with("cure "));
}
#[test]
fn no_args_prints_help_and_exits_zero() {
cure()
.env("CURE_LANG", "en")
.assert()
.success()
.stdout(predicate::str::contains("https://github.com/ejfkdev/cure"))
.stdout(predicate::str::contains("Examples:"));
}
#[test]
fn help_word_and_flag_are_equivalent() {
for arg in ["help", "-h", "--help"] {
cure()
.env("CURE_LANG", "en")
.arg(arg)
.assert()
.success()
.stdout(predicate::str::contains("cure"))
.stdout(predicate::str::contains("https://github.com/ejfkdev/cure"))
.stdout(predicate::str::contains("Examples:"));
}
}
#[test]
fn help_zh_follows_cure_lang() {
cure()
.env("CURE_LANG", "zh")
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("代码简化 / 格式化器"))
.stdout(predicate::str::contains("示例:"));
}
#[test]
fn version_word_and_flag() {
let want = format!("cure {}", env!("CARGO_PKG_VERSION"));
for arg in ["version", "-V", "--version"] {
cure()
.env("CURE_LANG", "en")
.arg(arg)
.assert()
.success()
.stdout(predicate::str::contains(want.as_str()))
.stdout(predicate::str::contains("https://github.com/ejfkdev/cure"));
}
}
#[test]
fn rules_subcommand_lists_catalog() {
for args in [&["rules"][..], &["help", "rules"][..], &["rules", "-h"][..]] {
cure()
.env("CURE_LANG", "en")
.args(args)
.assert()
.success()
.stdout(predicate::str::contains("simplification rules"))
.stdout(predicate::str::contains("const_fold_bin"))
.stdout(predicate::str::contains("cff_recover"))
.stdout(predicate::str::contains("Enabled by default"))
.stdout(predicate::str::contains("--disable"));
}
}
#[test]
fn unknown_help_topic_is_usage_error() {
cure()
.env("CURE_LANG", "en")
.args(["help", "definitely-not-a-topic"])
.assert()
.failure()
.code(2)
.stderr(predicate::str::contains("unknown help topic"));
}
#[test]
fn default_dir_output_suffix_is_cure_out() {
let parent = TempDir::new().unwrap();
let src = parent.path().join("src");
std::fs::create_dir_all(&src).unwrap();
std::fs::write(src.join("A.java"), "class A{int m(){return 1;}}").unwrap();
cure()
.arg(&src)
.assert()
.success();
let out_root = parent.path().join("src-cure-out");
assert!(out_root.is_dir(), "默认输出根应为 {out_root:?}");
let out = out_root.join("A.java");
assert!(out.is_file());
assert_eq!(std::fs::read_to_string(&out).unwrap().trim_end(), "class A {\n int m() {\n return 1;\n }\n}");
assert!(!parent.path().join("src-cure").exists());
}
#[test]
fn broken_pipe_exits_quietly() {
let dir = TempDir::new().unwrap();
let p = tmp_java(&dir, "A.java", "class A{int m(){int a=foo();int b=a;return b;}}");
let out = Command::new("sh")
.arg("-c")
.arg(format!("{BIN} {:?} | head -1", p.as_os_str()))
.output()
.unwrap();
assert!(out.status.success(), "pipe exit: {}", out.status);
assert!(!String::from_utf8_lossy(&out.stderr).contains("failed printing"));
}
#[test]
fn single_file_with_unimplemented_ext_is_clear_error() {
let dir = TempDir::new().unwrap();
let p = dir.path().join("foo.rs");
std::fs::write(&p, "fn main() {}").unwrap();
cure()
.env("CURE_LANG", "en")
.arg(&p)
.assert()
.failure()
.code(2)
.stderr(predicate::str::contains("no language backend"))
.stderr(predicate::str::contains("java"));
let p2 = dir.path().join("noext");
std::fs::write(&p2, "class A{int m(){return 1;}}").unwrap();
cure()
.arg(&p2)
.assert()
.success()
.stdout(predicate::str::contains("return 1;"));
}
#[test]
fn stats_reports_structural_metrics_and_classification() {
let dir = TempDir::new().unwrap();
let p = tmp_java(
&dir,
"Noise.java",
"class Noise{int f(){int a=0;if(2<3){a=1;}a+=1;a-=1;return a;}}",
);
let out = cure()
.env("CURE_LANG", "zh")
.arg("--stats")
.arg(&p)
.assert()
.success()
.get_output()
.clone();
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("节点"), "{stderr}");
assert!(stderr.contains("判定点"), "{stderr}");
assert!(stderr.contains("简化质量:结构简化 1"), "{stderr}");
assert!(
stderr.contains("判定点 1 → 0"),
"if(常量谓词) 折叠应减判定点: {stderr}"
);
let p2 = tmp_java(&dir, "Fmt.java", "class Fmt{int m(){return foo();}}");
let out2 = cure()
.env("CURE_LANG", "zh")
.arg("--stats")
.arg(&p2)
.assert()
.success()
.get_output()
.clone();
let stderr2 = String::from_utf8_lossy(&out2.stderr);
assert!(
stderr2.contains("简化质量:结构简化 0 / 仅格式 1 / 未变 0"),
"{stderr2}"
);
assert!(stderr2.contains("节点 4 → 4"), "{stderr2}");
}
#[test]
fn metrics_unit_counting() {
let mut out = cure_java_parser::parse(
"class A{int f(int x){if(x>0){x=1;}while(x<3){x=x+1;}for(int i=0;i<2;i++){}\
switch(x){case 1:break;default:break;}return x>0?1:(x&&true?2:3);}}",
);
let before = cure_java_ast::unit_metrics(&out.ast, &out.unit);
assert_eq!(before.decisions, 8, "{before:?}");
assert!(before.nodes > 40, "{before:?}");
assert!(before.max_depth >= 2, "{before:?}");
let _ = &mut out;
}