use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::sync::Arc;
use kglite::api::io::{load_file, load_file_with, save_graph, LoadOptions};
use kglite::api::mutation::{add_nodes, ColumnData, ColumnType, DataFrame};
use kglite::api::{DirGraph, GraphRead};
const CASE_VAR: &str = "KGLITE_TEST_CEILING_CASE";
const FIXTURE_VAR: &str = "KGLITE_TEST_CEILING_FIXTURE";
const MAX_LOAD_VAR: &str = "KGLITE_MAX_LOAD_MB";
fn write_fixture(path: &Path) {
let mut graph = DirGraph::new();
let rows = 2_000;
let body = "x".repeat(64);
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");
let mut arc = Arc::new(graph);
save_graph(&mut arc, path.to_str().unwrap()).expect("save fixture");
}
fn run_child(case: &str, max_load: Option<&str>) -> (Output, PathBuf) {
static NEXT: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
let dir = std::env::temp_dir().join(format!(
"kglite_ceiling_env_{}_{case}_{}",
std::process::id(),
NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
));
std::fs::create_dir_all(&dir).expect("scenario dir");
let fixture = dir.join("graph.kgl");
write_fixture(&fixture);
let mut command = Command::new(std::env::current_exe().expect("current exe"));
command
.args(["--exact", "child_scenario", "--ignored", "--nocapture"])
.env(CASE_VAR, case)
.env(FIXTURE_VAR, &fixture);
match max_load {
Some(value) => command.env(MAX_LOAD_VAR, value),
None => command.env_remove(MAX_LOAD_VAR),
};
let output = command.output().expect("spawn child");
(output, dir)
}
fn stdout_of(output: &Output) -> String {
String::from_utf8_lossy(&output.stdout).into_owned()
}
fn stderr_of(output: &Output) -> String {
String::from_utf8_lossy(&output.stderr).into_owned()
}
#[test]
fn without_the_variable_there_is_no_ceiling() {
let (output, dir) = run_child("plain_load", None);
let stdout = stdout_of(&output);
let _ = std::fs::remove_dir_all(&dir);
assert!(stdout.contains("RESULT: loaded 2000"), "{stdout}");
}
#[test]
fn the_variable_alone_refuses_a_plain_load() {
let (output, dir) = run_child("plain_load", Some("0"));
let stdout = stdout_of(&output);
let _ = std::fs::remove_dir_all(&dir);
assert!(stdout.contains("RESULT: refused OutOfMemory"), "{stdout}");
assert!(stdout.contains("estimated to peak at"), "{stdout}");
assert!(stdout.contains(MAX_LOAD_VAR), "{stdout}");
}
#[test]
fn a_ceiling_above_the_estimate_loads() {
let (output, dir) = run_child("plain_load", Some("4096"));
let stdout = stdout_of(&output);
let _ = std::fs::remove_dir_all(&dir);
assert!(stdout.contains("RESULT: loaded 2000"), "{stdout}");
}
#[test]
fn an_explicit_option_lifts_the_variables_ceiling() {
let (output, dir) = run_child("option_lifts_ceiling", Some("0"));
let stdout = stdout_of(&output);
let _ = std::fs::remove_dir_all(&dir);
assert!(stdout.contains("RESULT: loaded 2000"), "{stdout}");
}
#[test]
fn an_explicit_option_imposes_a_ceiling_without_the_variable() {
let (output, dir) = run_child("option_imposes_ceiling", None);
let stdout = stdout_of(&output);
let _ = std::fs::remove_dir_all(&dir);
assert!(stdout.contains("RESULT: refused OutOfMemory"), "{stdout}");
}
#[test]
fn an_unparseable_value_warns_loudly_and_lifts_the_ceiling() {
let (output, dir) = run_child("plain_load", Some("1O24"));
let stdout = stdout_of(&output);
let stderr = stderr_of(&output);
let _ = std::fs::remove_dir_all(&dir);
assert!(stdout.contains("RESULT: loaded 2000"), "{stdout}");
assert!(
stderr.contains(MAX_LOAD_VAR) && stderr.contains("1O24"),
"the warning must name the variable and the value it could not read:\n{stderr}"
);
assert!(
stderr.contains("NO memory ceiling"),
"the warning must say what the operator lost:\n{stderr}"
);
}
#[test]
#[ignore]
fn child_scenario() {
let case = std::env::var(CASE_VAR).expect("child needs a case");
let fixture = std::env::var(FIXTURE_VAR).expect("child needs a fixture");
let loaded = match case.as_str() {
"plain_load" => load_file(&fixture),
"option_lifts_ceiling" => {
load_file_with(&fixture, &LoadOptions::new().with_max_load_bytes(None))
}
"option_imposes_ceiling" => {
load_file_with(&fixture, &LoadOptions::new().with_max_load_bytes(Some(1)))
}
other => panic!("unknown case {other}"),
};
match loaded {
Ok(graph) => println!("RESULT: loaded {}", graph.graph.node_count()),
Err(error) => println!("RESULT: refused {:?} — {error}", error.kind()),
}
}