use super::*;
use crate::loc::language::detect;
use crate::walk::{self, ExcludeFilter, WalkConfig};
use std::fs;
use std::path::Path;
#[test]
fn run_on_empty_dir() {
let dir = tempfile::tempdir().unwrap();
let filter = ExcludeFilter::default();
let cfg = WalkConfig::new(dir.path(), false, &filter);
run(&cfg, 6, false, false, false, gate()).unwrap();
}
#[test]
fn run_with_no_duplicates() {
let dir = tempfile::tempdir().unwrap();
fs::write(
dir.path().join("a.rs"),
"fn foo() {\n let x = 1;\n let y = 2;\n let z = x + y;\n println!(\"{}\", z);\n return z;\n}\n",
).unwrap();
fs::write(
dir.path().join("b.rs"),
"fn bar() {\n let a = 10;\n let b = 20;\n let c = a * b;\n println!(\"{}\", c);\n return c;\n}\n",
).unwrap();
let filter = ExcludeFilter::default();
let cfg = WalkConfig::new(dir.path(), false, &filter);
run(&cfg, 6, false, false, false, gate()).unwrap();
}
#[test]
fn run_detects_duplicates() {
let dir = tempfile::tempdir().unwrap();
let code = "fn process() {\n let x = read();\n let y = transform(x);\n write(y);\n log(\"done\");\n cleanup();\n}\n";
fs::write(dir.path().join("a.rs"), code).unwrap();
fs::write(dir.path().join("b.rs"), code).unwrap();
let filter = ExcludeFilter::default();
let cfg = WalkConfig::new(dir.path(), false, &filter);
run(&cfg, 6, false, false, false, gate()).unwrap();
}
#[test]
fn run_with_report_flag() {
let dir = tempfile::tempdir().unwrap();
let code = "fn process() {\n let x = read();\n let y = transform(x);\n write(y);\n log(\"done\");\n cleanup();\n}\n";
fs::write(dir.path().join("a.rs"), code).unwrap();
fs::write(dir.path().join("b.rs"), code).unwrap();
let filter = ExcludeFilter::default();
let cfg = WalkConfig::new(dir.path(), false, &filter);
run(&cfg, 6, true, false, false, gate()).unwrap();
}
#[test]
fn run_with_show_all_flag() {
let dir = tempfile::tempdir().unwrap();
let code = "fn process() {\n let x = read();\n let y = transform(x);\n write(y);\n log(\"done\");\n cleanup();\n}\n";
fs::write(dir.path().join("a.rs"), code).unwrap();
fs::write(dir.path().join("b.rs"), code).unwrap();
let filter = ExcludeFilter::default();
let cfg = WalkConfig::new(dir.path(), false, &filter);
run(&cfg, 6, true, true, false, gate()).unwrap();
}
#[test]
fn run_skips_binary_files() {
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join("data.c"), b"hello\x00world").unwrap();
let filter = ExcludeFilter::default();
let cfg = WalkConfig::new(dir.path(), false, &filter);
run(&cfg, 6, false, false, false, gate()).unwrap();
}
#[test]
fn run_with_high_min_lines() {
let dir = tempfile::tempdir().unwrap();
let code = "fn f() {\n let x = 1;\n let y = 2;\n}\n";
fs::write(dir.path().join("a.rs"), code).unwrap();
fs::write(dir.path().join("b.rs"), code).unwrap();
let filter = ExcludeFilter::default();
let cfg = WalkConfig::new(dir.path(), false, &filter);
run(&cfg, 20, false, false, false, gate()).unwrap();
}
#[test]
fn normalize_file_skips_comments_and_blanks() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.rs");
fs::write(
&path,
"// comment\n\nfn main() {\n // another comment\n let x = 1;\n}\n",
)
.unwrap();
let spec = detect(Path::new("test.rs")).unwrap();
let nf = normalize_file(&path, spec, false).unwrap().unwrap();
assert_eq!(nf.lines.len(), 3);
assert_eq!(nf.lines[0].content, "fn main() {");
assert_eq!(nf.lines[1].content, "let x = 1;");
assert_eq!(nf.lines[2].content, "}");
}
#[test]
fn normalize_file_binary_returns_none() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("data.c");
fs::write(&path, b"hello\x00world").unwrap();
let spec = detect(Path::new("test.c")).unwrap();
assert!(normalize_file(&path, spec, false).unwrap().is_none());
}
#[test]
fn run_json_with_duplicates() {
let dir = tempfile::tempdir().unwrap();
let code = "fn process() {\n let x = read();\n let y = transform(x);\n write(y);\n log(\"done\");\n cleanup();\n}\n";
fs::write(dir.path().join("a.rs"), code).unwrap();
fs::write(dir.path().join("b.rs"), code).unwrap();
let filter = ExcludeFilter::default();
let cfg = WalkConfig::new(dir.path(), false, &filter);
run(&cfg, 6, false, false, true, gate()).unwrap();
}
#[test]
fn run_json_empty_dir() {
let dir = tempfile::tempdir().unwrap();
let filter = ExcludeFilter::default();
let cfg = WalkConfig::new(dir.path(), false, &filter);
run(&cfg, 6, false, false, true, gate()).unwrap();
}
#[test]
fn run_json_no_duplicates() {
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join("a.rs"), "fn foo() {\n let x = 1;\n}\n").unwrap();
let filter = ExcludeFilter::default();
let cfg = WalkConfig::new(dir.path(), false, &filter);
run(&cfg, 6, false, false, true, gate()).unwrap();
}
#[test]
fn test_file_rust() {
assert!(walk::is_test_file(Path::new("parser_test.rs")));
assert!(!walk::is_test_file(Path::new("parser.rs")));
assert!(!walk::is_test_file(Path::new("test.rs"))); }
#[test]
fn test_file_python() {
assert!(walk::is_test_file(Path::new("test_parser.py")));
assert!(walk::is_test_file(Path::new("parser_test.py")));
assert!(!walk::is_test_file(Path::new("parser.py")));
}
#[test]
fn test_file_javascript() {
assert!(walk::is_test_file(Path::new("parser.test.js")));
assert!(walk::is_test_file(Path::new("parser.spec.js")));
assert!(walk::is_test_file(Path::new("parser.test.tsx")));
assert!(walk::is_test_file(Path::new("parser.spec.ts")));
assert!(!walk::is_test_file(Path::new("parser.js")));
}
#[test]
fn test_file_java_kotlin() {
assert!(walk::is_test_file(Path::new("ParserTest.java")));
assert!(walk::is_test_file(Path::new("ParserTests.java")));
assert!(!walk::is_test_file(Path::new("Parser.java")));
assert!(walk::is_test_file(Path::new("ParserTest.kt")));
}
#[test]
fn test_file_go() {
assert!(walk::is_test_file(Path::new("parser_test.go")));
assert!(!walk::is_test_file(Path::new("parser.go")));
}
#[test]
fn test_file_csharp() {
assert!(walk::is_test_file(Path::new("ParserTest.cs")));
assert!(walk::is_test_file(Path::new("ParserTests.cs")));
assert!(!walk::is_test_file(Path::new("Parser.cs")));
}
#[test]
fn test_file_ruby() {
assert!(walk::is_test_file(Path::new("parser_spec.rb")));
assert!(walk::is_test_file(Path::new("parser_test.rb")));
assert!(!walk::is_test_file(Path::new("parser.rb")));
}
#[test]
fn test_file_cpp() {
assert!(walk::is_test_file(Path::new("parser_test.cpp")));
assert!(walk::is_test_file(Path::new("test_parser.cpp")));
assert!(walk::is_test_file(Path::new("parser_unittest.cpp")));
assert!(walk::is_test_file(Path::new("ParserTest.cpp")));
assert!(!walk::is_test_file(Path::new("parser.cpp")));
}
#[test]
fn test_file_c() {
assert!(walk::is_test_file(Path::new("parser_test.c")));
assert!(walk::is_test_file(Path::new("test_parser.c")));
assert!(walk::is_test_file(Path::new("parser_unittest.c")));
assert!(!walk::is_test_file(Path::new("parser.c")));
}
#[test]
fn test_file_other_languages() {
assert!(walk::is_test_file(Path::new("parser_test.exs")));
assert!(walk::is_test_file(Path::new("parser_test.dart")));
assert!(walk::is_test_file(Path::new("ParserTest.swift")));
assert!(walk::is_test_file(Path::new("ParserSpec.scala")));
assert!(walk::is_test_file(Path::new("ParserSpec.hs")));
assert!(walk::is_test_file(Path::new("ParserTest.php")));
}
#[test]
fn test_file_no_extension() {
assert!(!walk::is_test_file(Path::new("Makefile")));
assert!(!walk::is_test_file(Path::new("README")));
}
#[test]
fn test_file_unknown_extension() {
assert!(!walk::is_test_file(Path::new("test_foo.xyz")));
}
#[test]
fn run_exclude_tests_skips_test_dir() {
let dir = tempfile::tempdir().unwrap();
let code = "fn process() {\n let x = read();\n let y = transform(x);\n write(y);\n log(\"done\");\n cleanup();\n}\n";
fs::create_dir(dir.path().join("tests")).unwrap();
fs::write(dir.path().join("tests/a.rs"), code).unwrap();
fs::write(dir.path().join("tests/b.rs"), code).unwrap();
fs::write(dir.path().join("lib.rs"), "fn foo() {\n let x = 1;\n}\n").unwrap();
let filter = ExcludeFilter::default();
let cfg = WalkConfig::new(dir.path(), true, &filter);
run(&cfg, 6, false, false, false, gate()).unwrap();
let cfg = WalkConfig::new(dir.path(), false, &filter);
run(&cfg, 6, false, false, false, gate()).unwrap();
}
#[test]
fn run_exclude_tests_skips_test_files() {
let dir = tempfile::tempdir().unwrap();
let code = "fn process() {\n let x = read();\n let y = transform(x);\n write(y);\n log(\"done\");\n cleanup();\n}\n";
fs::write(dir.path().join("parser_test.rs"), code).unwrap();
fs::write(dir.path().join("handler_test.rs"), code).unwrap();
fs::write(dir.path().join("lib.rs"), "fn foo() {\n let x = 1;\n}\n").unwrap();
let filter = ExcludeFilter::default();
let cfg = WalkConfig::new(dir.path(), false, &filter);
run(&cfg, 6, false, false, false, gate()).unwrap();
}
#[test]
fn run_exclude_tests_skips_test_file_in_subdirectory() {
let dir = tempfile::tempdir().unwrap();
let code = "fn process() {\n let x = read();\n let y = transform(x);\n write(y);\n log(\"done\");\n cleanup();\n}\n";
fs::create_dir_all(dir.path().join("src/utils")).unwrap();
fs::write(dir.path().join("src/utils/parser_test.rs"), code).unwrap();
fs::write(dir.path().join("src/utils/handler_test.rs"), code).unwrap();
fs::write(
dir.path().join("src/lib.rs"),
"fn foo() {\n let x = 1;\n}\n",
)
.unwrap();
let filter = ExcludeFilter::default();
let cfg = WalkConfig::new(dir.path(), false, &filter);
run(&cfg, 6, false, false, false, gate()).unwrap();
}
#[test]
fn run_exclude_tests_skips_entire_test_dir_tree() {
let dir = tempfile::tempdir().unwrap();
let code = "fn process() {\n let x = read();\n let y = transform(x);\n write(y);\n log(\"done\");\n cleanup();\n}\n";
fs::create_dir_all(dir.path().join("tests/helpers")).unwrap();
fs::write(dir.path().join("tests/integration.rs"), code).unwrap();
fs::write(dir.path().join("tests/helpers/utils.rs"), code).unwrap();
fs::write(dir.path().join("lib.rs"), "fn foo() {\n let x = 1;\n}\n").unwrap();
let filter = ExcludeFilter::default();
let cfg = WalkConfig::new(dir.path(), false, &filter);
run(&cfg, 6, false, false, false, gate()).unwrap();
}
const DUP_CODE: &str = "fn process() {\n let x = read();\n let y = transform(x);\n write(y);\n log(\"done\");\n cleanup();\n}\n";
fn gate() -> DupsGate {
DupsGate::default()
}
fn gate_max_dups(n: usize) -> DupsGate {
DupsGate {
max_duplicates: Some(n),
..Default::default()
}
}
fn gate_max_ratio(pct: f64) -> DupsGate {
DupsGate {
max_dup_ratio: Some(pct),
..Default::default()
}
}
fn gate_fail_on_increase(git_ref: &str) -> DupsGate {
DupsGate {
fail_on_increase: Some(git_ref.to_owned()),
..Default::default()
}
}
#[test]
fn max_duplicates_none_does_not_fail_with_duplicates() {
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join("a.rs"), DUP_CODE).unwrap();
fs::write(dir.path().join("b.rs"), DUP_CODE).unwrap();
let filter = ExcludeFilter::default();
let cfg = WalkConfig::new(dir.path(), false, &filter);
run(&cfg, 6, false, false, false, gate()).unwrap();
}
#[test]
fn max_duplicates_zero_passes_when_no_duplicates() {
let dir = tempfile::tempdir().unwrap();
fs::write(
dir.path().join("a.rs"),
"fn foo() {\n let x = 1;\n let y = 2;\n let z = x + y;\n println!(\"{}\", z);\n return z;\n}\n",
)
.unwrap();
let filter = ExcludeFilter::default();
let cfg = WalkConfig::new(dir.path(), false, &filter);
run(&cfg, 6, false, false, false, gate_max_dups(0)).unwrap();
}
#[test]
fn max_duplicates_zero_fails_when_duplicates_found() {
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join("a.rs"), DUP_CODE).unwrap();
fs::write(dir.path().join("b.rs"), DUP_CODE).unwrap();
let filter = ExcludeFilter::default();
let cfg = WalkConfig::new(dir.path(), false, &filter);
let err = run(&cfg, 6, false, false, false, gate_max_dups(0)).unwrap_err();
assert!(
err.to_string().contains("quality gate failed"),
"expected quality gate error, got: {err}"
);
}
#[test]
fn max_duplicates_limit_passes_when_within_limit() {
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join("a.rs"), DUP_CODE).unwrap();
fs::write(dir.path().join("b.rs"), DUP_CODE).unwrap();
let filter = ExcludeFilter::default();
let cfg = WalkConfig::new(dir.path(), false, &filter);
run(&cfg, 6, false, false, false, gate_max_dups(5)).unwrap();
}
#[test]
fn max_duplicates_error_message_contains_counts() {
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join("a.rs"), DUP_CODE).unwrap();
fs::write(dir.path().join("b.rs"), DUP_CODE).unwrap();
let filter = ExcludeFilter::default();
let cfg = WalkConfig::new(dir.path(), false, &filter);
let err = run(&cfg, 6, false, false, false, gate_max_dups(0)).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("limit: 0"),
"expected limit in message, got: {msg}"
);
assert!(
msg.contains("duplicate groups"),
"expected group count in message, got: {msg}"
);
}
#[test]
fn max_dup_ratio_none_does_not_fail() {
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join("a.rs"), DUP_CODE).unwrap();
fs::write(dir.path().join("b.rs"), DUP_CODE).unwrap();
let filter = ExcludeFilter::default();
let cfg = WalkConfig::new(dir.path(), false, &filter);
run(&cfg, 6, false, false, false, gate()).unwrap();
}
#[test]
fn max_dup_ratio_passes_when_no_duplicates() {
let dir = tempfile::tempdir().unwrap();
fs::write(
dir.path().join("a.rs"),
"fn foo() {\n let x = 1;\n let y = 2;\n let z = x + y;\n println!(\"{}\", z);\n return z;\n}\n",
)
.unwrap();
let filter = ExcludeFilter::default();
let cfg = WalkConfig::new(dir.path(), false, &filter);
run(&cfg, 6, false, false, false, gate_max_ratio(0.0)).unwrap();
}
#[test]
fn max_dup_ratio_fails_when_ratio_exceeded() {
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join("a.rs"), DUP_CODE).unwrap();
fs::write(dir.path().join("b.rs"), DUP_CODE).unwrap();
let filter = ExcludeFilter::default();
let cfg = WalkConfig::new(dir.path(), false, &filter);
let err = run(&cfg, 6, false, false, false, gate_max_ratio(1.0)).unwrap_err();
assert!(
err.to_string().contains("quality gate failed"),
"expected quality gate error, got: {err}"
);
}
#[test]
fn max_dup_ratio_passes_when_within_limit() {
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join("a.rs"), DUP_CODE).unwrap();
fs::write(dir.path().join("b.rs"), DUP_CODE).unwrap();
let filter = ExcludeFilter::default();
let cfg = WalkConfig::new(dir.path(), false, &filter);
run(&cfg, 6, false, false, false, gate_max_ratio(100.0)).unwrap();
}
#[test]
fn max_dup_ratio_error_message_shows_percentages() {
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join("a.rs"), DUP_CODE).unwrap();
fs::write(dir.path().join("b.rs"), DUP_CODE).unwrap();
let filter = ExcludeFilter::default();
let cfg = WalkConfig::new(dir.path(), false, &filter);
let err = run(&cfg, 6, false, false, false, gate_max_ratio(1.0)).unwrap_err();
let msg = err.to_string();
assert!(msg.contains('%'), "expected % in message, got: {msg}");
assert!(
msg.contains("limit of"),
"expected limit in message, got: {msg}"
);
}
#[test]
fn both_gates_checked_independently() {
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join("a.rs"), DUP_CODE).unwrap();
fs::write(dir.path().join("b.rs"), DUP_CODE).unwrap();
let filter = ExcludeFilter::default();
let cfg = WalkConfig::new(dir.path(), false, &filter);
let g = DupsGate {
max_duplicates: Some(100),
max_dup_ratio: Some(1.0),
..Default::default()
};
let err = run(&cfg, 6, false, false, false, g).unwrap_err();
assert!(err.to_string().contains('%'), "ratio gate should trigger");
}
fn make_git_repo_with_files(files: &[(&str, &str)]) -> tempfile::TempDir {
let dir = tempfile::tempdir().unwrap();
let repo = git2::Repository::init(dir.path()).unwrap();
let mut config = repo.config().unwrap();
config.set_str("user.name", "Test").unwrap();
config.set_str("user.email", "test@test.com").unwrap();
let sig =
git2::Signature::new("Test", "test@test.com", &git2::Time::new(1_700_000_000, 0)).unwrap();
for (name, content) in files {
fs::write(dir.path().join(name), content).unwrap();
}
let mut index = repo.index().unwrap();
for (name, _) in files {
index.add_path(Path::new(name)).unwrap();
}
index.write().unwrap();
let tree_oid = index.write_tree().unwrap();
let tree = repo.find_tree(tree_oid).unwrap();
repo.commit(Some("HEAD"), &sig, &sig, "init", &tree, &[])
.unwrap();
dir
}
#[test]
fn fail_on_increase_none_does_not_fail() {
let dir = make_git_repo_with_files(&[("a.rs", DUP_CODE), ("b.rs", DUP_CODE)]);
let filter = ExcludeFilter::default();
let cfg = WalkConfig::new(dir.path(), false, &filter);
run(&cfg, 6, false, false, false, gate()).unwrap();
}
#[test]
fn fail_on_increase_same_ref_passes() {
let dir = make_git_repo_with_files(&[("a.rs", DUP_CODE), ("b.rs", DUP_CODE)]);
let filter = ExcludeFilter::default();
let cfg = WalkConfig::new(dir.path(), false, &filter);
let result = run(&cfg, 6, false, false, false, gate_fail_on_increase("HEAD"));
assert!(
result.is_ok(),
"same ref should not trigger gate: {result:?}"
);
}
#[test]
fn fail_on_increase_passes_when_duplication_decreased() {
let unique: String = (0..40)
.map(|i| {
format!(
"fn unique_{i}(x{i}: i64, y{i}: i64) -> i64 {{\n \
let a{i} = x{i} * {i} + y{i};\n \
let b{i} = a{i} ^ (a{i} >> 3);\n \
let c{i} = b{i}.wrapping_add({i} * 17);\n \
let d{i} = c{i}.wrapping_mul(b{i} | 0x{i:04X});\n \
d{i}.wrapping_sub(a{i})\n}}\n"
)
})
.collect();
let dir = make_git_repo_with_files(&[("a.rs", DUP_CODE), ("b.rs", DUP_CODE)]);
fs::write(dir.path().join("extra.rs"), &unique).unwrap();
let filter = ExcludeFilter::default();
let cfg = WalkConfig::new(dir.path(), false, &filter);
let result = run(&cfg, 6, false, false, false, gate_fail_on_increase("HEAD"));
assert!(
result.is_ok(),
"decreased duplication should not trigger gate: {result:?}"
);
}
#[test]
fn fail_on_increase_fails_when_duplication_increased() {
let unique_a = "fn foo() {\n let x = 1;\n let y = 2;\n let z = 3;\n let w = 4;\n let v = 5;\n}\n";
let unique_b = "fn bar() {\n let a = 10;\n let b = 20;\n let c = 30;\n let d = 40;\n let e = 50;\n}\n";
let dir = make_git_repo_with_files(&[("a.rs", unique_a), ("b.rs", unique_b)]);
fs::write(dir.path().join("dup1.rs"), DUP_CODE).unwrap();
fs::write(dir.path().join("dup2.rs"), DUP_CODE).unwrap();
let filter = ExcludeFilter::default();
let cfg = WalkConfig::new(dir.path(), false, &filter);
let result = run(&cfg, 6, false, false, false, gate_fail_on_increase("HEAD"));
assert!(result.is_err(), "increased duplication should trigger gate");
let msg = result.unwrap_err().to_string();
assert!(
msg.contains("quality gate failed") && msg.contains("duplication increased"),
"unexpected error message: {msg}"
);
}