use super::common::{create_config, fixture_path};
use fallow_core::discover::{DiscoveredFile, FileId};
#[test]
fn duplicate_code_detects_exact_clones() {
let root = fixture_path("duplicate-code");
let config = create_config(root.clone());
let files = fallow_core::discover::discover_files(&config);
let dupes_config = fallow_core::duplicates::DuplicatesConfig {
min_tokens: 20,
min_lines: 3,
..fallow_core::duplicates::DuplicatesConfig::default()
};
let report = fallow_core::duplicates::find_duplicates(&root, &files, &dupes_config);
assert!(
!report.clone_groups.is_empty(),
"Should detect clones in duplicate-code fixture"
);
assert!(
report.stats.files_with_clones >= 2,
"At least 2 files should have clones"
);
assert!(
report.stats.duplication_percentage > 0.0,
"Duplication percentage should be > 0"
);
}
#[test]
fn duplicate_code_semantic_mode_detects_type2_clones() {
let root = fixture_path("duplicate-code");
let config = create_config(root.clone());
let files = fallow_core::discover::discover_files(&config);
let dupes_config = fallow_core::duplicates::DuplicatesConfig {
min_tokens: 20,
min_lines: 3,
mode: fallow_core::duplicates::DetectionMode::Semantic,
..fallow_core::duplicates::DuplicatesConfig::default()
};
let report = fallow_core::duplicates::find_duplicates(&root, &files, &dupes_config);
let files_with_clones: rustc_hash::FxHashSet<_> = report
.clone_groups
.iter()
.flat_map(|g| g.instances.iter())
.map(|inst| inst.file.file_name().unwrap().to_string_lossy().to_string())
.collect();
assert!(
files_with_clones.contains("copy2.ts"),
"Semantic mode should detect copy2.ts with renamed variables, files found: {files_with_clones:?}"
);
}
#[test]
fn duplicate_code_unique_file_has_no_clones() {
let root = fixture_path("duplicate-code");
let config = create_config(root.clone());
let files = fallow_core::discover::discover_files(&config);
let dupes_config = fallow_core::duplicates::DuplicatesConfig {
min_tokens: 20,
min_lines: 3,
..fallow_core::duplicates::DuplicatesConfig::default()
};
let report = fallow_core::duplicates::find_duplicates(&root, &files, &dupes_config);
let all_clone_files: Vec<String> = report
.clone_groups
.iter()
.flat_map(|g| g.instances.iter())
.map(|inst| inst.file.file_name().unwrap().to_string_lossy().to_string())
.collect();
assert!(
!all_clone_files.contains(&"unique.ts".to_string()),
"unique.ts should not appear in any clone group, found in: {all_clone_files:?}"
);
}
#[test]
fn duplicate_code_json_output_serializable() {
let root = fixture_path("duplicate-code");
let config = create_config(root.clone());
let files = fallow_core::discover::discover_files(&config);
let dupes_config = fallow_core::duplicates::DuplicatesConfig {
min_tokens: 20,
min_lines: 3,
..fallow_core::duplicates::DuplicatesConfig::default()
};
let report = fallow_core::duplicates::find_duplicates(&root, &files, &dupes_config);
let json = serde_json::to_string_pretty(&report).expect("report should serialize to JSON");
let reparsed: serde_json::Value = serde_json::from_str(&json).expect("JSON should be valid");
assert!(reparsed["clone_groups"].is_array());
assert!(reparsed["stats"]["total_files"].is_number());
}
#[test]
fn duplicate_code_skip_local_filters_same_directory() {
let root = fixture_path("duplicate-code");
let config = create_config(root.clone());
let files = fallow_core::discover::discover_files(&config);
let dupes_config = fallow_core::duplicates::DuplicatesConfig {
min_tokens: 20,
min_lines: 3,
skip_local: true,
..fallow_core::duplicates::DuplicatesConfig::default()
};
let report = fallow_core::duplicates::find_duplicates(&root, &files, &dupes_config);
assert!(
report.clone_groups.is_empty(),
"skip_local should filter same-directory clones"
);
}
#[test]
fn duplicate_code_min_tokens_threshold_filters() {
let root = fixture_path("duplicate-code");
let config = create_config(root.clone());
let files = fallow_core::discover::discover_files(&config);
let dupes_config = fallow_core::duplicates::DuplicatesConfig {
min_tokens: 10000,
min_lines: 1,
..fallow_core::duplicates::DuplicatesConfig::default()
};
let report = fallow_core::duplicates::find_duplicates(&root, &files, &dupes_config);
assert!(
report.clone_groups.is_empty(),
"Very high min_tokens should find no clones"
);
}
#[test]
fn duplicate_code_find_duplicates_in_project_convenience() {
let root = fixture_path("duplicate-code");
let dupes_config = fallow_core::duplicates::DuplicatesConfig {
min_tokens: 20,
min_lines: 3,
..fallow_core::duplicates::DuplicatesConfig::default()
};
let report = fallow_core::duplicates::find_duplicates_in_project(&root, &dupes_config);
assert!(
!report.clone_groups.is_empty(),
"Convenience function should detect clones"
);
}
#[test]
fn ignore_imports_removes_import_only_clones() {
let dir = tempfile::tempdir().expect("create temp dir");
let src = dir.path().join("src");
std::fs::create_dir_all(&src).expect("create src");
let imports = "import { A } from './a';\n\
import { B } from './b';\n\
import { C } from './c';\n\
import { D } from './d';\n\
import { E } from './e';\n\
import { F } from './f';\n\
import { G } from './g';\n\
import { H } from './h';\n";
let file1 = format!("{imports}\nexport function foo() {{ return A + B + C; }}\n");
let file2 = format!("{imports}\nexport function bar() {{ return D * E * F; }}\n");
std::fs::write(src.join("file1.ts"), &file1).expect("write file1");
std::fs::write(src.join("file2.ts"), &file2).expect("write file2");
std::fs::write(dir.path().join("package.json"), r#"{"name": "test"}"#)
.expect("write package.json");
let files = vec![
DiscoveredFile {
id: FileId(0),
path: src.join("file1.ts"),
size_bytes: file1.len() as u64,
},
DiscoveredFile {
id: FileId(1),
path: src.join("file2.ts"),
size_bytes: file2.len() as u64,
},
];
let config_with_imports = fallow_core::duplicates::DuplicatesConfig {
min_tokens: 10,
min_lines: 3,
..Default::default()
};
let report_with =
fallow_core::duplicates::find_duplicates(dir.path(), &files, &config_with_imports);
assert!(
!report_with.clone_groups.is_empty(),
"Without ignore_imports, identical import blocks should be detected as clones"
);
let config_ignore = fallow_core::duplicates::DuplicatesConfig {
min_tokens: 10,
min_lines: 3,
ignore_imports: true,
..Default::default()
};
let report_without =
fallow_core::duplicates::find_duplicates(dir.path(), &files, &config_ignore);
assert!(
report_without.clone_groups.is_empty(),
"With ignore_imports=true, import-only clones should be eliminated, but found {} groups",
report_without.clone_groups.len()
);
}