use codelore_lib::Options;
use codelore_lib::analyses::clone_coupling::run_clone_coupling;
use codelore_lib::facts::FactsDb;
use codelore_lib::repo::GixRepo;
#[test]
fn live_clone_pair_surfaces_in_clone_coupling() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path();
std::fs::create_dir_all(path.join("src/a")).unwrap();
std::fs::create_dir_all(path.join("src/b")).unwrap();
run_git(path, &["init", "-b", "main", "--quiet"]);
run_git(path, &["config", "user.email", "t@e.com"]);
run_git(path, &["config", "user.name", "T"]);
std::fs::create_dir_all(path.join("src/c")).unwrap();
write(
path.join("src/a/lib.rs"),
"fn add(a: i32, b: i32) -> i32 { let x = 1; let y = 2; a + b + x + y }\n",
);
write(
path.join("src/b/lib.rs"),
"fn mul(p: u64, q: u64) -> u64 { let s = 9; let t = 7; p + q + s + t }\n",
);
write(path.join("src/c/distractor.rs"), "fn distractor() {}\n");
run_git(path, &["add", "."]);
run_git(path, &["commit", "-m", "init", "--quiet"]);
for i in 0..5 {
let v = i + 10;
write(
path.join("src/a/lib.rs"),
&format!(
"fn add(a: i32, b: i32) -> i32 {{ let x = 1; let y = 2; a + b + x + y + {v} }}\n"
),
);
write(
path.join("src/b/lib.rs"),
&format!(
"fn mul(p: u64, q: u64) -> u64 {{ let s = 9; let t = 7; p + q + s + t + {v} }}\n"
),
);
run_git(path, &["add", "."]);
run_git(
path,
&["commit", "-m", &format!("paired update {i}"), "--quiet"],
);
}
for i in 0..5 {
write(
path.join("src/c/distractor.rs"),
&format!("fn distractor_{i}() {{}}\n"),
);
run_git(path, &["add", "."]);
run_git(
path,
&["commit", "-m", &format!("distractor {i}"), "--quiet"],
);
}
let repo = GixRepo::open(path).expect("gix open");
let db = FactsDb::new_in_memory().expect("db");
let opts = Options {
repo_path: path.to_path_buf(),
min_revs: 1,
min_shared_revs: 1, min_clone_node_count: 0,
min_clone_shared_revs: 1, ..Options::default()
};
db.ingest(&repo, &opts).expect("ingest");
let rows = run_clone_coupling(&db, &opts).expect("clone-coupling");
assert!(
!rows.is_empty(),
"live clone pair (5 co-changes, same AST shape) should surface"
);
let r = &rows[0];
assert!(r.file_a.contains("src/a/lib.rs"));
assert!(r.file_b.contains("src/b/lib.rs"));
assert!(r.combined_score > 0.0);
assert!(
(r.similarity - 1.0).abs() < f64::EPSILON,
"Type 1 + Type 2 → exact similarity, got {}",
r.similarity
);
assert!(
!r.at_risk,
"at_risk must default false for fixture with active author; got at_risk=true",
);
assert!(
r.p_value > 0.0,
"p_value must be the real Fisher exact test result, not 0.0 — got {}",
r.p_value
);
assert!(
r.p_value < 0.05,
"Fisher-filtered row should have p < 0.05, got {}",
r.p_value
);
let expected_score = r.similarity * r.degree_pct * (1.0 - r.p_value);
assert!(
(r.combined_score - expected_score).abs() < 1e-9,
"combined_score formula broken: got {}, expected {} = {} * {} * (1 - {})",
r.combined_score,
expected_score,
r.similarity,
r.degree_pct,
r.p_value,
);
}
#[test]
fn dead_clone_pair_does_not_surface() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path();
std::fs::create_dir_all(path.join("src/a")).unwrap();
std::fs::create_dir_all(path.join("src/b")).unwrap();
run_git(path, &["init", "-b", "main", "--quiet"]);
run_git(path, &["config", "user.email", "t@e.com"]);
run_git(path, &["config", "user.name", "T"]);
write(
path.join("src/a/lib.rs"),
"fn add(a: i32, b: i32) -> i32 { let x = 1; let y = 2; a + b + x + y }\n",
);
write(
path.join("src/b/lib.rs"),
"fn mul(p: u64, q: u64) -> u64 { let s = 9; let t = 7; p + q + s + t }\n",
);
run_git(path, &["add", "."]);
run_git(path, &["commit", "-m", "init", "--quiet"]);
for i in 0..3 {
let v = i + 100;
write(
path.join("src/a/lib.rs"),
&format!(
"fn add(a: i32, b: i32) -> i32 {{ let x = 1; let y = 2; a + b + x + y + {v} }}\n"
),
);
run_git(path, &["add", "."]);
run_git(path, &["commit", "-m", &format!("a-only {i}"), "--quiet"]);
}
for i in 0..3 {
let v = i + 200;
write(
path.join("src/b/lib.rs"),
&format!(
"fn mul(p: u64, q: u64) -> u64 {{ let s = 9; let t = 7; p + q + s + t + {v} }}\n"
),
);
run_git(path, &["add", "."]);
run_git(path, &["commit", "-m", &format!("b-only {i}"), "--quiet"]);
}
let repo = GixRepo::open(path).expect("gix open");
let db = FactsDb::new_in_memory().expect("db");
let opts = Options {
repo_path: path.to_path_buf(),
min_revs: 1,
min_shared_revs: 1,
min_clone_node_count: 0,
min_clone_shared_revs: 1,
..Options::default()
};
db.ingest(&repo, &opts).expect("ingest");
let rows = run_clone_coupling(&db, &opts).expect("clone-coupling");
assert!(
rows.is_empty(),
"dead clone pair (no co-changes) must NOT surface; got {rows:?}"
);
}
#[test]
fn same_dir_clones_filtered_by_default() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path();
std::fs::create_dir_all(path.join("src")).unwrap();
std::fs::create_dir_all(path.join("misc")).unwrap();
write(
path.join("src/a.rs"),
"fn add(a: i32, b: i32) -> i32 { let x = 1; let y = 2; a + b + x + y }\n",
);
write(
path.join("src/b.rs"),
"fn mul(p: u64, q: u64) -> u64 { let s = 9; let t = 7; p + q + s + t }\n",
);
write(path.join("misc/distractor.rs"), "fn distractor() {}\n");
run_git(path, &["init", "-b", "main", "--quiet"]);
run_git(path, &["config", "user.email", "t@e.com"]);
run_git(path, &["config", "user.name", "T"]);
run_git(path, &["add", "."]);
run_git(path, &["commit", "-m", "init", "--quiet"]);
for i in 0..5 {
write(
path.join("src/a.rs"),
&format!(
"fn add(a: i32, b: i32) -> i32 {{ let x = 1; let y = 2; a + b + x + y + {i} }}\n"
),
);
write(
path.join("src/b.rs"),
&format!(
"fn mul(p: u64, q: u64) -> u64 {{ let s = 9; let t = 7; p + q + s + t + {i} }}\n"
),
);
run_git(path, &["add", "."]);
run_git(path, &["commit", "-m", &format!("c{i}"), "--quiet"]);
}
for i in 0..5 {
write(
path.join("misc/distractor.rs"),
&format!("fn distractor_{i}() {{}}\n"),
);
run_git(path, &["add", "."]);
run_git(path, &["commit", "-m", &format!("d{i}"), "--quiet"]);
}
let repo = GixRepo::open(path).expect("gix open");
let db = FactsDb::new_in_memory().expect("db");
let opts = Options {
repo_path: path.to_path_buf(),
min_revs: 1,
min_shared_revs: 1,
min_clone_node_count: 0,
min_clone_shared_revs: 1,
clone_skip_same_dir: true, ..Options::default()
};
db.ingest(&repo, &opts).expect("ingest");
let rows = run_clone_coupling(&db, &opts).expect("clone-coupling");
assert!(
rows.is_empty(),
"same-dir clones should be filtered; got {} rows",
rows.len()
);
let opts2 = Options {
clone_skip_same_dir: false,
..opts
};
let rows2 = run_clone_coupling(&db, &opts2).expect("clone-coupling no-skip");
assert!(
!rows2.is_empty(),
"with clone_skip_same_dir=false, same-dir live clones should surface"
);
}
fn run_git(path: &std::path::Path, args: &[&str]) {
let status = std::process::Command::new("git")
.arg("-C")
.arg(path)
.args(args)
.status()
.expect("git");
assert!(status.success(), "git {args:?} failed");
}
fn write(p: std::path::PathBuf, content: &str) {
std::fs::write(p, content).unwrap();
}