use std::collections::BTreeSet;
use crate::cli::Options;
use crate::files::SourceFile;
use crate::tokenizer::Location;
use super::{
CloneMatch, Fragment, dedup_exact_clones, detect, detect_prepared_drafts, prepare_source_drafts,
};
fn detection_options() -> Options {
Options {
min_tokens: 3,
min_lines: 0,
..Options::default()
}
}
#[test]
fn detects_cross_file_duplicates() {
let content = "alpha beta gamma delta epsilon\n";
let files = vec![
source("a.js", content),
source("b.js", &format!("prefix\n{content}\nsuffix\n")),
];
let result = detect(files, &detection_options());
assert!(!result.clones.is_empty());
}
#[test]
fn detects_generic_format_duplicates() {
let content = "alpha beta gamma delta epsilon\n";
let files = vec![
source_with_format("a.css", "css", content),
source_with_format("b.css", "css", &format!("prefix\n{content}\nsuffix\n")),
];
let result = detect(files, &detection_options());
assert!(!result.clones.is_empty());
}
#[test]
fn skip_local_skips_clones_inside_same_configured_root() {
let options = Options {
paths: vec!["project".into()],
skip_local: true,
min_tokens: 3,
min_lines: 0,
..Options::default()
};
let content = "alpha beta gamma delta epsilon\n";
let files = vec![
source("project/dir1/a.js", content),
source("project/dir2/b.js", content),
];
let result = detect(files, &options);
assert!(result.clones.is_empty());
}
#[test]
fn skip_local_keeps_clones_across_configured_roots() {
let options = Options {
paths: vec!["left".into(), "right".into()],
skip_local: true,
min_tokens: 3,
min_lines: 0,
..Options::default()
};
let content = "alpha beta gamma delta epsilon\n";
let files = vec![source("left/a.js", content), source("right/b.js", content)];
let result = detect(files, &options);
assert!(!result.clones.is_empty());
}
#[test]
fn skips_empty_token_sources_in_statistics() {
let content = "// jscpd:ignore-start\nignored\n// jscpd:ignore-end\n";
let result = detect(vec![source("ignored.js", content)], &Options::default());
assert_eq!(result.sources.len(), 0);
assert_eq!(result.statistics.total.sources, 0);
}
#[test]
fn prepared_drafts_detection_matches_direct_detection() {
let options = Options {
reporters: vec!["json".to_string()],
..detection_options()
};
let content = "alpha beta gamma delta epsilon\n";
let files = vec![
source("a.js", content),
source("b.js", &format!("prefix\n{content}\nsuffix\n")),
];
let direct = detect(files.clone(), &options);
let prepared = detect_prepared_drafts(prepare_source_drafts(files, &options), &options);
assert_eq!(prepared.clones.len(), direct.clones.len());
assert_eq!(
prepared.statistics.total.sources,
direct.statistics.total.sources
);
assert_eq!(
prepared.statistics.total.clones,
direct.statistics.total.clones
);
assert_eq!(
prepared.source_contents.keys().collect::<BTreeSet<_>>(),
direct.source_contents.keys().collect::<BTreeSet<_>>()
);
}
#[test]
fn detects_typescript_template_tail_clone_before_member_name_difference() {
let options = Options {
min_tokens: 50,
min_lines: 5,
..Options::default()
};
let content = r#"
function first(workUnitAsyncStorage, reportResult) {
console.log = function (...args: Array<any>) {
const store = workUnitAsyncStorage.getStore()
reportResult({
type: 'console-call',
method: 'log',
input: `${store ? '[Store]' : '[No Store]'}: ${args.join(' ')}`,
})
}
require('next/dist/server/node-environment-extensions/console-exit')
workUnitAsyncStorage.run({ type: 'request' } as WorkUnitStore, () => {
console.log('inside')
})
}
function second(workUnitAsyncStorage, reportResult) {
console.error = function (...args: Array<any>) {
const store = workUnitAsyncStorage.getStore()
reportResult({
type: 'console-call',
method: 'error',
input: `${store ? '[Store]' : '[No Store]'}: ${args.join(' ')}`,
})
}
require('next/dist/server/node-environment-extensions/console-exit')
workUnitAsyncStorage.run({ type: 'request' } as WorkUnitStore, () => {
console.error('inside')
})
}
"#;
let result = detect(
vec![source_with_format("console.ts", "typescript", content)],
&options,
);
assert!(result.clones.iter().any(|clone| {
clone.duplication_a.start.line <= 24
&& clone.duplication_a.end.line >= 32
&& clone.duplication_b.start.line <= 7
&& clone.duplication_b.end.line >= 15
}));
}
#[test]
fn deduplicates_exact_clone_records() {
let mut clones = vec![
clone_with_lines("javascript", "a.js", 1, 5, "b.js", 1, 5),
clone_with_lines("javascript", "a.js", 1, 5, "b.js", 1, 5),
clone_with_lines("javascript", "a.js", 6, 10, "b.js", 6, 10),
];
dedup_exact_clones(&mut clones);
assert_eq!(clones.len(), 2);
assert_eq!(clones[0].duplication_a.start.line, 1);
assert_eq!(clones[1].duplication_a.start.line, 6);
}
fn source(path: &str, content: &str) -> SourceFile {
source_with_format(path, "javascript", content)
}
fn source_with_format(path: &str, format: &str, content: &str) -> SourceFile {
SourceFile {
source_id: path.to_string(),
format: format.to_string(),
content: content.to_string(),
}
}
fn clone_with_lines(
format: &str,
source_a: &str,
start_a: usize,
end_a: usize,
source_b: &str,
start_b: usize,
end_b: usize,
) -> CloneMatch {
CloneMatch {
format: format.to_string(),
duplication_a: fragment(source_a, start_a, end_a),
duplication_b: fragment(source_b, start_b, end_b),
tokens: 20,
}
}
fn fragment(source_id: &str, start: usize, end: usize) -> Fragment {
Fragment {
source_id: source_id.to_string(),
start: location(start, 1, start),
end: location(end, 1, end),
range: [start, end],
blame: None,
}
}
fn location(line: usize, column: usize, position: usize) -> Location {
Location {
line,
column,
position,
}
}