Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,4 @@ ignore:
- "**/tests/**"
- "**/benches/**"
- "**/examples/**"
- "vectorset/src/main.rs" # binary entry point (Redis CLI), not unit-testable
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

42 changes: 42 additions & 0 deletions diskann-bftree/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,45 @@ impl Default for TestCallCount {
Self::new()
}
}

#[cfg(test)]
mod error_tests {
use super::*;

#[test]
fn vector_unavailable_display_variants() {
let deleted = VectorUnavailable {
id: 7,
err: VectorError::Deleted,
};
assert_eq!(deleted.to_string(), "vector 7 was deleted");

let not_found = VectorUnavailable {
id: 9,
err: VectorError::NotFound,
};
assert_eq!(not_found.to_string(), "vector 9 not found");
}

#[test]
fn vector_unavailable_acknowledge_is_noop() {
let transient = VectorUnavailable {
id: 1,
err: VectorError::Deleted,
};
// Acknowledging a transient deletion swallows it without producing an error.
transient.acknowledge("expected during traversal");
}

#[test]
fn vector_unavailable_escalate_produces_error() {
let transient = VectorUnavailable {
id: 3,
err: VectorError::NotFound,
};
let escalated: ANNError = transient.escalate("lookup failed");
let message = escalated.to_string();
assert!(message.contains("vector 3 not found"), "got: {message}");
assert!(message.contains("lookup failed"), "got: {message}");
}
}
18 changes: 18 additions & 0 deletions diskann-bftree/src/neighbors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,24 @@ mod tests {
assert!(neighbor_provider.get_neighbors(1, &mut result).is_err());
}

#[tokio::test]
async fn neighbor_error_paths() {
let provider = NeighborProvider::<u32>::new_with_config(6, Config::default()).unwrap();

// Reading a never-set id returns the NotFound error path.
let mut result = AdjacencyList::with_capacity(10);
assert!(provider.get_neighbors(42, &mut result).is_err());

// A neighbor list longer than the max degree is rejected.
let too_long: Vec<u32> = (0..=provider.max_degree()).collect();
let mut buf = vec![0u32; provider.dim];
assert!(provider.set_neighbors(7, &too_long, &mut buf).is_err());

// A write buffer shorter than `dim` is rejected.
let mut short_buf = vec![0u32; 1];
assert!(provider.set_neighbors(7, &[1, 2], &mut short_buf).is_err());
}

/// Test the interleaved and parallel traversal of the Bf-Tree
/// by invoking the async accessors of the neighbor list provider
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
Expand Down
23 changes: 23 additions & 0 deletions diskann-bftree/src/quant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,29 @@ mod tests {
assert_eq!(quant_bytes, provider.quantizer.bytes());
}

#[tokio::test]
async fn get_vector_into_error_paths() {
let provider = create_test_provider();
let expected = provider.quantizer.bytes();

// Wrong buffer length → Error.
let mut wrong = vec![0u8; expected + 1];
match provider.get_vector_into(0, &mut wrong).unwrap_err() {
AccessError::Error(_) => {}
other => panic!("expected Error for wrong buffer length, got {other:?}"),
}

// Unset id within an empty slot → NotFound transient.
let mut buffer = vec![0u8; expected];
match provider.get_vector_into(99, &mut buffer).unwrap_err() {
AccessError::Transient(VectorUnavailable {
id,
err: VectorError::NotFound,
}) => assert_eq!(id, 99),
other => panic!("expected NotFound transient, got {other:?}"),
}
}

fn create_test_provider() -> QuantVectorProvider {
let dim = 2;

Expand Down
40 changes: 40 additions & 0 deletions diskann-bftree/src/vectors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -306,4 +306,44 @@ mod tests {
let result = provider.get_vector_sync(0).unwrap();
assert_eq!(result, vec![1.0, 2.0, 3.0]);
}

#[test]
fn get_vector_into_wrong_buffer_dim_errors() {
let provider = VectorProvider::<f32>::new_with_config(5, 3, 0, Config::default()).unwrap();
let mut buffer = [0.0f32; 2];
let err = provider.get_vector_into(0, &mut buffer).unwrap_err();
assert!(matches!(err, RankedError::Error(_)));
}

#[test]
fn get_unset_vector_reports_not_found() {
let provider = VectorProvider::<f32>::new_with_config(5, 3, 0, Config::default()).unwrap();
match provider.get_vector_sync(2).unwrap_err() {
RankedError::Transient(VectorUnavailable {
id,
err: VectorError::NotFound,
}) => assert_eq!(id, 2),
other => panic!("expected NotFound transient, got {other:?}"),
}
}

#[test]
fn deleted_vector_reports_deleted() {
let provider = VectorProvider::<f32>::new_with_config(5, 3, 0, Config::default()).unwrap();
provider.set_vector_sync(0, &[1.0, 2.0, 3.0]).unwrap();
provider.delete_vector(0);
match provider.get_vector_sync(0).unwrap_err() {
RankedError::Transient(VectorUnavailable {
id,
err: VectorError::Deleted,
}) => assert_eq!(id, 0),
other => panic!("expected Deleted transient, got {other:?}"),
}
}

#[test]
fn starting_points_and_getters() {
let provider = VectorProvider::<f32>::new_with_config(5, 3, 2, Config::default()).unwrap();
assert_eq!(provider.starting_points().unwrap(), vec![5u32, 6u32]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -178,3 +178,221 @@ where
self.status_by_internal_id(context, internal_id).await
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::attribute::{Attribute, AttributeValue};
use crate::document::Document;
use crate::encoded_attribute_provider::roaring_attribute_store::RoaringAttributeStore;
use crate::traits::attribute_store::AttributeStore;
use diskann::provider::{DefaultContext, ElementStatus, Guard, NoopGuard};
use std::collections::HashMap;
use std::sync::Mutex;

/// Minimal in-memory `DataProvider` that records element statuses and uses
/// an identity external-to-internal id mapping.
#[derive(Default)]
struct MockProvider {
statuses: Mutex<HashMap<u32, ElementStatus>>,
released: Mutex<Vec<u32>>,
}

impl MockProvider {
fn set_status(&self, id: u32, status: ElementStatus) {
self.statuses.lock().unwrap().insert(id, status);
}
}

impl DataProvider for MockProvider {
type Context = DefaultContext;
type InternalId = u32;
type ExternalId = u32;
type Error = ANNError;
type Guard = NoopGuard<u32>;

fn to_internal_id(&self, _ctx: &Self::Context, gid: &u32) -> Result<u32, ANNError> {
Ok(*gid)
}

fn to_external_id(&self, _ctx: &Self::Context, id: u32) -> Result<u32, ANNError> {
Ok(id)
}
}

impl<T: Send> SetElement<T> for MockProvider {
type SetError = ANNError;

async fn set_element(
&self,
_ctx: &Self::Context,
id: &u32,
_element: T,
) -> Result<NoopGuard<u32>, ANNError> {
self.set_status(*id, ElementStatus::Valid);
Ok(NoopGuard::new(*id))
}
}

impl Delete for MockProvider {
async fn delete(&self, _ctx: &Self::Context, gid: &u32) -> Result<(), ANNError> {
self.set_status(*gid, ElementStatus::Deleted);
Ok(())
}

async fn release(&self, _ctx: &Self::Context, id: u32) -> Result<(), ANNError> {
self.released.lock().unwrap().push(id);
Ok(())
}

async fn status_by_internal_id(
&self,
_ctx: &Self::Context,
id: u32,
) -> Result<ElementStatus, ANNError> {
Ok(self
.statuses
.lock()
.unwrap()
.get(&id)
.copied()
.unwrap_or(ElementStatus::Deleted))
}

async fn status_by_external_id(
&self,
ctx: &Self::Context,
gid: &u32,
) -> Result<ElementStatus, ANNError> {
let id = self.to_internal_id(ctx, gid)?;
self.status_by_internal_id(ctx, id).await
}
}

/// Drive a future to completion on the current thread. The futures produced
/// here never suspend, so a simple poll loop is sufficient.
fn block_on<F: std::future::Future>(fut: F) -> F::Output {
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
fn raw() -> RawWaker {
fn no_op(_: *const ()) {}
fn clone(_: *const ()) -> RawWaker {
raw()
}
RawWaker::new(
std::ptr::null(),
&RawWakerVTable::new(clone, no_op, no_op, no_op),
)
}
// SAFETY: the vtable functions are all no-ops operating on a null pointer.
let waker = unsafe { Waker::from_raw(raw()) };
let mut cx = Context::from_waker(&waker);
let mut fut = std::pin::pin!(fut);
loop {
match fut.as_mut().poll(&mut cx) {
Poll::Ready(v) => return v,
Poll::Pending => std::hint::spin_loop(),
}
}
Comment on lines +290 to +295
}

type Dp = DocumentProvider<MockProvider, RoaringAttributeStore<u32>>;

fn make() -> Dp {
DocumentProvider::new(MockProvider::default(), RoaringAttributeStore::<u32>::new())
}

fn attrs() -> Vec<Attribute> {
vec![Attribute::from_value(
"category",
AttributeValue::String("electronics".to_owned()),
)]
}

#[test]
fn id_translation_delegates_to_inner() {
let dp = make();
let ctx = DefaultContext;
assert_eq!(dp.to_internal_id(&ctx, &9).unwrap(), 9);
assert_eq!(dp.to_external_id(&ctx, 9).unwrap(), 9);
}

#[test]
fn attribute_accessor_is_available() {
let dp = make();
assert!(dp.attribute_accessor().is_ok());
}

#[test]
fn set_element_marks_vector_and_attributes() {
let dp = make();
let ctx = DefaultContext;
let vector = vec![1.0_f32, 2.0, 3.0];
let doc = Document::new(&vector, attrs());

let guard = block_on(dp.set_element(&ctx, &1, doc)).unwrap();
assert_eq!(guard.id(), 1);

// Present in both the data store and the attribute store -> valid.
let status = block_on(dp.status_by_internal_id(&ctx, 1)).unwrap();
assert_eq!(status, ElementStatus::Valid);

// The external-id path resolves to the same status.
let status = block_on(dp.status_by_external_id(&ctx, &1)).unwrap();
assert_eq!(status, ElementStatus::Valid);
}

#[test]
fn status_of_unknown_id_is_deleted() {
let dp = make();
let ctx = DefaultContext;
// Absent from both stores -> reported as deleted.
let status = block_on(dp.status_by_internal_id(&ctx, 42)).unwrap();
assert_eq!(status, ElementStatus::Deleted);
}

#[test]
fn status_errors_when_attribute_present_but_data_deleted() {
let dp = make();
let ctx = DefaultContext;
// Attribute store knows the id, data store does not.
dp.attribute_store().set_element(&5, &attrs()).unwrap();
let result = block_on(dp.status_by_internal_id(&ctx, 5));
assert!(result.is_err());
}

#[test]
fn status_errors_when_data_valid_but_attribute_absent() {
let dp = make();
let ctx = DefaultContext;
// Data store reports valid, attribute store has no record.
dp.inner_provider().set_status(6, ElementStatus::Valid);
let result = block_on(dp.status_by_internal_id(&ctx, 6));
assert!(result.is_err());
}

#[test]
fn delete_delegates_to_inner_provider() {
let dp = make();
let ctx = DefaultContext;
let vector = vec![0.0_f32];
block_on(dp.set_element(&ctx, &1, Document::new(&vector, attrs()))).unwrap();

block_on(dp.delete(&ctx, &1)).unwrap();

// The inner provider now reports the id as deleted.
let status = block_on(dp.inner_provider().status_by_internal_id(&ctx, 1)).unwrap();
assert_eq!(status, ElementStatus::Deleted);
}

#[test]
fn release_removes_attributes_and_releases_slot() {
let dp = make();
let ctx = DefaultContext;
let vector = vec![0.0_f32];
block_on(dp.set_element(&ctx, &1, Document::new(&vector, attrs()))).unwrap();

block_on(dp.release(&ctx, 1)).unwrap();

assert!(dp.inner_provider().released.lock().unwrap().contains(&1));
}
}
Loading