feat(java): Phase 1.5 — tree-sitter Java frontend, package-aware mv, e2e fixtures
This commit is contained in:
parent
55670030c3
commit
603f916ee9
25 changed files with 1288 additions and 366 deletions
86
tests/cli.rs
86
tests/cli.rs
|
|
@ -1,71 +1,19 @@
|
|||
//! End-to-end tests for the `jmove` CLI against the TypeScript fixtures.
|
||||
//!
|
||||
//! Each test copies a fixture tree into a fresh tempdir, runs the real
|
||||
//! binary with `--root <tmp>` and asserts on exit codes, stdout/stderr and
|
||||
//! the resulting files on disk.
|
||||
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
mod common;
|
||||
|
||||
use assert_cmd::Command;
|
||||
use assert_cmd::assert::Assert;
|
||||
use common::{copy_fixture, fixture_dir, in_root, jmove, read};
|
||||
use predicates::prelude::*;
|
||||
use tempfile::TempDir;
|
||||
|
||||
/// Recursively copy `tests/typescript/<name>` into a tempdir and return it.
|
||||
///
|
||||
/// An empty `.git` marker is created in the copy: the `ignore` crate only
|
||||
/// applies `.gitignore` rules inside a git repository by default, and the
|
||||
/// `normal` fixture relies on its `node_modules/` rule being effective.
|
||||
fn copy_fixture(name: &str) -> TempDir {
|
||||
let tmp = TempDir::new().expect("tempdir");
|
||||
let from = fixture_dir(name);
|
||||
copy_dir(&from, tmp.path());
|
||||
fs::create_dir(tmp.path().join(".git")).expect("git marker");
|
||||
tmp
|
||||
}
|
||||
|
||||
/// Source path of a TypeScript fixture tree inside the repository.
|
||||
fn fixture_dir(name: &str) -> PathBuf {
|
||||
Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests/typescript")
|
||||
.join(name)
|
||||
}
|
||||
|
||||
/// Recursive file/dir copy; plain `std::fs` only, no symlinks in fixtures.
|
||||
fn copy_dir(from: &Path, to: &Path) {
|
||||
fs::create_dir_all(to).expect("create dir");
|
||||
for entry in fs::read_dir(from).expect("read dir") {
|
||||
let entry = entry.expect("entry");
|
||||
let target = to.join(entry.file_name());
|
||||
if entry.file_type().expect("file type").is_dir() {
|
||||
copy_dir(&entry.path(), &target);
|
||||
} else {
|
||||
fs::copy(entry.path(), target).expect("copy file");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Run `jmove --root <dir> <args…>` and return an assertable outcome.
|
||||
fn jmove(root: &TempDir, args: &[&str]) -> Assert {
|
||||
let mut cmd = Command::cargo_bin("jmove").expect("jmove binary");
|
||||
cmd.arg("--root").arg(root.path()).args(args);
|
||||
cmd.assert()
|
||||
}
|
||||
|
||||
/// Read `path` as a string, panicking with the path on failure.
|
||||
fn read(path: &Path) -> String {
|
||||
fs::read_to_string(path).unwrap_or_else(|err| panic!("read {}: {err}", path.display()))
|
||||
}
|
||||
|
||||
/// Build a path inside the tempdir root from a `/`-separated relative path.
|
||||
fn in_root(root: &Path, rel: &str) -> PathBuf {
|
||||
root.join(rel)
|
||||
/// Convenience wrapper: a TypeScript fixture by name.
|
||||
fn fixture(name: &str) -> TempDir {
|
||||
copy_fixture("typescript", name)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mv_dry_run_prints_diff_and_leaves_disk_untouched() {
|
||||
let tmp = copy_fixture("basic");
|
||||
let tmp = fixture("basic");
|
||||
jmove(&tmp, &["mv", "lib/sum.ts", "utils/sum.ts", "--dry-run"])
|
||||
.success()
|
||||
.stdout(predicate::str::contains("./lib/sum"))
|
||||
|
|
@ -80,14 +28,14 @@ fn mv_dry_run_prints_diff_and_leaves_disk_untouched() {
|
|||
);
|
||||
assert_eq!(
|
||||
read(&in_root(tmp.path(), "app.ts")),
|
||||
read(&fixture_dir("basic").join("app.ts")),
|
||||
read(&fixture_dir("typescript", "basic").join("app.ts")),
|
||||
"importer untouched"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mv_rewrites_importer_and_moves_the_file() {
|
||||
let tmp = copy_fixture("basic");
|
||||
let tmp = fixture("basic");
|
||||
jmove(&tmp, &["mv", "lib/sum.ts", "utils/sum.ts"])
|
||||
.success()
|
||||
.stdout(predicate::str::contains("moved lib/sum.ts -> utils/sum.ts"))
|
||||
|
|
@ -109,7 +57,7 @@ fn mv_rewrites_importer_and_moves_the_file() {
|
|||
|
||||
#[test]
|
||||
fn mv_updates_barrel_and_never_touches_node_modules() {
|
||||
let tmp = copy_fixture("normal");
|
||||
let tmp = fixture("normal");
|
||||
jmove(&tmp, &["mv", "src/impl/core.ts", "src/impl/calc.ts"]).success();
|
||||
assert!(
|
||||
read(&in_root(tmp.path(), "src/impl/index.ts")).contains("./calc"),
|
||||
|
|
@ -128,7 +76,7 @@ fn mv_updates_barrel_and_never_touches_node_modules() {
|
|||
|
||||
#[test]
|
||||
fn mv_rewrites_only_the_specifier_line_in_multi_line_imports() {
|
||||
let tmp = copy_fixture("complex");
|
||||
let tmp = fixture("complex");
|
||||
let view = in_root(tmp.path(), "src/ui/deep/nested/view.ts");
|
||||
let before: Vec<String> = read(&view).lines().map(str::to_owned).collect();
|
||||
|
||||
|
|
@ -154,12 +102,12 @@ fn mv_rewrites_only_the_specifier_line_in_multi_line_imports() {
|
|||
|
||||
#[test]
|
||||
fn check_passes_on_clean_and_fails_on_broken_fixture() {
|
||||
let clean = copy_fixture("basic");
|
||||
let clean = fixture("basic");
|
||||
jmove(&clean, &["check"])
|
||||
.success()
|
||||
.stdout(predicate::str::contains("no broken imports"));
|
||||
|
||||
let messy = copy_fixture("complex");
|
||||
let messy = fixture("complex");
|
||||
jmove(&messy, &["check"])
|
||||
.code(2)
|
||||
.stdout(predicate::str::contains(
|
||||
|
|
@ -169,7 +117,7 @@ fn check_passes_on_clean_and_fails_on_broken_fixture() {
|
|||
|
||||
#[test]
|
||||
fn check_json_reports_broken_import_payload() {
|
||||
let tmp = copy_fixture("complex");
|
||||
let tmp = fixture("complex");
|
||||
jmove(&tmp, &["check", "--json"]).code(2).stdout(
|
||||
predicate::str::contains("\"status\": \"ok\"")
|
||||
.and(predicate::str::contains("\"operation\": \"check\""))
|
||||
|
|
@ -183,7 +131,7 @@ fn check_json_reports_broken_import_payload() {
|
|||
|
||||
#[test]
|
||||
fn mv_json_happy_path_reports_changed_files() {
|
||||
let tmp = copy_fixture("basic");
|
||||
let tmp = fixture("basic");
|
||||
jmove(&tmp, &["mv", "lib/sum.ts", "utils/sum.ts", "--json"])
|
||||
.success()
|
||||
.stdout(
|
||||
|
|
@ -203,7 +151,7 @@ fn mv_json_happy_path_reports_changed_files() {
|
|||
|
||||
#[test]
|
||||
fn mv_json_dry_run_reports_preview_payload() {
|
||||
let tmp = copy_fixture("basic");
|
||||
let tmp = fixture("basic");
|
||||
jmove(
|
||||
&tmp,
|
||||
&["mv", "lib/sum.ts", "utils/sum.ts", "--dry-run", "--json"],
|
||||
|
|
@ -224,7 +172,7 @@ fn mv_json_dry_run_reports_preview_payload() {
|
|||
|
||||
#[test]
|
||||
fn mv_json_reports_target_exists_error_shape() {
|
||||
let tmp = copy_fixture("basic");
|
||||
let tmp = fixture("basic");
|
||||
jmove(&tmp, &["mv", "app.ts", "lib/sum.ts", "--json"])
|
||||
.code(1)
|
||||
.stdout(
|
||||
|
|
@ -241,7 +189,7 @@ fn mv_json_reports_target_exists_error_shape() {
|
|||
|
||||
#[test]
|
||||
fn mv_reports_source_not_found() {
|
||||
let tmp = copy_fixture("basic");
|
||||
let tmp = fixture("basic");
|
||||
jmove(&tmp, &["mv", "lib/nope.ts", "utils/nope.ts", "--json"])
|
||||
.code(1)
|
||||
.stdout(predicate::str::contains("\"code\": \"SOURCE_NOT_FOUND\""));
|
||||
|
|
|
|||
151
tests/cli_java.rs
Normal file
151
tests/cli_java.rs
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
//! End-to-end tests for the `jmove` CLI against the Java fixtures.
|
||||
|
||||
mod common;
|
||||
|
||||
use common::{copy_fixture, in_root, jmove, read};
|
||||
use predicates::prelude::*;
|
||||
|
||||
fn fixture(name: &str) -> tempfile::TempDir {
|
||||
copy_fixture("java", name)
|
||||
}
|
||||
|
||||
const TEXT: &str = "src/main/java/com/example/util/Text.java";
|
||||
const MOVED: &str = "src/main/java/com/example/core/Text.java";
|
||||
|
||||
#[test]
|
||||
fn mv_between_packages_updates_declaration_and_every_importer() {
|
||||
let tmp = fixture("basic");
|
||||
jmove(&tmp, &["mv", TEXT, MOVED])
|
||||
.success()
|
||||
.stdout(predicate::str::contains("4 imports in 3 files"));
|
||||
|
||||
let moved = read(&in_root(tmp.path(), MOVED));
|
||||
assert!(moved.contains("package com.example.core;"), "{moved}");
|
||||
assert!(!moved.contains("com.example.util"), "{moved}");
|
||||
|
||||
let app = read(&in_root(
|
||||
tmp.path(),
|
||||
"src/main/java/com/example/app/App.java",
|
||||
));
|
||||
assert!(app.contains("import com.example.core.Text;"), "{app}");
|
||||
assert!(
|
||||
app.contains("import static com.example.core.Text.shout;"),
|
||||
"{app}"
|
||||
);
|
||||
// Specifiers that never named the moved class are byte-identical.
|
||||
assert!(app.contains("import com.example.unknown.*;"), "{app}");
|
||||
assert!(app.contains("import java.util.List;"), "{app}");
|
||||
|
||||
let user = read(&in_root(
|
||||
tmp.path(),
|
||||
"src/main/java/com/example/model/User.java",
|
||||
));
|
||||
assert!(user.contains("import com.example.core.Text;"), "{user}");
|
||||
assert!(!in_root(tmp.path(), TEXT).exists());
|
||||
|
||||
// Unresolvable Java imports (jdk, wildcard) are not "broken".
|
||||
jmove(&tmp, &["check"]).success();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mv_dry_run_previews_all_three_edits_without_writing() {
|
||||
let tmp = fixture("basic");
|
||||
jmove(&tmp, &["mv", TEXT, MOVED, "--dry-run"])
|
||||
.success()
|
||||
.stdout(predicate::str::contains("-package com.example.util;"))
|
||||
.stdout(predicate::str::contains("+package com.example.core;"))
|
||||
.stdout(predicate::str::contains("-import com.example.util.Text;"))
|
||||
.stdout(predicate::str::contains("+import com.example.core.Text;"))
|
||||
.stdout(predicate::str::contains(format!("move {TEXT} -> {MOVED}")));
|
||||
assert!(in_root(tmp.path(), TEXT).is_file());
|
||||
assert!(!in_root(tmp.path(), MOVED).exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mv_json_reports_package_and_importer_changes() {
|
||||
let tmp = fixture("basic");
|
||||
jmove(&tmp, &["mv", TEXT, MOVED, "--json"])
|
||||
.success()
|
||||
.stdout(
|
||||
predicate::str::contains("\"old\": \"com.example.util\"")
|
||||
.and(predicate::str::contains("\"new\": \"com.example.core\""))
|
||||
.and(predicate::str::contains(
|
||||
"\"old\": \"com.example.util.Text.shout\"",
|
||||
))
|
||||
.and(predicate::str::contains(
|
||||
"\"new\": \"com.example.core.Text.shout\"",
|
||||
))
|
||||
.and(predicate::str::contains("\"updated_imports\": 4")),
|
||||
);
|
||||
let changed = read(&in_root(tmp.path(), MOVED));
|
||||
assert!(changed.contains("package com.example.core"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_inside_same_package_keeps_package_declaration() {
|
||||
let tmp = fixture("basic");
|
||||
jmove(
|
||||
&tmp,
|
||||
&["mv", TEXT, "src/main/java/com/example/util/Paragraph.java"],
|
||||
)
|
||||
.success();
|
||||
let renamed = read(&in_root(
|
||||
tmp.path(),
|
||||
"src/main/java/com/example/util/Paragraph.java",
|
||||
));
|
||||
assert!(renamed.contains("package com.example.util;"), "{renamed}");
|
||||
let app = read(&in_root(
|
||||
tmp.path(),
|
||||
"src/main/java/com/example/app/App.java",
|
||||
));
|
||||
assert!(app.contains("import com.example.util.Paragraph;"), "{app}");
|
||||
assert!(
|
||||
app.contains("import static com.example.util.Paragraph.shout;"),
|
||||
"{app}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn target_outside_the_source_root_is_rejected() {
|
||||
let tmp = fixture("basic");
|
||||
jmove(&tmp, &["mv", TEXT, "webapp/core/Text.java", "--json"])
|
||||
.code(1)
|
||||
.stdout(
|
||||
predicate::str::contains("\"code\": \"PLAN_REJECTED\"")
|
||||
.and(predicate::str::contains("source root")),
|
||||
);
|
||||
assert!(in_root(tmp.path(), TEXT).is_file());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn java_file_must_keep_the_java_extension() {
|
||||
let tmp = fixture("basic");
|
||||
jmove(
|
||||
&tmp,
|
||||
&["mv", TEXT, "src/main/java/com/example/core/Text.txt"],
|
||||
)
|
||||
.code(1)
|
||||
.stderr(predicate::str::contains(".java extension"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_package_file_cannot_change_directory() {
|
||||
let tmp = fixture("basic");
|
||||
jmove(
|
||||
&tmp,
|
||||
&[
|
||||
"mv",
|
||||
"src/main/java/Main.java",
|
||||
"src/main/java/app/Main.java",
|
||||
],
|
||||
)
|
||||
.code(1)
|
||||
.stderr(predicate::str::contains("default package"));
|
||||
// ...but an in-place rename is fine and needs no rewrites.
|
||||
jmove(
|
||||
&tmp,
|
||||
&["mv", "src/main/java/Main.java", "src/main/java/Run.java"],
|
||||
)
|
||||
.success();
|
||||
assert!(in_root(tmp.path(), "src/main/java/Run.java").is_file());
|
||||
}
|
||||
64
tests/common/mod.rs
Normal file
64
tests/common/mod.rs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
//! Shared helpers for the CLI integration tests.
|
||||
//!
|
||||
//! Each test copies a fixture tree into a fresh tempdir, runs the real
|
||||
//! binary with `--root <tmp>` and asserts on exit codes, stdout/stderr and
|
||||
//! the resulting files on disk.
|
||||
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use assert_cmd::Command;
|
||||
use assert_cmd::assert::Assert;
|
||||
use tempfile::TempDir;
|
||||
|
||||
/// Recursively copy `tests/<language>/<name>` into a tempdir and return it.
|
||||
///
|
||||
/// An empty `.git` marker is created in the copy: the `ignore` crate only
|
||||
/// applies `.gitignore` rules inside a git repository by default, and the
|
||||
/// typescript `normal` fixture relies on its `node_modules/` rule being
|
||||
/// effective.
|
||||
pub fn copy_fixture(language: &str, name: &str) -> TempDir {
|
||||
let tmp = TempDir::new().expect("tempdir");
|
||||
copy_dir(&fixture_dir(language, name), tmp.path());
|
||||
fs::create_dir(tmp.path().join(".git")).expect("git marker");
|
||||
tmp
|
||||
}
|
||||
|
||||
/// Source path of a fixture tree inside the repository.
|
||||
pub fn fixture_dir(language: &str, name: &str) -> PathBuf {
|
||||
Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests")
|
||||
.join(language)
|
||||
.join(name)
|
||||
}
|
||||
|
||||
/// Recursive file/dir copy; plain `std::fs` only, no symlinks in fixtures.
|
||||
fn copy_dir(from: &Path, to: &Path) {
|
||||
fs::create_dir_all(to).expect("create dir");
|
||||
for entry in fs::read_dir(from).expect("read dir") {
|
||||
let entry = entry.expect("entry");
|
||||
let target = to.join(entry.file_name());
|
||||
if entry.file_type().expect("file type").is_dir() {
|
||||
copy_dir(&entry.path(), &target);
|
||||
} else {
|
||||
fs::copy(entry.path(), target).expect("copy file");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Run `jmove --root <dir> <args…>` and return an assertable outcome.
|
||||
pub fn jmove(root: &TempDir, args: &[&str]) -> Assert {
|
||||
let mut cmd = Command::cargo_bin("jmove").expect("jmove binary");
|
||||
cmd.arg("--root").arg(root.path()).args(args);
|
||||
cmd.assert()
|
||||
}
|
||||
|
||||
/// Read `path` as a string, panicking with the path on failure.
|
||||
pub fn read(path: &Path) -> String {
|
||||
fs::read_to_string(path).unwrap_or_else(|err| panic!("read {}: {err}", path.display()))
|
||||
}
|
||||
|
||||
/// Build a path inside the tempdir root from a `/`-separated relative path.
|
||||
pub fn in_root(root: &Path, rel: &str) -> PathBuf {
|
||||
root.join(rel)
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
> Planned Phase 1.5 fixture tree. `jmove` does not support Java yet; this folder intentionally holds only this note so nothing is dead code. See `docs/PLAN.md`.
|
||||
6
tests/java/basic/src/main/java/Main.java
Normal file
6
tests/java/basic/src/main/java/Main.java
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// No package declaration: the default package, un-importable.
|
||||
public class Main {
|
||||
public static void main(String[] args) {
|
||||
System.out.println("hi");
|
||||
}
|
||||
}
|
||||
10
tests/java/basic/src/main/java/com/example/app/App.java
Normal file
10
tests/java/basic/src/main/java/com/example/app/App.java
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
package com.example.app;
|
||||
|
||||
import com.example.util.Text;
|
||||
import static com.example.util.Text.shout;
|
||||
import com.example.unknown.*;
|
||||
import java.util.List;
|
||||
|
||||
public class App {
|
||||
private final List<String> items = List.of(shout("hi"), new Text().render());
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.example.model;
|
||||
|
||||
import com.example.util.Text;
|
||||
|
||||
public class User {
|
||||
String loudName() {
|
||||
return Text.shout("user");
|
||||
}
|
||||
}
|
||||
11
tests/java/basic/src/main/java/com/example/util/Text.java
Normal file
11
tests/java/basic/src/main/java/com/example/util/Text.java
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
package com.example.util;
|
||||
|
||||
public class Text {
|
||||
public static String shout(String s) {
|
||||
return s.toUpperCase();
|
||||
}
|
||||
|
||||
public String render() {
|
||||
return "text";
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue