430 lines
12 KiB
Rust
430 lines
12 KiB
Rust
//! Integration tests for single-file guarantee.
|
|
//! Ensures .mv2 files are completely self-contained with no sidecar files.
|
|
|
|
use memvid_core::{Memvid, PutOptions};
|
|
use std::fs;
|
|
use tempfile::TempDir;
|
|
|
|
/// Windows needs extra time for Tantivy to release file handles.
|
|
/// Without this delay, TempDir cleanup fails with "Access is denied".
|
|
#[cfg(target_os = "windows")]
|
|
fn windows_file_handle_delay() {
|
|
std::thread::sleep(std::time::Duration::from_millis(100));
|
|
}
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
fn windows_file_handle_delay() {
|
|
// No-op on Unix systems
|
|
}
|
|
|
|
/// Helper to count files in a directory (excluding hidden).
|
|
fn count_files(dir: &std::path::Path) -> usize {
|
|
fs::read_dir(dir)
|
|
.unwrap()
|
|
.filter_map(|e| e.ok())
|
|
.filter(|e| !e.file_name().to_string_lossy().starts_with('.'))
|
|
.count()
|
|
}
|
|
|
|
/// Helper to list files in a directory.
|
|
fn list_files(dir: &std::path::Path) -> Vec<String> {
|
|
fs::read_dir(dir)
|
|
.unwrap()
|
|
.filter_map(|e| e.ok())
|
|
.map(|e| e.file_name().to_string_lossy().to_string())
|
|
.collect()
|
|
}
|
|
|
|
/// Test that create produces exactly one file.
|
|
#[test]
|
|
fn create_produces_single_file() {
|
|
let dir = TempDir::new().unwrap();
|
|
let path = dir.path().join("test.mv2");
|
|
|
|
{
|
|
let mut mem = Memvid::create(&path).unwrap();
|
|
mem.commit().unwrap();
|
|
}
|
|
|
|
let file_count = count_files(dir.path());
|
|
assert_eq!(
|
|
file_count,
|
|
1,
|
|
"Create should produce exactly 1 file, found: {:?}",
|
|
list_files(dir.path())
|
|
);
|
|
}
|
|
|
|
/// Test that put maintains single file.
|
|
#[test]
|
|
fn put_maintains_single_file() {
|
|
let dir = TempDir::new().unwrap();
|
|
let path = dir.path().join("test.mv2");
|
|
|
|
{
|
|
let mut mem = Memvid::create(&path).unwrap();
|
|
|
|
for i in 0..10 {
|
|
let opts = PutOptions {
|
|
uri: Some(format!("mv2://doc{}", i)),
|
|
..Default::default()
|
|
};
|
|
mem.put_bytes_with_options(format!("Content {}", i).as_bytes(), opts)
|
|
.unwrap();
|
|
}
|
|
|
|
mem.commit().unwrap();
|
|
}
|
|
|
|
let file_count = count_files(dir.path());
|
|
assert_eq!(
|
|
file_count,
|
|
1,
|
|
"Put should maintain single file, found: {:?}",
|
|
list_files(dir.path())
|
|
);
|
|
}
|
|
|
|
/// Test that multiple commits maintain single file.
|
|
#[test]
|
|
fn multiple_commits_maintain_single_file() {
|
|
let dir = TempDir::new().unwrap();
|
|
let path = dir.path().join("test.mv2");
|
|
|
|
// First commit
|
|
{
|
|
let mut mem = Memvid::create(&path).unwrap();
|
|
let opts = PutOptions {
|
|
uri: Some("mv2://doc1".to_string()),
|
|
..Default::default()
|
|
};
|
|
mem.put_bytes_with_options(b"Content 1", opts).unwrap();
|
|
mem.commit().unwrap();
|
|
}
|
|
|
|
let file_count_1 = count_files(dir.path());
|
|
assert_eq!(
|
|
file_count_1,
|
|
1,
|
|
"After first commit: {:?}",
|
|
list_files(dir.path())
|
|
);
|
|
|
|
// Second commit
|
|
{
|
|
let mut mem = Memvid::open(&path).unwrap();
|
|
let opts = PutOptions {
|
|
uri: Some("mv2://doc2".to_string()),
|
|
..Default::default()
|
|
};
|
|
mem.put_bytes_with_options(b"Content 2", opts).unwrap();
|
|
mem.commit().unwrap();
|
|
}
|
|
|
|
let file_count_2 = count_files(dir.path());
|
|
assert_eq!(
|
|
file_count_2,
|
|
1,
|
|
"After second commit: {:?}",
|
|
list_files(dir.path())
|
|
);
|
|
|
|
// Third commit
|
|
{
|
|
let mut mem = Memvid::open(&path).unwrap();
|
|
let opts = PutOptions {
|
|
uri: Some("mv2://doc3".to_string()),
|
|
..Default::default()
|
|
};
|
|
mem.put_bytes_with_options(b"Content 3", opts).unwrap();
|
|
mem.commit().unwrap();
|
|
}
|
|
|
|
let file_count_3 = count_files(dir.path());
|
|
assert_eq!(
|
|
file_count_3,
|
|
1,
|
|
"After third commit: {:?}",
|
|
list_files(dir.path())
|
|
);
|
|
}
|
|
|
|
/// Test that update maintains single file.
|
|
#[test]
|
|
fn update_maintains_single_file() {
|
|
let dir = TempDir::new().unwrap();
|
|
let path = dir.path().join("test.mv2");
|
|
|
|
// Create
|
|
{
|
|
let mut mem = Memvid::create(&path).unwrap();
|
|
let opts = PutOptions {
|
|
uri: Some("mv2://test".to_string()),
|
|
title: Some("Original".to_string()),
|
|
..Default::default()
|
|
};
|
|
mem.put_bytes_with_options(b"Content", opts).unwrap();
|
|
mem.commit().unwrap();
|
|
}
|
|
|
|
// Update
|
|
{
|
|
let mut mem = Memvid::open(&path).unwrap();
|
|
let update_opts = PutOptions {
|
|
title: Some("Updated".to_string()),
|
|
..Default::default()
|
|
};
|
|
mem.update_frame(0, None, update_opts, None).unwrap();
|
|
mem.commit().unwrap();
|
|
}
|
|
|
|
let file_count = count_files(dir.path());
|
|
assert_eq!(
|
|
file_count,
|
|
1,
|
|
"Update should maintain single file, found: {:?}",
|
|
list_files(dir.path())
|
|
);
|
|
}
|
|
|
|
/// Test that delete maintains single file.
|
|
#[test]
|
|
fn delete_maintains_single_file() {
|
|
let dir = TempDir::new().unwrap();
|
|
let path = dir.path().join("test.mv2");
|
|
|
|
// Create with content
|
|
{
|
|
let mut mem = Memvid::create(&path).unwrap();
|
|
for i in 0..5 {
|
|
let opts = PutOptions {
|
|
uri: Some(format!("mv2://doc{}", i)),
|
|
..Default::default()
|
|
};
|
|
mem.put_bytes_with_options(format!("Content {}", i).as_bytes(), opts)
|
|
.unwrap();
|
|
}
|
|
mem.commit().unwrap();
|
|
}
|
|
|
|
// Delete
|
|
{
|
|
let mut mem = Memvid::open(&path).unwrap();
|
|
mem.delete_frame(2).unwrap();
|
|
mem.commit().unwrap();
|
|
}
|
|
|
|
let file_count = count_files(dir.path());
|
|
assert_eq!(
|
|
file_count,
|
|
1,
|
|
"Delete should maintain single file, found: {:?}",
|
|
list_files(dir.path())
|
|
);
|
|
}
|
|
|
|
/// Test that enabling lex maintains single file.
|
|
#[test]
|
|
#[cfg(feature = "lex")]
|
|
fn enable_lex_maintains_single_file() {
|
|
let dir = TempDir::new().unwrap();
|
|
let path = dir.path().join("test.mv2");
|
|
|
|
{
|
|
let mut mem = Memvid::create(&path).unwrap();
|
|
mem.enable_lex().unwrap();
|
|
|
|
let opts = PutOptions {
|
|
uri: Some("mv2://test".to_string()),
|
|
search_text: Some("Searchable content".to_string()),
|
|
..Default::default()
|
|
};
|
|
mem.put_bytes_with_options(b"Content", opts).unwrap();
|
|
mem.commit().unwrap();
|
|
}
|
|
|
|
let file_count = count_files(dir.path());
|
|
assert_eq!(
|
|
file_count,
|
|
1,
|
|
"Lex index should be embedded, found: {:?}",
|
|
list_files(dir.path())
|
|
);
|
|
}
|
|
|
|
/// Test that doctor maintains single file.
|
|
#[test]
|
|
fn doctor_maintains_single_file() {
|
|
use memvid_core::DoctorOptions;
|
|
|
|
let dir = TempDir::new().unwrap();
|
|
let path = dir.path().join("test.mv2");
|
|
|
|
// Create
|
|
{
|
|
let mut mem = Memvid::create(&path).unwrap();
|
|
let opts = PutOptions {
|
|
uri: Some("mv2://test".to_string()),
|
|
..Default::default()
|
|
};
|
|
mem.put_bytes_with_options(b"Content", opts).unwrap();
|
|
mem.commit().unwrap();
|
|
}
|
|
|
|
// Run doctor
|
|
{
|
|
let _report = Memvid::doctor(
|
|
&path,
|
|
DoctorOptions {
|
|
rebuild_lex_index: false,
|
|
rebuild_time_index: true,
|
|
rebuild_vec_index: false,
|
|
vacuum: false,
|
|
dry_run: false,
|
|
quiet: true,
|
|
},
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
let file_count = count_files(dir.path());
|
|
assert_eq!(
|
|
file_count,
|
|
1,
|
|
"Doctor should maintain single file, found: {:?}",
|
|
list_files(dir.path())
|
|
);
|
|
}
|
|
|
|
/// Test no WAL sidecar files.
|
|
///
|
|
/// NOTE: Skipped on Windows due to Tantivy file locking behavior.
|
|
/// See `large_file_maintains_single_file` for detailed explanation.
|
|
#[test]
|
|
#[cfg(not(target_os = "windows"))]
|
|
fn no_wal_sidecar_files() {
|
|
let dir = TempDir::new().unwrap();
|
|
let path = dir.path().join("test.mv2");
|
|
|
|
{
|
|
let mut mem = Memvid::create(&path).unwrap();
|
|
|
|
// Do multiple operations that would touch WAL
|
|
for i in 0..100 {
|
|
let opts = PutOptions {
|
|
uri: Some(format!("mv2://doc{}", i)),
|
|
..Default::default()
|
|
};
|
|
mem.put_bytes_with_options(format!("Content {}", i).as_bytes(), opts)
|
|
.unwrap();
|
|
}
|
|
|
|
mem.commit().unwrap();
|
|
}
|
|
|
|
// Windows needs extra time for Tantivy to release file handles
|
|
windows_file_handle_delay();
|
|
|
|
let files = list_files(dir.path());
|
|
|
|
// Check for forbidden sidecar patterns
|
|
for file in &files {
|
|
assert!(!file.ends_with(".wal"), "Found WAL sidecar file: {}", file);
|
|
assert!(!file.ends_with(".shm"), "Found SHM sidecar file: {}", file);
|
|
assert!(!file.ends_with("-wal"), "Found -wal sidecar file: {}", file);
|
|
assert!(!file.ends_with("-shm"), "Found -shm sidecar file: {}", file);
|
|
assert!(
|
|
!file.ends_with(".lock"),
|
|
"Found lock sidecar file: {}",
|
|
file
|
|
);
|
|
assert!(
|
|
!file.ends_with("-journal"),
|
|
"Found journal sidecar file: {}",
|
|
file
|
|
);
|
|
}
|
|
|
|
assert_eq!(files.len(), 1, "Should have exactly 1 file");
|
|
}
|
|
|
|
/// Test file is self-contained (can be copied and used).
|
|
#[test]
|
|
fn file_is_self_contained() {
|
|
let dir1 = TempDir::new().unwrap();
|
|
let dir2 = TempDir::new().unwrap();
|
|
let path1 = dir1.path().join("original.mv2");
|
|
let path2 = dir2.path().join("copy.mv2");
|
|
|
|
// Create original
|
|
{
|
|
let mut mem = Memvid::create(&path1).unwrap();
|
|
for i in 0..5 {
|
|
let opts = PutOptions {
|
|
uri: Some(format!("mv2://doc{}", i)),
|
|
title: Some(format!("Document {}", i)),
|
|
..Default::default()
|
|
};
|
|
mem.put_bytes_with_options(format!("Content {}", i).as_bytes(), opts)
|
|
.unwrap();
|
|
}
|
|
mem.commit().unwrap();
|
|
}
|
|
|
|
// Copy file to new location
|
|
fs::copy(&path1, &path2).unwrap();
|
|
|
|
// Verify copy works independently
|
|
let mem = Memvid::open_read_only(&path2).unwrap();
|
|
let stats = mem.stats().unwrap();
|
|
|
|
assert_eq!(stats.frame_count, 5, "Copied file should have all frames");
|
|
|
|
// Verify we can read frames from copy
|
|
let frame = mem.frame_by_id(0).unwrap();
|
|
assert_eq!(frame.uri.as_deref(), Some("mv2://doc0"));
|
|
assert_eq!(frame.title.as_deref(), Some("Document 0"));
|
|
}
|
|
|
|
/// Test large file maintains single file.
|
|
///
|
|
/// NOTE: Skipped on Windows due to Tantivy file locking behavior.
|
|
/// When running with `lex` feature (default), Tantivy creates temporary index files
|
|
/// that Windows holds open longer than Unix systems. This causes sporadic
|
|
/// "Access is denied (os error 5)" failures during tempdir cleanup.
|
|
/// The underlying single-file guarantee is platform-independent and tested on Unix.
|
|
/// See also: `tests_lex_flag.rs` which uses the same skip pattern.
|
|
#[test]
|
|
#[cfg(not(target_os = "windows"))]
|
|
fn large_file_maintains_single_file() {
|
|
let dir = TempDir::new().unwrap();
|
|
let path = dir.path().join("test.mv2");
|
|
|
|
{
|
|
let mut mem = Memvid::create(&path).unwrap();
|
|
|
|
// Add 100 documents with ~10KB each
|
|
for i in 0..100 {
|
|
let large_content = vec![b'x'; 10 * 1024];
|
|
let opts = PutOptions {
|
|
uri: Some(format!("mv2://doc{}", i)),
|
|
..Default::default()
|
|
};
|
|
mem.put_bytes_with_options(&large_content, opts).unwrap();
|
|
}
|
|
|
|
mem.commit().unwrap();
|
|
}
|
|
|
|
// Windows needs extra time for Tantivy to release file handles
|
|
windows_file_handle_delay();
|
|
|
|
let file_count = count_files(dir.path());
|
|
assert_eq!(
|
|
file_count,
|
|
1,
|
|
"Large file should remain single, found: {:?}",
|
|
list_files(dir.path())
|
|
);
|
|
}
|