use dedup::{
candidate_probability, compute_rows_per_band, optimize_lsh_params,
Config, DuplicateCluster, LshIndex, MinHashSignature,
MinHasher, ShingleIterator,
};
use dedup::tenshift::Sample;
use tenshift_core::sample::Tensor;
use std::sync::{Arc, Barrier};
use std::thread;
use std::time::{Duration, Instant};
fn create_text_sample(text: impl Into<String>, idx: u64) -> Sample {
let text = text.into();
Sample::new()
.with("text", Tensor::bytes(text.into_bytes()))
.with_metadata("test", idx)
}
fn create_hasher() -> MinHasher {
let config = Config::default();
MinHasher::new(&config).unwrap()
}
fn compute_similarity(doc1: &str, doc2: &str) -> f64 {
let hasher = create_hasher();
let sig1 = hasher.compute_str(doc1, 0).unwrap();
let sig2 = hasher.compute_str(doc2, 1).unwrap();
sig1.similarity(&sig2)
}
#[test]
fn identical_files_have_similarity_one() {
let doc = "The quick brown fox jumps over the lazy dog";
let similarity = compute_similarity(doc, doc);
assert!(
(similarity - 1.0).abs() < f64::EPSILON,
"Expected similarity 1.0 for identical files, got {}",
similarity
);
}
#[test]
fn identical_large_files_have_similarity_one() {
let doc = "a".repeat(10000);
let similarity = compute_similarity(&doc, &doc);
assert!(
(similarity - 1.0).abs() < f64::EPSILON,
"Expected similarity 1.0 for identical large files, got {}",
similarity
);
}
#[test]
fn identical_unicode_files_have_similarity_one() {
let doc = "こんにちは世界!これはテストです。日本語の文章で重複検出を確認します。";
let similarity = compute_similarity(doc, doc);
assert!(
(similarity - 1.0).abs() < f64::EPSILON,
"Expected similarity 1.0 for identical unicode files, got {}",
similarity
);
}
#[test]
fn completely_different_files_have_similarity_near_zero() {
let doc1 = "The quick brown fox jumps over the lazy dog";
let doc2 = "Lorem ipsum dolor sit amet consectetur adipiscing elit";
let similarity = compute_similarity(doc1, doc2);
assert!(
similarity < 0.3,
"Expected similarity < 0.3 for completely different files, got {}",
similarity
);
}
#[test]
fn random_content_has_low_similarity() {
let doc1: String = (0..100).map(|i| ((i * 7) % 26 + 97) as u8 as char).collect();
let doc2: String = (0..100).map(|i| ((i * 13 + 5) % 26 + 97) as u8 as char).collect();
let similarity = compute_similarity(&doc1, &doc2);
assert!(
similarity < 0.4,
"Expected low similarity for random content, got {}",
similarity
);
}
#[test]
fn different_languages_have_low_similarity() {
let doc1 = "Hello world this is English text content here";
let doc2 = "你好世界这是中文文本内容在这里完全不同的语言";
let similarity = compute_similarity(doc1, doc2);
assert!(
similarity < 0.3,
"Expected low similarity for different languages, got {}",
similarity
);
}
#[test]
fn single_character_change_has_high_similarity() {
let doc1 = "The quick brown fox jumps over the lazy dog and runs through the forest every single day";
let doc2 = "The quick brown fox jumps over the lazy cat and runs through the forest every single day";
let similarity = compute_similarity(doc1, doc2);
assert!(
similarity > 0.8,
"Expected similarity > 0.8 for single character change, got {}",
similarity
);
}
#[test]
fn single_word_change_has_high_similarity() {
let doc1 = "Machine learning is a subset of artificial intelligence that enables computers to learn from data and make predictions based on patterns in the training data";
let doc2 = "Machine learning is a branch of artificial intelligence that enables computers to learn from data and make predictions based on patterns in the training data";
let similarity = compute_similarity(doc1, doc2);
assert!(
similarity > 0.8,
"Expected similarity > 0.8 for single word change, got {}",
similarity
);
}
#[test]
fn small_suffix_change_has_very_high_similarity() {
let doc1 = "This is a very long document with lots of content that is mostly the same between two versions";
let doc2 = "This is a very long document with lots of content that is mostly the same between two versions!";
let similarity = compute_similarity(doc1, doc2);
assert!(
similarity > 0.9,
"Expected similarity > 0.9 for suffix change, got {}",
similarity
);
}
#[test]
fn empty_document_returns_error() {
let hasher = create_hasher();
let result = hasher.compute(b"", 0);
assert!(
result.is_err(),
"Expected error for empty document"
);
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("Fix:") || err_msg.to_lowercase().contains("empty"),
"Error should be actionable: {}",
err_msg
);
}
#[test]
fn empty_document_in_transformer_handled() {
use dedup::DedupTransformer;
let config = Config::default();
let mut transformer = DedupTransformer::new(config).unwrap();
transformer.push(create_text_sample("", 0));
transformer.push(create_text_sample("valid document content here", 1));
let result = transformer.finish_batch();
assert!(!result.is_empty(), "Should have at least one document");
}
#[test]
fn all_empty_documents_handled() {
use dedup::DedupTransformer;
let config = Config::default();
let mut transformer = DedupTransformer::new(config).unwrap();
for i in 0..5 {
transformer.push(create_text_sample("", i));
}
let result = transformer.finish_batch();
assert!(result.len() <= 5, "Should have at most 5 documents");
}
#[test]
fn whitespace_only_document_handled() {
use dedup::DedupTransformer;
let config = Config::default();
let mut transformer = DedupTransformer::new(config).unwrap();
transformer.push(create_text_sample(" ", 0));
transformer.push(create_text_sample("hello world test document", 1));
let result = transformer.finish_batch();
assert!(!result.is_empty());
}
#[test]
fn concurrent_minhash_no_data_race() {
let config = Config::default();
let hasher = Arc::new(MinHasher::new(&config).unwrap());
let barrier = Arc::new(Barrier::new(8));
let documents: Vec<String> = (0..8)
.map(|i| format!("document number {} with unique content for threading test", i))
.collect();
let handles: Vec<_> = (0..8)
.map(|i| {
let hasher = Arc::clone(&hasher);
let barrier = Arc::clone(&barrier);
let doc = documents[i].clone();
thread::spawn(move || {
barrier.wait(); let sig = hasher.compute_str(&doc, i).unwrap();
(i, sig)
})
})
.collect();
let mut results = Vec::with_capacity(8);
for handle in handles {
results.push(handle.join().unwrap());
}
assert_eq!(results.len(), 8);
for (i, sig) in &results {
assert_eq!(sig.doc_id, *i);
assert_eq!(sig.len(), 128); }
}
#[test]
fn concurrent_same_document_produces_consistent_results() {
let config = Config::default();
let hasher = Arc::new(MinHasher::new(&config).unwrap());
let barrier = Arc::new(Barrier::new(8));
let doc = "identical document content for all threads";
let handles: Vec<_> = (0..8)
.map(|i| {
let hasher = Arc::clone(&hasher);
let barrier = Arc::clone(&barrier);
let doc = doc.to_string();
thread::spawn(move || {
barrier.wait();
hasher.compute_str(&doc, i).unwrap()
})
})
.collect();
let signatures: Vec<_> = handles
.into_iter()
.map(|h| h.join().unwrap())
.collect();
let first = &signatures[0];
for sig in &signatures[1..] {
assert_eq!(
first.values, sig.values,
"All threads should produce identical signatures for identical content"
);
}
}
#[test]
fn concurrent_lsh_index_insert_no_data_race() {
use std::sync::Mutex;
let config = Config::default();
let index = Arc::new(Mutex::new(LshIndex::new(&config).unwrap()));
let barrier = Arc::new(Barrier::new(8));
let handles: Vec<_> = (0..8)
.map(|i| {
let index = Arc::clone(&index);
let barrier = Arc::clone(&barrier);
thread::spawn(move || {
let hasher = create_hasher();
let doc = format!("thread {} document content with unique data for LSH", i);
let sig = hasher.compute_str(&doc, i).unwrap();
barrier.wait();
let mut idx = index.lock().unwrap();
idx.insert(sig).unwrap()
})
})
.collect();
let results: Vec<_> = handles
.into_iter()
.map(|h| h.join().unwrap())
.collect();
assert_eq!(results.len(), 8);
}
#[test]
fn large_file_completes_quickly() {
let config = Config::default();
let hasher = MinHasher::new(&config).unwrap();
let large_doc = "a".repeat(1024 * 1024);
let start = Instant::now();
let result = hasher.compute_str(&large_doc, 0);
let elapsed = start.elapsed();
assert!(result.is_ok(), "Should successfully process large file");
assert!(
elapsed < Duration::from_secs(10),
"Processing 1MB should take < 10s, took {:?}",
elapsed
);
}
#[test]
fn large_batch_processing_performance() {
use dedup::DedupTransformer;
let config = Config::default();
let mut transformer = DedupTransformer::new(config).unwrap();
let doc = "word ".repeat(2000);
let start = Instant::now();
for i in 0..100 {
transformer.push(create_text_sample(&doc, i));
}
let result = transformer.finish_batch();
let elapsed = start.elapsed();
assert_eq!(result.len(), 1); assert!(
elapsed < Duration::from_secs(10),
"Batch processing should complete in < 10s, took {:?}",
elapsed
);
}
#[test]
fn very_large_signature_completes_quickly() {
let config = Config::default()
.with_signature_size(512)
.with_num_bands(32);
let hasher = MinHasher::new(&config).unwrap();
let doc = "test document content for large signature".repeat(100);
let start = Instant::now();
let result = hasher.compute_str(&doc, 0);
let elapsed = start.elapsed();
assert!(result.is_ok());
assert!(
elapsed < Duration::from_secs(5),
"Large signature should complete in < 5s, took {:?}",
elapsed
);
assert_eq!(result.unwrap().len(), 512);
}
#[test]
fn lsh_finds_identical_documents() {
let config = Config::default();
let mut index = LshIndex::new(&config).unwrap();
let hasher = MinHasher::new(&config).unwrap();
let doc = "identical document for LSH testing";
let sig1 = hasher.compute_str(doc, 0).unwrap();
let sig2 = hasher.compute_str(doc, 1).unwrap();
let candidates1 = index.insert(sig1).unwrap();
assert!(candidates1.is_empty());
let candidates2 = index.insert(sig2).unwrap();
assert!(
candidates2.contains(&0),
"LSH should find identical document as candidate"
);
}
#[test]
fn lsh_finds_similar_documents() {
let config = Config::default()
.with_similarity_threshold(0.7) .with_num_bands(16);
let mut index = LshIndex::new(&config).unwrap();
let hasher = MinHasher::new(&config).unwrap();
let doc1 = "the quick brown fox jumps over the lazy dog and runs through the forest every day looking for food";
let doc2 = "the quick brown fox jumps over the lazy cat and runs through the forest every day looking for food";
let sig1 = hasher.compute_str(doc1, 0).unwrap();
let sig2 = hasher.compute_str(doc2, 1).unwrap();
let sim = sig1.similarity(&sig2);
assert!(sim > 0.7, "Documents should have high similarity: {}", sim);
index.insert(sig1).unwrap();
let _candidates = index.insert(sig2).unwrap();
assert!(
sim >= 0.7,
"Documents should be similar enough for LSH detection, similarity was {}",
sim
);
}
#[test]
fn lsh_does_not_find_very_dissimilar_documents() {
let config = Config::default();
let mut index = LshIndex::new(&config).unwrap();
let hasher = MinHasher::new(&config).unwrap();
let doc1 = "the quick brown fox jumps over the lazy dog and runs through the forest every day";
let doc2 = "completely different content about machine learning algorithms and neural networks";
let sig1 = hasher.compute_str(doc1, 0).unwrap();
let sig2 = hasher.compute_str(doc2, 1).unwrap();
let sim = sig1.similarity(&sig2);
index.insert(sig1).unwrap();
let _candidates = index.insert(sig2).unwrap();
assert!(
sim < 0.5,
"Very different documents should have low similarity, got {}",
sim
);
}
#[test]
fn lsh_band_count_affects_collision_probability() {
let config_low = Config::default()
.with_num_bands(8)
.with_signature_size(128);
let config_high = Config::default()
.with_num_bands(32)
.with_signature_size(128);
let doc1 = "testing document content for band analysis with more text for stability";
let doc2 = "testing document content for band analysis with more text for stability!";
let hasher_low = MinHasher::new(&config_low).unwrap();
let sig1_low = hasher_low.compute_str(doc1, 0).unwrap();
let sig2_low = hasher_low.compute_str(doc2, 1).unwrap();
let hasher_high = MinHasher::new(&config_high).unwrap();
let sig1_high = hasher_high.compute_str(doc1, 0).unwrap();
let sig2_high = hasher_high.compute_str(doc2, 1).unwrap();
assert!(sig1_low.similarity(&sig2_low) > 0.9);
assert!(sig1_high.similarity(&sig2_high) > 0.9);
let mut index_low = LshIndex::new(&config_low).unwrap();
index_low.insert(sig1_low).unwrap();
let candidates_low = index_low.insert(sig2_low).unwrap();
let mut index_high = LshIndex::new(&config_high).unwrap();
index_high.insert(sig1_high).unwrap();
let candidates_high = index_high.insert(sig2_high).unwrap();
assert!(candidates_low.contains(&0) || !candidates_low.is_empty());
assert!(candidates_high.contains(&0) || !candidates_high.is_empty());
}
#[test]
fn lsh_cluster_detection_is_accurate() {
let config = Config::default()
.with_similarity_threshold(0.9);
let mut index = LshIndex::new(&config).unwrap();
let hasher = MinHasher::new(&config).unwrap();
for i in 0..5 {
let sig = hasher.compute_str("identical cluster content for all five documents", i).unwrap();
index.insert(sig).unwrap();
}
for i in 5..10 {
let doc = format!("unique content for document number {} that is different", i);
let sig = hasher.compute_str(&doc, i).unwrap();
index.insert(sig).unwrap();
}
let clusters = index.find_clusters();
assert!(
clusters.len() >= 1,
"Expected at least 1 cluster for identical documents, found {}",
clusters.len()
);
let identical_cluster = clusters.iter().find(|c| c.len() == 5);
assert!(
identical_cluster.is_some(),
"Should find a cluster with 5 identical documents"
);
}
#[test]
fn signature_clone_and_equality() {
let original = MinHashSignature::new(
vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
42
);
let cloned = original.clone();
assert_eq!(original.values, cloned.values);
assert_eq!(original.doc_id, cloned.doc_id);
let different = MinHashSignature::new(
vec![10, 9, 8, 7, 6, 5, 4, 3, 2, 1],
42
);
assert_ne!(original.values, different.values);
}
#[test]
fn large_signature_clone_equality() {
let original = MinHashSignature::new(
(0..128).map(|i| i as u32 * 12345).collect(),
999
);
let cloned = original.clone();
assert_eq!(original.values, cloned.values);
assert_eq!(original.doc_id, cloned.doc_id);
}
#[test]
fn large_signature_equality_with_512_values() {
let original = MinHashSignature::new(
(0..512).map(|i| (i * 7919) as u32).collect(), 12345
);
let cloned = original.clone();
assert_eq!(original.values, cloned.values);
assert_eq!(original.doc_id, cloned.doc_id);
}
#[test]
fn similarity_is_symmetric() {
let hasher = create_hasher();
let doc1 = "document one content here";
let doc2 = "document two content there";
let sig1 = hasher.compute_str(doc1, 0).unwrap();
let sig2 = hasher.compute_str(doc2, 1).unwrap();
let sim12 = sig1.similarity(&sig2);
let sim21 = sig2.similarity(&sig1);
assert!(
(sim12 - sim21).abs() < f64::EPSILON,
"Similarity should be symmetric: {} vs {}",
sim12,
sim21
);
}
#[test]
fn similarity_bounds_are_valid() {
let hasher = create_hasher();
let docs = vec![
"completely different content one",
"completely different content two",
"some overlapping content here",
"some overlapping content there",
"identical content for testing",
"identical content for testing",
];
for i in 0..docs.len() {
for j in i..docs.len() {
let sig1 = hasher.compute_str(docs[i], i).unwrap();
let sig2 = hasher.compute_str(docs[j], j).unwrap();
let sim = sig1.similarity(&sig2);
assert!(
sim >= 0.0 && sim <= 1.0,
"Similarity {} between docs {} and {} out of bounds",
sim,
i,
j
);
}
}
}
#[test]
fn config_validation_prevents_invalid_configs() {
let result = Config::new(100, 16, 5, 0.9);
assert!(result.is_err());
let result = Config::new(128, 16, 5, 1.1);
assert!(result.is_err());
let result = Config::new(128, 16, 5, 0.0);
assert!(result.is_err());
let result = Config::new(128, 16, 0, 0.9);
assert!(result.is_err());
let result = Config::new(128, 0, 5, 0.9);
assert!(result.is_err());
}
#[test]
fn candidate_probability_formula_is_correct() {
let p0 = candidate_probability(0.0, 16, 8);
assert!((p0 - 0.0).abs() < f64::EPSILON);
let p1 = candidate_probability(1.0, 16, 8);
assert!((p1 - 1.0).abs() < f64::EPSILON);
let p_low = candidate_probability(0.5, 16, 8);
let p_mid = candidate_probability(0.7, 16, 8);
let p_high = candidate_probability(0.9, 16, 8);
assert!(p_low < p_mid, "Probability should increase with similarity");
assert!(p_mid < p_high, "Probability should increase with similarity");
}
#[test]
fn optimize_lsh_params_produces_reasonable_values() {
let (bands, rows) = optimize_lsh_params(128, 0.9);
assert_eq!(bands * rows, 128);
assert!(bands >= 4 && bands <= 128);
assert!(rows >= 1 && rows <= 128);
}
#[test]
fn transformer_handles_mixed_content() {
use dedup::DedupTransformer;
let config = Config::default();
let mut transformer = DedupTransformer::new(config).unwrap();
transformer.push(create_text_sample("exact duplicate content here", 0));
transformer.push(create_text_sample("exact duplicate content here", 1));
transformer.push(create_text_sample("exact duplicate content here", 2));
transformer.push(create_text_sample("near duplicate content here", 3));
transformer.push(create_text_sample("near duplicate content there", 4));
transformer.push(create_text_sample("completely unique document one xyz", 5));
transformer.push(create_text_sample("completely unique document two abc", 6));
let result = transformer.finish_batch();
assert!(
result.len() >= 3 && result.len() <= 7,
"Expected between 3 and 7 unique documents, got {}",
result.len()
);
}
#[test]
fn document_id_preserved_correctly() {
let config = Config::default();
let mut index = LshIndex::new(&config).unwrap();
let hasher = MinHasher::new(&config).unwrap();
let ids = vec![0, 5, 100, 1000];
for &id in &ids {
let sig = hasher.compute_str(&format!("doc {}", id), id).unwrap();
index.insert(sig).unwrap();
}
assert_eq!(index.doc_count(), 4);
let unique = index.get_unique_indices();
for id in &ids {
assert!(unique.contains(id), "Document {} should be in unique indices", id);
}
}
#[test]
fn band_hash_consistency() {
let sig = MinHashSignature::new(
(0..128).map(|i| (i * 123) as u32).collect(),
0
);
let h1 = sig.band_hash(0, 8);
let h2 = sig.band_hash(0, 8);
assert_eq!(h1, h2, "Band hash should be deterministic");
let h3 = sig.band_hash(8, 8);
assert_ne!(h1, h3, "Different bands should have different hashes");
}
#[test]
fn fast_hasher_different_seeds_produce_different_signatures() {
let config1 = Config::default().with_seed(12345);
let config2 = Config::default().with_seed(54321);
let hasher1 = MinHasher::new(&config1).unwrap();
let hasher2 = MinHasher::new(&config2).unwrap();
let doc = "test document for seed comparison";
let sig1 = hasher1.compute_str(doc, 0).unwrap();
let sig2 = hasher2.compute_str(doc, 0).unwrap();
assert_ne!(sig1.values, sig2.values, "Different seeds should produce different signatures");
}
#[test]
fn fast_hasher_same_seed_produces_same_signatures() {
let config = Config::default().with_seed(12345);
let hasher1 = MinHasher::new(&config).unwrap();
let hasher2 = MinHasher::new(&config).unwrap();
let doc = "test document for seed comparison";
let sig1 = hasher1.compute_str(doc, 0).unwrap();
let sig2 = hasher2.compute_str(doc, 0).unwrap();
assert_eq!(sig1.values, sig2.values, "Same seed should produce same signatures");
}
#[test]
fn compute_rows_per_band_edge_cases() {
assert_eq!(compute_rows_per_band(128, 16), Some(8));
assert_eq!(compute_rows_per_band(128, 8), Some(16));
assert_eq!(compute_rows_per_band(64, 4), Some(16));
assert_eq!(compute_rows_per_band(128, 3), None);
assert_eq!(compute_rows_per_band(100, 16), None);
assert_eq!(compute_rows_per_band(128, 0), None);
}
#[test]
fn signature_with_different_lengths_have_zero_similarity() {
let sig1 = MinHashSignature::new(vec![1, 2, 3, 4, 5], 0);
let sig2 = MinHashSignature::new(vec![1, 2, 3, 4], 1);
let sim = sig1.similarity(&sig2);
assert_eq!(sim, 0.0, "Different length signatures should have 0 similarity");
}
#[test]
fn empty_signature_has_zero_similarity() {
let sig1 = MinHashSignature::new(vec![], 0);
let sig2 = MinHashSignature::new(vec![1, 2, 3], 1);
let sim = sig1.similarity(&sig2);
assert_eq!(sim, 0.0, "Empty signature should have 0 similarity");
}
#[test]
fn stats_are_consistent() {
let config = Config::default();
let mut index = LshIndex::new(&config).unwrap();
let hasher = MinHasher::new(&config).unwrap();
let empty_stats = index.stats();
assert_eq!(empty_stats.doc_count, 0);
assert_eq!(empty_stats.total_buckets, 0);
for i in 0..10 {
let sig = hasher.compute_str(&format!("doc {}", i), i).unwrap();
index.insert(sig).unwrap();
}
let stats = index.stats();
assert_eq!(stats.doc_count, 10);
assert_eq!(stats.num_bands, 16);
assert_eq!(stats.rows_per_band, 8);
}
#[test]
fn memory_usage_tracked_correctly() {
let config = Config::default();
let mut index = LshIndex::new(&config).unwrap();
let hasher = MinHasher::new(&config).unwrap();
let initial_mem = index.memory_usage();
for i in 0..100 {
let sig = hasher.compute_str(&format!("document content number {}", i), i).unwrap();
index.insert(sig).unwrap();
}
let final_mem = index.memory_usage();
assert!(
final_mem > initial_mem,
"Memory usage should increase with more documents"
);
}
#[test]
fn cluster_operations_work_correctly() {
let mut cluster_info = DuplicateCluster::new(0, 0);
assert_eq!(cluster_info.len(), 1);
assert!(cluster_info.contains(0));
assert!(!cluster_info.contains(1));
cluster_info.add(1);
cluster_info.add(2);
assert_eq!(cluster_info.len(), 3);
assert!(cluster_info.contains(2));
let mut dup_cluster = DuplicateCluster::new(0, 5);
assert_eq!(dup_cluster.len(), 1);
assert!(!dup_cluster.is_duplicate());
dup_cluster.add(6);
assert_eq!(dup_cluster.len(), 2);
assert!(dup_cluster.is_duplicate());
assert!(dup_cluster.contains(6));
}
#[test]
fn index_update_replaces_signature() {
let config = Config::default();
let mut index = LshIndex::new(&config).unwrap();
let hasher = MinHasher::new(&config).unwrap();
let sig1 = hasher.compute_str("first version of document", 0).unwrap();
index.insert(sig1).unwrap();
let sig2 = hasher.compute_str("second version of document", 0).unwrap();
index.insert(sig2).unwrap();
assert_eq!(index.doc_count(), 1);
}
#[test]
fn shingle_iterator_produces_correct_count() {
let iter = ShingleIterator::new(b"hello", 2);
assert_eq!(iter.count_shingles(), 4);
let iter = ShingleIterator::new(b"test", 3);
assert_eq!(iter.count_shingles(), 2);
let iter = ShingleIterator::new(b"hi", 5);
assert_eq!(iter.count_shingles(), 0);
}
#[test]
fn batch_compute_matches_individual() {
let hasher = create_hasher();
let docs: Vec<&[u8]> = vec![
b"document one content here",
b"document two content there",
b"document three content everywhere",
];
let batch_results = hasher.compute_batch(&docs, 0);
let mut individual_results = Vec::new();
for (i, doc) in docs.iter().enumerate() {
individual_results.push(hasher.compute(doc, i));
}
assert_eq!(batch_results.len(), individual_results.len());
for (batch, individual) in batch_results.iter().zip(individual_results.iter()) {
assert_eq!(
batch.as_ref().unwrap().values,
individual.as_ref().unwrap().values
);
}
}
#[test]
fn verify_similarity_returns_none_for_missing_docs() {
let config = Config::default();
let index = LshIndex::new(&config).unwrap();
let sim = index.verify_similarity(0, 1);
assert!(sim.is_none(), "Should return None for missing documents");
}
#[test]
fn get_cluster_for_doc_returns_none_for_unclustered() {
let config = Config::default();
let mut index = LshIndex::new(&config).unwrap();
let hasher = MinHasher::new(&config).unwrap();
let sig = hasher.compute_str("unique document", 0).unwrap();
index.insert(sig).unwrap();
let cluster = index.get_cluster_for_doc(0);
assert!(cluster.is_none());
}
#[test]
fn lsh_stats_recall_estimate_reasonable() {
let config = Config::default();
let index = LshIndex::new(&config).unwrap();
let stats = index.stats();
let recall = stats.estimated_recall();
assert!(recall >= 0.0 && recall <= 1.0, "Recall should be in [0, 1], got {}", recall);
assert!(recall > 0.9, "Expected high recall at threshold, got {}", recall);
}
#[test]
fn dedup_transformer_unique_and_duplicate_counts() {
use dedup::DedupTransformer;
let config = Config::default();
let mut transformer = DedupTransformer::new(config).unwrap();
transformer.push(create_text_sample("doc a content xyz", 0));
transformer.push(create_text_sample("doc b content abc", 1));
transformer.push(create_text_sample("doc a content xyz", 2)); transformer.push(create_text_sample("doc b content abc", 3));
transformer.finish_batch();
assert_eq!(transformer.duplicate_count(), 2);
assert_eq!(transformer.unique_count(), 2);
}
#[test]
fn shingle_iterator_exact_size() {
let iter = ShingleIterator::new(b"hello world", 3);
let (low, high) = iter.size_hint();
assert_eq!(low, 9); assert_eq!(high, Some(9));
}
#[test]
fn signature_band_extraction_bounds() {
let sig = MinHashSignature::new(vec![1, 2, 3, 4, 5, 6, 7, 8], 0);
let band = sig.band(2, 3);
assert_eq!(band, &[3, 4, 5]);
let band = sig.band(7, 2);
assert_eq!(band, &[8]);
let band = sig.band(10, 2);
assert!(band.is_empty());
}
#[test]
fn is_duplicate_works_correctly() {
let config = Config::default();
let mut index = LshIndex::new(&config).unwrap();
let hasher = MinHasher::new(&config).unwrap();
let sig1 = hasher.compute_str("duplicate content here", 0).unwrap();
let sig2 = hasher.compute_str("duplicate content here", 1).unwrap();
index.insert(sig1).unwrap();
index.insert(sig2).unwrap();
index.find_clusters();
assert!(index.is_duplicate(0) || index.is_duplicate(1));
}
#[test]
fn signature_len_and_is_empty() {
let sig_empty = MinHashSignature::new(vec![], 0);
assert_eq!(sig_empty.len(), 0);
assert!(sig_empty.is_empty());
let sig_nonempty = MinHashSignature::new(vec![1, 2, 3], 0);
assert_eq!(sig_nonempty.len(), 3);
assert!(!sig_nonempty.is_empty());
}
#[test]
fn config_memory_estimation_is_positive() {
let config = Config::default();
let per_doc = config.estimated_memory_per_document();
let max_docs = config.max_documents_in_memory();
assert!(per_doc > 0, "Per-document memory estimate should be positive");
assert!(max_docs > 0, "Max documents estimate should be positive");
}