#[cfg(test)]
mod tests {
use assert_cmd::Command as AssertCommand;
use sqlitegraph::backend::native::{EdgeStore, GraphFile, NodeStore};
use sqlitegraph::{BackendDirection, GraphConfig, NeighborQuery, SnapshotId, open_graph};
use std::env;
use std::fs;
use std::path::Path;
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
use tempfile::TempDir;
#[derive(Debug, PartialEq)]
enum CrashTestResult {
Pass,
FailSafe,
Corruption,
}
fn should_run_crash_tests() -> bool {
env::var("RUST_TEST_CRASH").is_ok() || env::var("CRASH_TESTS").is_ok()
}
#[derive(Debug)]
struct CrashTestConfig {
node_count: usize,
edges_before_crash: usize,
multi_edge_factor: usize,
child_timeout_secs: u64,
wait_for_progress_secs: u64,
}
impl Default for CrashTestConfig {
fn default() -> Self {
Self {
node_count: 10_000,
edges_before_crash: 50_000,
multi_edge_factor: 1,
child_timeout_secs: 30,
wait_for_progress_secs: 10,
}
}
}
fn run_crash_simulation_test(config: &CrashTestConfig) -> (CrashTestResult, String) {
let temp_dir = TempDir::new().expect("Failed to create temp directory");
let db_path = temp_dir.path().join("v2_crash_test.db");
println!("Starting V2 crash simulation test...");
println!(" DB path: {}", db_path.display());
println!(" Config: {:?}", config);
println!("Building crash test child binary...");
let build_result = Command::new("cargo")
.args(["build", "--example", "crash_test_child"])
.current_dir(temp_dir.path())
.output();
match build_result {
Ok(output) => {
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return (
CrashTestResult::Corruption,
format!("Failed to build child binary: {}", stderr),
);
}
println!("✅ Child binary built successfully");
}
Err(e) => {
return (
CrashTestResult::Corruption,
format!("Failed to execute cargo build: {}", e),
);
}
}
let child_binary_path = temp_dir
.path()
.join("target/debug/examples/crash_test_child");
if !child_binary_path.exists() {
return (
CrashTestResult::Corruption,
"Child binary not found after build".to_string(),
);
}
println!("Launching child process...");
let mut child = Command::new(&child_binary_path)
.arg(db_path.to_str().unwrap())
.arg(config.node_count.to_string())
.arg("10000") .arg(config.multi_edge_factor.to_string())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("Failed to spawn child process");
println!("Waiting for child process to make progress...");
let start_time = Instant::now();
let mut child_output_lines = Vec::new();
let mut edges_written = 0;
if let Some(stdout) = child.stdout.as_mut() {
use std::io::{BufRead, BufReader};
let mut reader = BufReader::new(stdout);
let mut line = String::new();
while start_time.elapsed().as_secs() < config.wait_for_progress_secs {
line.clear();
match reader.read_line(&mut line) {
Ok(0) => break, Ok(_) => {
child_output_lines.push(line.clone());
if line.contains("PROGRESS:") {
println!("Child: {}", line.trim());
if let Some(edge_str) = line.split_whitespace().nth(1) {
if let Ok(parsed_edges) = edge_str.parse::<usize>() {
edges_written = parsed_edges;
if edges_written >= config.edges_before_crash {
break;
}
}
}
}
}
Err(_) => break,
}
}
}
if edges_written < config.edges_before_crash {
println!("⚠️ Child didn't reach target edge count before timeout, proceeding anyway");
}
println!("Sending SIGKILL to child process...");
match child.kill() {
Ok(_) => println!("✅ SIGKILL sent successfully"),
Err(e) => println!("⚠️ Failed to send SIGKILL: {}", e),
}
match child.wait() {
Ok(status) => {
if !status.success() {
println!("Child terminated with status: {}", status);
}
}
Err(e) => println!("Failed to wait for child: {}", e),
}
std::thread::sleep(Duration::from_millis(100));
println!("Running post-crash validation...");
let (result, details) = validate_crashed_file(&db_path, &child_output_lines);
(result, details)
}
fn validate_crashed_file(db_path: &Path, child_output: &[String]) -> (CrashTestResult, String) {
let mut details = Vec::new();
let file_exists = db_path.exists();
let file_size = if file_exists {
fs::metadata(db_path).map(|m| m.len()).unwrap_or(0)
} else {
0
};
details.push(format!("File exists: {}", file_exists));
details.push(format!("File size: {} bytes", file_size));
if !file_exists {
return (
CrashTestResult::FailSafe,
"File was deleted after crash - this is acceptable behavior".to_string(),
);
}
if file_size < 4096 {
return (
CrashTestResult::FailSafe,
format!(
"File size {} bytes indicates empty or header-only file (acceptable)",
file_size
),
);
}
match open_graph(db_path, &GraphConfig::native()) {
Ok(graph) => {
details.push("Graph reopened successfully".to_string());
match test_basic_graph_functionality(&graph) {
Ok(_) => {
details.push("Basic functionality tests passed".to_string());
(CrashTestResult::Pass, details.join("; "))
}
Err(e) => {
details.push(format!("Basic functionality test failed: {}", e));
(CrashTestResult::FailSafe, details.join("; "))
}
}
}
Err(e) => {
details.push(format!("Failed to reopen graph: {}", e));
match validate_low_level_file(db_path) {
Ok(_) => {
details.push("Low-level file validation passed".to_string());
(CrashTestResult::FailSafe, details.join("; "))
}
Err(e) => {
details.push(format!("Low-level validation failed: {}", e));
(CrashTestResult::Corruption, details.join("; "))
}
}
}
}
}
fn test_basic_graph_functionality(
graph: &Box<dyn sqlitegraph::GraphBackend>,
) -> Result<(), String> {
let test_node_id = 1;
match graph.get_node(SnapshotId::current(), test_node_id) {
Ok(_) => {
match graph.neighbors(
SnapshotId::current(),
test_node_id,
NeighborQuery {
direction: BackendDirection::Outgoing,
edge_type: None,
},
) {
Ok(_neighbors) => Ok(()),
Err(e) => Err(format!("Neighbor query failed: {}", e)),
}
}
Err(e) => {
let mut found_node = false;
for node_id in 2..=100 {
if graph.get_node(SnapshotId::current(), node_id).is_ok() {
found_node = true;
break;
}
}
if found_node {
Ok(())
} else {
Err(format!("No nodes found in range 2-100: {}", e))
}
}
}
}
fn validate_low_level_file(db_path: &Path) -> Result<(), String> {
let mut graph_file =
GraphFile::open(db_path).map_err(|e| format!("Failed to open graph file: {}", e))?;
let header = graph_file.header();
if header.magic != [b'S', b'Q', b'L', b'T', b'G', b'F', 0, 0] {
return Err("Invalid magic bytes - file corrupted".to_string());
}
if header.node_count < 0 {
return Err("Invalid node count in header".to_string());
}
if header.edge_count < 0 {
return Err("Invalid edge count in header".to_string());
}
let file_size = fs::metadata(db_path)
.map_err(|e| format!("Failed to get file metadata: {}", e))?
.len();
let min_expected_size = 1024; if file_size < min_expected_size && (header.node_count > 0 || header.edge_count > 0) {
return Err(format!(
"File size {} too small for reported {} nodes and {} edges",
file_size, header.node_count, header.edge_count
));
}
Ok(())
}
#[test]
#[ignore] fn v2_crash_simulation_test() {
if !should_run_crash_tests() {
println!("Skipping V2 crash simulation test (set RUST_TEST_CRASH=1 to enable)");
return;
}
println!("Starting V2 crash simulation test...");
let config = CrashTestConfig::default();
let (result, details) = run_crash_simulation_test(&config);
println!("✅ Crash simulation test completed");
println!("Result: {:?}", result);
println!("Details: {}", details);
match result {
CrashTestResult::Pass => {
println!("🎉 PASS: Graph file survived SIGKILL with full functionality preserved");
}
CrashTestResult::FailSafe => {
println!("✅ PASS: Graph file shows expected safe failure behavior after crash");
}
CrashTestResult::Corruption => {
panic!("❌ FAIL: File corruption detected after crash: {}", details);
}
}
}
#[test]
#[ignore] fn v2_crash_simulation_multi_edge_test() {
if !should_run_crash_tests() {
println!(
"Skipping V2 crash simulation multi-edge test (set RUST_TEST_CRASH=1 to enable)"
);
return;
}
println!("Starting V2 crash simulation test with multi-edge scenario...");
let config = CrashTestConfig {
multi_edge_factor: 5, ..Default::default()
};
let (result, details) = run_crash_simulation_test(&config);
println!("✅ Multi-edge crash simulation test completed");
println!("Result: {:?}", result);
println!("Details: {}", details);
match result {
CrashTestResult::Pass => {
println!(
"🎉 PASS: Multi-edge graph file survived SIGKILL with full functionality preserved"
);
}
CrashTestResult::FailSafe => {
println!(
"✅ PASS: Multi-edge graph file shows expected safe failure behavior after crash"
);
}
CrashTestResult::Corruption => {
panic!(
"❌ FAIL: Multi-edge file corruption detected after crash: {}",
details
);
}
}
}
}