use super::common::{create_config, fixture_path};
use fallow_core::discover::{DiscoveredFile, FileId};
use rustc_hash::FxHashSet;
#[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()
);
}
fn default_ignore_fixture_files(root: &std::path::Path) -> Vec<DiscoveredFile> {
["src/foo.ts", "lib/foo.js", ".next/static/chunks/foo.js"]
.into_iter()
.enumerate()
.map(|(idx, rel)| {
let path = root.join(rel);
let size_bytes = std::fs::metadata(&path)
.expect("fixture file should exist")
.len();
DiscoveredFile {
id: FileId(idx as u32),
path,
size_bytes,
}
})
.collect()
}
fn cloned_relative_files(
root: &std::path::Path,
report: &fallow_core::duplicates::DuplicationReport,
) -> FxHashSet<String> {
report
.clone_groups
.iter()
.flat_map(|group| group.instances.iter())
.map(|instance| {
instance
.file
.strip_prefix(root)
.expect("clone path should be under fixture root")
.to_string_lossy()
.replace('\\', "/")
})
.collect()
}
#[test]
fn duplicate_default_ignores_skip_framework_cache_but_not_lib() {
let root = fixture_path("duplicates_default_ignores");
let files = default_ignore_fixture_files(&root);
let dupes_config = fallow_core::duplicates::DuplicatesConfig {
min_tokens: 10,
min_lines: 3,
cross_language: true,
..Default::default()
};
let (report, skips) = fallow_core::duplicates::find_duplicates_with_default_ignore_skips(
&root,
&files,
&dupes_config,
);
let cloned_files = cloned_relative_files(&root, &report);
assert!(cloned_files.contains("src/foo.ts"));
assert!(
cloned_files.contains("lib/foo.js"),
"lib is authored-looking and must not be a default ignore"
);
assert!(
!cloned_files.contains(".next/static/chunks/foo.js"),
".next should be skipped by built-in duplicates ignores"
);
assert_eq!(skips.total, 1);
assert_eq!(skips.by_pattern[0].pattern, "**/.next/**");
assert_eq!(skips.by_pattern[0].count, 1);
}
#[test]
fn duplicate_ignore_defaults_false_replaces_defaults_with_user_ignore() {
let root = fixture_path("duplicates_default_ignores");
let files = default_ignore_fixture_files(&root);
let dupes_config = fallow_core::duplicates::DuplicatesConfig {
min_tokens: 10,
min_lines: 3,
cross_language: true,
ignore_defaults: false,
ignore: vec!["**/lib/**".to_string()],
..Default::default()
};
let (report, skips) = fallow_core::duplicates::find_duplicates_with_default_ignore_skips(
&root,
&files,
&dupes_config,
);
let cloned_files = cloned_relative_files(&root, &report);
assert!(cloned_files.contains("src/foo.ts"));
assert!(
!cloned_files.contains("lib/foo.js"),
"user ignore should remove lib when defaults are disabled"
);
assert!(
cloned_files.contains(".next/static/chunks/foo.js"),
".next should be analyzed when ignoreDefaults is false"
);
assert_eq!(skips.total, 0);
}