use super::async_index_builder::{AsyncIndexBuilder, AsyncIndexBuilderConfig};
use crate::distance::DistanceMetric;
use crate::index::hnsw::HnswIndex;
fn default_config() -> AsyncIndexBuilderConfig {
AsyncIndexBuilderConfig {
merge_threshold: 100,
segment_count: Some(2),
}
}
fn make_index(dim: usize) -> HnswIndex {
HnswIndex::new(dim, DistanceMetric::Cosine).expect("test index creation")
}
#[test]
fn test_new_creates_empty_builder() {
let builder = AsyncIndexBuilder::new(default_config());
assert_eq!(builder.buffer_len(), 0);
assert!(!builder.is_building());
}
#[test]
fn test_enqueue_adds_vectors() {
let builder = AsyncIndexBuilder::new(default_config());
let vectors = vec![(1_u64, vec![1.0_f32, 0.0, 0.0])];
let triggered = builder.enqueue(vectors);
assert!(!triggered);
assert_eq!(builder.buffer_len(), 1);
}
#[test]
fn test_enqueue_returns_true_at_threshold() {
let config = AsyncIndexBuilderConfig {
merge_threshold: 3,
segment_count: Some(1),
};
let builder = AsyncIndexBuilder::new(config);
assert!(!builder.enqueue(vec![(1, vec![1.0])]));
assert!(!builder.enqueue(vec![(2, vec![2.0])]));
assert!(builder.enqueue(vec![(3, vec![3.0])]));
}
#[test]
fn test_drain_buffer_returns_all_and_empties() {
let builder = AsyncIndexBuilder::new(default_config());
builder.enqueue(vec![(1, vec![1.0, 0.0]), (2, vec![0.0, 1.0])]);
assert_eq!(builder.buffer_len(), 2);
let drained = builder.drain_buffer();
assert_eq!(drained.len(), 2);
assert_eq!(builder.buffer_len(), 0);
}
#[test]
fn test_search_buffer_finds_vectors() {
let builder = AsyncIndexBuilder::new(default_config());
builder.enqueue(vec![
(1, vec![1.0, 0.0, 0.0]),
(2, vec![0.0, 1.0, 0.0]),
(3, vec![0.0, 0.0, 1.0]),
]);
let results = builder.search_buffer(&[1.0, 0.0, 0.0], 2, DistanceMetric::Cosine);
assert!(!results.is_empty());
assert_eq!(results[0].0, 1);
}
#[test]
fn test_search_buffer_empty() {
let builder = AsyncIndexBuilder::new(default_config());
let results = builder.search_buffer(&[1.0, 0.0], 5, DistanceMetric::Cosine);
assert!(results.is_empty());
}
#[test]
fn test_flush_sync_indexes_vectors() {
let dim = 4;
let index = make_index(dim);
let builder = AsyncIndexBuilder::new(default_config());
let vectors: Vec<(u64, Vec<f32>)> = (0..20)
.map(|i| {
let mut v = vec![0.0_f32; dim];
v[i % dim] = 1.0;
(i as u64, v)
})
.collect();
builder.enqueue(vectors);
assert_eq!(builder.buffer_len(), 20);
let indexed = builder.flush_sync(&index).unwrap();
assert_eq!(indexed, 20);
assert_eq!(builder.buffer_len(), 0);
assert_eq!(index.len(), 20);
}
#[test]
fn test_flush_sync_empty_buffer() {
let index = make_index(4);
let builder = AsyncIndexBuilder::new(default_config());
let indexed = builder.flush_sync(&index).unwrap();
assert_eq!(indexed, 0);
}
#[test]
fn test_merge_threshold_accessor() {
let config = AsyncIndexBuilderConfig {
merge_threshold: 5000,
..default_config()
};
let builder = AsyncIndexBuilder::new(config);
assert_eq!(builder.merge_threshold(), 5000);
}
#[test]
fn test_config_serde_roundtrip() {
let config = AsyncIndexBuilderConfig {
merge_threshold: 5000,
segment_count: Some(8),
};
let json = serde_json::to_string(&config).expect("serialize");
let restored: AsyncIndexBuilderConfig = serde_json::from_str(&json).expect("deserialize");
assert_eq!(restored.merge_threshold, 5000);
assert_eq!(restored.segment_count, Some(8));
}
#[test]
fn test_config_serde_defaults() {
let json = "{}";
let config: AsyncIndexBuilderConfig = serde_json::from_str(json).expect("deserialize empty");
assert_eq!(config.merge_threshold, 10_000);
assert!(config.segment_count.is_none());
}
#[test]
fn test_config_legacy_sync_mode_field_is_ignored() {
let legacy_json = r#"{"merge_threshold": 500, "segment_count": 4, "sync_mode": true}"#;
let config: AsyncIndexBuilderConfig =
serde_json::from_str(legacy_json).expect("legacy config must still deserialize");
assert_eq!(config.merge_threshold, 500);
assert_eq!(config.segment_count, Some(4));
}
#[test]
fn test_trigger_build_async_indexes_in_background() {
let dim = 4;
let index = std::sync::Arc::new(make_index(dim));
let builder = AsyncIndexBuilder::new(default_config());
let vectors: Vec<(u64, Vec<f32>)> = (0..20)
.map(|i| {
let mut v = vec![0.0_f32; dim];
v[i % dim] = 1.0;
(i as u64, v)
})
.collect();
builder.enqueue(vectors);
assert_eq!(builder.buffer_len(), 20);
builder.trigger_build_async(&index);
let start = std::time::Instant::now();
while builder.is_building() {
std::thread::sleep(std::time::Duration::from_millis(10));
if start.elapsed() > std::time::Duration::from_secs(10) {
panic!("background build did not complete within 10s");
}
}
assert_eq!(builder.buffer_len(), 0);
assert_eq!(index.len(), 20);
}
#[test]
fn test_trigger_build_async_noop_when_empty() {
let dim = 4;
let index = std::sync::Arc::new(make_index(dim));
let builder = AsyncIndexBuilder::new(default_config());
builder.trigger_build_async(&index);
assert!(!builder.is_building());
assert_eq!(index.len(), 0);
}
#[test]
fn test_trigger_build_async_skips_when_already_building() {
let dim = 4;
let index = std::sync::Arc::new(make_index(dim));
let builder = AsyncIndexBuilder::new(default_config());
let vectors: Vec<(u64, Vec<f32>)> = (0..10)
.map(|i| {
let mut v = vec![0.0_f32; dim];
v[i % dim] = 1.0;
(i as u64, v)
})
.collect();
builder.enqueue(vectors);
builder.trigger_build_async(&index);
let more: Vec<(u64, Vec<f32>)> = (10..15)
.map(|i| {
let mut v = vec![0.0_f32; dim];
v[i % dim] = 1.0;
(i as u64, v)
})
.collect();
builder.enqueue(more);
builder.trigger_build_async(&index);
let start = std::time::Instant::now();
while builder.is_building() {
std::thread::sleep(std::time::Duration::from_millis(10));
if start.elapsed() > std::time::Duration::from_secs(10) {
panic!("background build did not complete within 10s");
}
}
assert_eq!(index.len(), 10);
assert_eq!(builder.buffer_len(), 5);
}