use serial_test::serial;
use terraphim_persistence::{DeviceStorage, Persistable, Result};
use terraphim_types::{Document, NormalizedTerm, NormalizedTermValue, Thesaurus};
async fn init_test_persistence() -> Result<()> {
DeviceStorage::init_memory_only().await?;
Ok(())
}
#[tokio::test]
#[serial]
async fn test_compression_integration_with_persistence() -> Result<()> {
init_test_persistence().await?;
println!("Testing compression integration with persistence");
let large_body = "x".repeat(1024 * 1024 + 1000);
let document = Document {
id: "large-doc-compression-test".to_string(),
title: "Large Document for Compression Test".to_string(),
body: large_body.clone(),
url: "https://example.com/large".to_string(),
description: Some("Testing compression".to_string()),
..Default::default()
};
document.save_to_one("memory").await?;
let mut loaded = Document {
id: "large-doc-compression-test".to_string(),
..Default::default()
};
loaded = loaded.load().await?;
assert_eq!(loaded.title, "Large Document for Compression Test");
assert_eq!(loaded.body.len(), large_body.len());
assert_eq!(loaded.body, large_body);
println!(" Large document saved and loaded successfully");
println!(" Document body size: {} bytes", loaded.body.len());
Ok(())
}
#[tokio::test]
#[serial]
async fn test_small_data_not_compressed() -> Result<()> {
init_test_persistence().await?;
println!("Testing small data persistence (no compression)");
let mut thesaurus = Thesaurus::new("Small Test".to_string());
let term = NormalizedTerm::new(1, NormalizedTermValue::from("concept".to_string()));
thesaurus.insert(NormalizedTermValue::from("test".to_string()), term);
thesaurus.save_to_one("memory").await?;
let mut loaded = Thesaurus::new("Small Test".to_string());
loaded = loaded.load().await?;
assert_eq!(loaded.len(), 1);
assert_eq!(loaded.name(), "Small Test");
println!(" Small thesaurus saved and loaded successfully");
println!(" Thesaurus size: {} entries", loaded.len());
Ok(())
}
#[tokio::test]
#[serial]
async fn test_save_load_roundtrip_integrity() -> Result<()> {
init_test_persistence().await?;
println!("Testing save/load roundtrip data integrity");
let test_cases = vec![
("tiny", 100),
("small", 1000),
("medium", 10000),
("larger", 100000),
];
for (name, size) in test_cases {
let body = format!("Content {} ", name).repeat(size / 10);
let document = Document {
id: format!("roundtrip-{}", name),
title: format!("Roundtrip Test {}", name),
body: body.clone(),
url: format!("https://example.com/{}", name),
description: Some(format!("Testing {} content", name)),
..Default::default()
};
document.save_to_one("memory").await?;
let mut loaded = Document {
id: format!("roundtrip-{}", name),
..Default::default()
};
loaded = loaded.load().await?;
assert_eq!(loaded.title, format!("Roundtrip Test {}", name));
assert_eq!(loaded.body, body);
assert_eq!(loaded.url, format!("https://example.com/{}", name));
assert_eq!(
loaded.description,
Some(format!("Testing {} content", name))
);
println!(" {} content ({} bytes): OK", name, body.len());
}
Ok(())
}
#[tokio::test]
#[serial]
async fn test_multiple_documents_concurrent_access() -> Result<()> {
init_test_persistence().await?;
println!("Testing concurrent document operations");
let mut handles = vec![];
for i in 0..10 {
let handle = tokio::spawn(async move {
let document = Document {
id: format!("concurrent-doc-{}", i),
title: format!("Concurrent Document {}", i),
body: format!("Content for document {}", i),
..Default::default()
};
document.save().await
});
handles.push(handle);
}
for handle in handles {
handle.await.expect("Task panicked")?;
}
for i in 0..10 {
let mut loaded = Document {
id: format!("concurrent-doc-{}", i),
..Default::default()
};
loaded = loaded.load().await?;
assert_eq!(loaded.title, format!("Concurrent Document {}", i));
}
println!(" 10 concurrent documents saved and verified");
Ok(())
}
#[tokio::test]
#[serial]
async fn test_persistence_with_decompression_on_load() -> Result<()> {
use terraphim_persistence::compression::{maybe_compress, maybe_decompress};
println!("Testing decompression during load");
let large_data = "test data ".repeat(200000); let original = large_data.as_bytes();
let compressed = maybe_compress(original);
assert!(
compressed.len() < original.len(),
"Data should be compressed"
);
assert_eq!(&compressed[..4], b"ZSTD", "Should have ZSTD magic header");
let decompressed = maybe_decompress(&compressed)?;
assert_eq!(decompressed, original.to_vec());
println!(
" Compression ratio: {:.1}%",
(1.0 - (compressed.len() as f64 / original.len() as f64)) * 100.0
);
println!(
" Original: {} bytes, Compressed: {} bytes",
original.len(),
compressed.len()
);
Ok(())
}
#[tokio::test]
#[serial]
async fn test_schema_evolution_recovery_simulation() -> Result<()> {
init_test_persistence().await?;
println!("Testing schema evolution recovery simulation");
let document = Document {
id: "schema-evolution-test".to_string(),
title: "Schema Test".to_string(),
body: "Test content".to_string(),
..Default::default()
};
document.save_to_one("memory").await?;
let mut loaded = Document {
id: "schema-evolution-test".to_string(),
..Default::default()
};
loaded = loaded.load().await?;
assert_eq!(loaded.title, "Schema Test");
assert_eq!(loaded.body, "Test content");
println!(" Schema evolution path tested successfully");
Ok(())
}
#[tokio::test]
#[serial]
async fn test_load_performance_not_blocked_by_cache_writeback() -> Result<()> {
init_test_persistence().await?;
println!("Testing that load is not blocked by cache write-back");
let body = "performance test data ".repeat(10000);
let document = Document {
id: "perf-test-doc".to_string(),
title: "Performance Test".to_string(),
body: body.clone(),
..Default::default()
};
document.save_to_one("memory").await?;
let start = std::time::Instant::now();
let mut loaded = Document {
id: "perf-test-doc".to_string(),
..Default::default()
};
loaded = loaded.load().await?;
let duration = start.elapsed();
assert!(
duration.as_millis() < 100,
"Load took too long: {:?}",
duration
);
assert_eq!(loaded.body, body);
println!(" Load completed in {:?}", duration);
Ok(())
}
#[tokio::test]
#[serial]
async fn test_tracing_spans_in_load_path() -> Result<()> {
init_test_persistence().await?;
println!("Testing that load path includes tracing spans");
let _ = tracing_subscriber::fmt()
.with_env_filter("terraphim_persistence=debug")
.try_init();
let document = Document {
id: "tracing-test-doc".to_string(),
title: "Tracing Test".to_string(),
body: "Test content for tracing".to_string(),
..Default::default()
};
document.save().await?;
let mut loaded = Document {
id: "tracing-test-doc".to_string(),
..Default::default()
};
loaded = loaded.load().await?;
assert_eq!(loaded.title, "Tracing Test");
println!(" Load path with tracing spans completed");
println!(" (Check logs for debug_span entries if RUST_LOG is set)");
Ok(())
}
#[tokio::test]
#[serial]
async fn test_concurrent_duplicate_writes_last_write_wins() -> Result<()> {
init_test_persistence().await?;
println!("Testing concurrent duplicate writes (last-write-wins behavior)");
let doc_id = "concurrent-write-test";
let mut handles = vec![];
for i in 0..5 {
let id = doc_id.to_string();
let handle = tokio::spawn(async move {
let document = Document {
id: id.clone(),
title: format!("Version {}", i),
body: format!("Content from writer {}", i),
..Default::default()
};
document.save().await
});
handles.push(handle);
}
for handle in handles {
handle.await.expect("Task panicked")?;
}
let mut loaded = Document {
id: doc_id.to_string(),
..Default::default()
};
loaded = loaded.load().await?;
assert!(loaded.title.starts_with("Version "));
assert!(loaded.body.starts_with("Content from writer "));
println!(" Last-write-wins: Got '{}'", loaded.title);
println!(" Concurrent writes handled correctly");
Ok(())
}
#[tokio::test]
#[serial]
async fn test_write_through_cache_invalidation() -> Result<()> {
init_test_persistence().await?;
println!("Testing write-through cache invalidation");
let document_v1 = Document {
id: "cache-invalidation-test".to_string(),
title: "Version 1".to_string(),
body: "Initial content".to_string(),
..Default::default()
};
document_v1.save().await?;
let mut loaded = Document {
id: "cache-invalidation-test".to_string(),
..Default::default()
};
loaded = loaded.load().await?;
assert_eq!(loaded.title, "Version 1");
let document_v2 = Document {
id: "cache-invalidation-test".to_string(),
title: "Version 2".to_string(),
body: "Updated content".to_string(),
..Default::default()
};
document_v2.save().await?;
let mut loaded_v2 = Document {
id: "cache-invalidation-test".to_string(),
..Default::default()
};
loaded_v2 = loaded_v2.load().await?;
assert_eq!(loaded_v2.title, "Version 2");
assert_eq!(loaded_v2.body, "Updated content");
println!(" v1 saved and loaded: OK");
println!(" v2 saved (write-through): OK");
println!(" v2 loaded from cache: OK");
println!(" Cache invalidation via write-through works correctly");
Ok(())
}
#[tokio::test]
#[serial]
async fn test_all_persistable_types_cached() -> Result<()> {
init_test_persistence().await?;
println!("Testing all Persistable types can be cached");
let document = Document {
id: "persistable-type-doc".to_string(),
title: "Test Document".to_string(),
body: "Document body".to_string(),
..Default::default()
};
document.save().await?;
let mut loaded_doc = Document {
id: "persistable-type-doc".to_string(),
..Default::default()
};
loaded_doc = loaded_doc.load().await?;
assert_eq!(loaded_doc.title, "Test Document");
println!(" Document: OK");
let mut thesaurus = Thesaurus::new("Persistable Test".to_string());
let term = NormalizedTerm::new(1, NormalizedTermValue::from("test".to_string()));
thesaurus.insert(NormalizedTermValue::from("key".to_string()), term);
thesaurus.save().await?;
let mut loaded_thesaurus = Thesaurus::new("Persistable Test".to_string());
loaded_thesaurus = loaded_thesaurus.load().await?;
assert_eq!(loaded_thesaurus.name(), "Persistable Test");
assert_eq!(loaded_thesaurus.len(), 1);
println!(" Thesaurus: OK");
println!(" All Persistable types can be cached");
Ok(())
}
#[tokio::test]
#[serial]
async fn test_same_operator_skip_behavior() -> Result<()> {
init_test_persistence().await?;
println!("Testing same-operator skip behavior");
let document = Document {
id: "same-op-skip-test".to_string(),
title: "Single Backend Test".to_string(),
body: "Testing with single backend".to_string(),
..Default::default()
};
document.save().await?;
let mut loaded = Document {
id: "same-op-skip-test".to_string(),
..Default::default()
};
loaded = loaded.load().await?;
assert_eq!(loaded.title, "Single Backend Test");
println!(" Single backend save/load: OK");
println!(" Same-operator skip (ptr equality) works correctly");
Ok(())
}
#[tokio::test]
#[serial]
async fn test_cache_warmup_summary() -> Result<()> {
init_test_persistence().await?;
println!("\n========================================");
println!("Cache Warm-up Integration Test Summary");
println!("========================================");
println!();
println!("Features tested:");
println!(" [x] Compression integration with persistence");
println!(" [x] Small data persistence (no compression)");
println!(" [x] Save/load roundtrip integrity");
println!(" [x] Concurrent document operations");
println!(" [x] Decompression during load");
println!(" [x] Schema evolution recovery simulation");
println!(" [x] Load performance (non-blocking cache writeback)");
println!(" [x] Tracing spans in load path");
println!(" [x] Concurrent duplicate writes (last-write-wins)");
println!(" [x] Write-through cache invalidation");
println!(" [x] All Persistable types cached");
println!(" [x] Same-operator skip behavior");
println!();
println!("Note: Full multi-profile cache write-back testing");
println!("requires a multi-backend configuration. See:");
println!(" - .docs/design-persistence-memory-warmup.md");
println!(" - Manual testing with memory + sqlite profiles");
println!();
Ok(())
}