#![cfg(unix)]
use std::path::PathBuf;
use std::sync::Arc;
use kglite::api::io::{load_file, save_graph};
use kglite::api::mutation::{add_nodes, ColumnData, ColumnType, DataFrame};
use kglite::api::storage::{new_dir_graph_in_mode, StorageMode};
use kglite::api::{DirGraph, GraphRead};
fn doc_graph(rows: usize, body_len: usize) -> Arc<DirGraph> {
let mut graph = new_dir_graph_in_mode(StorageMode::Memory, None).expect("create graph");
let body = "x".repeat(body_len);
let mut df = DataFrame::new(Vec::new());
df.add_column(
"id".to_string(),
ColumnType::String,
ColumnData::String((0..rows).map(|i| Some(format!("n{i}"))).collect()),
)
.expect("id column");
df.add_column(
"body".to_string(),
ColumnType::String,
ColumnData::String((0..rows).map(|_| Some(body.clone())).collect()),
)
.expect("body column");
add_nodes(
&mut graph,
df,
"Doc".to_string(),
"id".to_string(),
None,
None,
)
.expect("add nodes");
Arc::new(graph)
}
fn save_to(graph: Arc<DirGraph>, path: &std::path::Path) {
let mut graph = graph;
save_graph(&mut graph, path.to_str().expect("utf-8 path")).expect("save");
}
fn our_spill_dirs() -> Vec<PathBuf> {
let mine = format!("kglite_portable_{}_", std::process::id());
let Ok(entries) = std::fs::read_dir(std::env::temp_dir()) else {
return Vec::new();
};
entries
.filter_map(Result::ok)
.map(|e| e.path())
.filter(|p| {
p.file_name()
.and_then(|n| n.to_str())
.is_some_and(|n| n.starts_with(&mine))
})
.collect()
}
#[test]
fn only_a_load_that_spills_touches_the_spill_root() {
let root = tempfile::tempdir().expect("tmpdir");
let small = root.path().join("small.kgl");
save_to(doc_graph(20, 16), &small);
assert!(
std::fs::metadata(&small).expect("small.kgl").len() < 256 * 1024,
"the small fixture must be smaller than one spill threshold"
);
let before = our_spill_dirs();
let loaded = load_file(small.to_str().unwrap()).expect("load small");
assert_eq!(loaded.graph.node_count(), 20);
let created: Vec<PathBuf> = our_spill_dirs()
.into_iter()
.filter(|p| !before.contains(p))
.collect();
assert!(
created.is_empty(),
"a load with nothing to spill still wrote to the spill root: {created:?}"
);
drop(loaded);
let big = root.path().join("big.kgl");
save_to(doc_graph(4_000, 128), &big);
let before = our_spill_dirs();
let loaded = load_file(big.to_str().unwrap()).expect("load big");
assert_eq!(loaded.graph.node_count(), 4_000);
let created: Vec<PathBuf> = our_spill_dirs()
.into_iter()
.filter(|p| !before.contains(p))
.collect();
assert_eq!(
created.len(),
1,
"a spilling load must mint exactly one directory: {created:?}"
);
assert!(
std::fs::read_dir(&created[0])
.expect("read spill dir")
.next()
.is_some(),
"the spill directory it minted is empty"
);
drop(loaded);
assert!(
!created[0].exists(),
"the spill directory outlived the graph that owned it"
);
}
#[test]
fn concurrent_loads_of_one_file_all_succeed() {
let root = tempfile::tempdir().expect("tmpdir");
let path = root.path().join("fixture.kgl");
save_to(doc_graph(50, 24), &path);
let path = path.to_str().expect("utf-8 path").to_string();
std::thread::scope(|scope| {
for _ in 0..16 {
scope.spawn(|| {
for _ in 0..25 {
let graph = load_file(&path).expect("concurrent load");
assert_eq!(graph.graph.node_count(), 50);
}
});
}
});
}