//! End-to-end tests for `jmove fix` against a Java fixture. mod common; use common::{in_root, jmove, read}; use predicates::prelude::*; fn dirty_project() -> tempfile::TempDir { common::copy_fixture("java", "fix") } #[test] fn fix_dry_run_previews_deletions_without_writing() { let tmp = dirty_project(); jmove(&tmp, &["fix", "--dry-run"]) .success() .stdout(predicate::str::contains( "-import com.example.unused.Ghost;", )) .stdout(predicate::str::contains(" import java.util.List;")); // Disk untouched: the unused import is still there. let app = read(&in_root( tmp.path(), "src/main/java/com/example/app/App.java", )); assert!(app.contains("import com.example.unused.Ghost;"), "{app}"); } #[test] fn fix_removes_unused_and_keeps_used_and_string_mentions() { let tmp = dirty_project(); jmove(&tmp, &["fix"]) .success() .stdout(predicate::str::contains("fixed 1 issue in 1 file")); let app = read(&in_root( tmp.path(), "src/main/java/com/example/app/App.java", )); // Unused single-type import is gone, whole line removed. assert!(!app.contains("com.example.unused.Ghost"), "{app}"); // Used import stays. assert!(app.contains("import com.example.Text;"), "{app}"); // `List` only appears inside a string literal => kept (safe direction). assert!(app.contains("import java.util.List;"), "{app}"); assert!(app.contains("\"java.util.List\""), "{app}"); // Run 2: convergence — the deferred `java/import-order` fix (it // overlapped the deletion in run 1) now applies on the clean block. jmove(&tmp, &["fix"]) .success() .stdout(predicate::str::contains("fixed 1 issue in 1 file")); let app = read(&in_root( tmp.path(), "src/main/java/com/example/app/App.java", )); assert!( app.contains( "import static com.example.Text.shout;\n\nimport com.example.Text;\nimport java.util.List;\n" ), "{app}" ); // Run 3: fixed point — nothing left to change. jmove(&tmp, &["fix"]) .success() .stdout(predicate::str::contains("nothing to change")); // The project still checks clean. jmove(&tmp, &["check"]).success(); } #[test] fn fix_json_reports_candidates_and_applied_count() { let tmp = dirty_project(); jmove(&tmp, &["fix", "--dry-run", "--json"]) .success() .stdout( predicate::str::contains("\"status\": \"dry_run\"") .and(predicate::str::contains("\"operation\": \"fix\"")) .and(predicate::str::contains("\"rule\": \"java/unused-import\"")) .and(predicate::str::contains("\"would_fix\": 1")), ); jmove(&tmp, &["fix", "--json"]).success().stdout( predicate::str::contains("\"status\": \"ok\"") .and(predicate::str::contains("\"fixes\": 1")) .and(predicate::str::contains("\"files_changed\": 1")), ); let app = read(&in_root( tmp.path(), "src/main/java/com/example/app/App.java", )); assert!(!app.contains("Ghost"), "{app}"); } #[test] fn fix_unknown_rule_is_rejected() { let tmp = dirty_project(); jmove(&tmp, &["fix", "--rule", "does/not-exist"]) .failure() .code(1) .stderr(predicate::str::contains( "unknown fix rule 'does/not-exist'", )) .stderr(predicate::str::contains("java/unused-import")); // Nothing on disk changed. let app = read(&in_root( tmp.path(), "src/main/java/com/example/app/App.java", )); assert!(app.contains("Ghost"), "{app}"); } #[test] fn fix_scoped_to_rule_and_reports_guava_like_sibling() { // `Text` is referenced only by a static member import of the same type, // so neither import is provably dead (mirrors the Guava smoke note). let tmp = dirty_project(); let app = read(&in_root( tmp.path(), "src/main/java/com/example/app/App.java", )); assert!( app.contains("import static com.example.Text.shout;"), "{app}" ); jmove(&tmp, &["fix", "--rule", "java/unused-import"]) .success() .stdout(predicate::str::contains("fixed 1 issue in 1 file")); let app = read(&in_root( tmp.path(), "src/main/java/com/example/app/App.java", )); assert!(app.contains("import com.example.Text;"), "{app}"); assert!( app.contains("import static com.example.Text.shout;"), "{app}" ); } fn missing_project() -> tempfile::TempDir { common::copy_fixture("java", "fix_missing") } #[test] fn missing_import_adds_unique_and_reports_ambiguous() { let tmp = missing_project(); jmove(&tmp, &["fix", "--rule", "java/missing-import", "--json"]) .success() .stdout( predicate::str::contains("\"rule\": \"java/missing-import\"") .and(predicate::str::contains("\"applied\": false")) .and(predicate::str::contains("\"com.example.a.Config\"")) .and(predicate::str::contains("\"com.example.b.Config\"")), ); let calc = read(&in_root( tmp.path(), "src/main/java/com/example/app/Calc.java", )); assert!( calc.contains("package com.example.app;\nimport com.example.util.Maths;\n"), "{calc}" ); // The ambiguous `Config` is never guessed at. let refer = read(&in_root( tmp.path(), "src/main/java/com/example/c/Refer.java", )); assert!(!refer.contains("import com.example."), "{refer}"); jmove(&tmp, &["check"]).success(); } #[test] fn missing_import_dry_run_then_stable_reapply() { let tmp = missing_project(); jmove(&tmp, &["fix", "--dry-run"]) .success() .stdout(predicate::str::contains("+import com.example.util.Maths;")); jmove(&tmp, &["fix"]) .success() .stdout(predicate::str::contains("fixed 3 issues in 2 files")); // Only manual (ambiguous) findings remain: nothing further applies. jmove(&tmp, &["fix"]) .success() .stdout(predicate::str::contains("nothing to change")); } #[test] fn unused_delete_and_missing_insert_coexist_in_one_file() { // Dual.java: the unused import is deleted while the missing `Maths` // import lands at the very byte of the deleted line's end — adjacent, // not overlapping, so one atomic plan carries both. let tmp = missing_project(); let dual = "src/main/java/com/example/app/Dual.java"; jmove(&tmp, &["fix"]).success(); let text = read(&in_root(tmp.path(), dual)); assert!(!text.contains("Gone"), "{text}"); assert!(text.contains("import com.example.util.Maths;"), "{text}"); jmove(&tmp, &["check"]).success(); } #[test] fn three_rules_converge_on_one_project() { // App.java: unused import inside an out-of-order block (the order // rewrite overlaps the deletion, so it defers to run 2). // C.java: out-of-order block + a bare `Maths` reference (insertion at // the block boundary coexists with the rewrite in run 1). let tmp = common::copy_fixture("java", "order"); jmove(&tmp, &["fix", "--dry-run", "--json"]) .success() .stdout( predicate::str::contains("\"rule\": \"java/import-order\"") .and(predicate::str::contains("\"applied\": false")) .and(predicate::str::contains("skipped")), ); let app = "src/main/java/com/example/app/App.java"; let c = "src/main/java/com/example/app/C.java"; jmove(&tmp, &["fix"]) .success() .stdout(predicate::str::contains("fixed 3 issues in 2 files")); let app_text = read(&in_root(tmp.path(), app)); // Unused is gone; the deferred order fix has not touched the block. assert!(!app_text.contains("Unneeded"), "{app_text}"); assert!( app_text.contains("import java.util.List;\nimport com.example.util.Maths;"), "{app_text}" ); jmove(&tmp, &["fix"]) .success() .stdout(predicate::str::contains("fixed 2 issues in 2 files")); let c_text = read(&in_root(tmp.path(), c)); assert!( c_text.contains( "import com.example.util.Maths;\nimport java.util.List;\nimport java.util.Map;\n" ), "{c_text}" ); let app_text = read(&in_root(tmp.path(), app)); assert!( app_text.contains( "import com.example.util.Maths;\nimport java.util.List;\nimport java.util.Map;\n" ), "{app_text}" ); // Fixed point. jmove(&tmp, &["fix"]) .success() .stdout(predicate::str::contains("nothing to change")); jmove(&tmp, &["check"]).success(); } fn ts_unused() -> tempfile::TempDir { common::copy_fixture("typescript", "unused") } #[test] fn ts_unused_import_deletes_dead_type_import_and_keeps_the_rest() { let tmp = ts_unused(); // The whole-statement delete must fire for the dead `Ghost` type import. jmove(&tmp, &["fix", "--rule", "ts/unused-import", "--json"]) .success() .stdout( predicate::str::contains("\"rule\": \"ts/unused-import\"") .and(predicate::str::contains("\"applied\": true")), ); let app = read(&in_root(tmp.path(), "src/app.ts")); assert!(!app.contains("Ghost"), "{app}"); // Side-effect import, the mixed used/unused statement and the default // class import all stay (under-delete safety). assert!(app.contains("import './side-effects';"), "{app}"); assert!(app.contains("unused"), "{app}"); assert!(app.contains("import Logger"), "{app}"); jmove(&tmp, &["check"]).success(); }