#![cfg(unix)]
use std::os::unix::process::ExitStatusExt;
use std::path::PathBuf;
use std::process::{Child, Command, Stdio};
use std::time::Duration;
use tempfile::TempDir;
fn bin_path() -> PathBuf {
assert_cmd::cargo::cargo_bin("sqlite-graphrag")
}
fn setup_db() -> TempDir {
let tmp = TempDir::new().expect("TempDir falhou");
let status = Command::new(bin_path())
.arg("init")
.env("SQLITE_GRAPHRAG_DB_PATH", tmp.path().join("test.sqlite"))
.env("SQLITE_GRAPHRAG_CACHE_DIR", tmp.path().join("cache"))
.env("SQLITE_GRAPHRAG_LOG_LEVEL", "error")
.status()
.expect("init falhou");
assert!(status.success(), "init deve ter sucesso: {status:?}");
tmp
}
fn sqlite_graphrag_cmd(tmp: &TempDir) -> Command {
let mut cmd = Command::new(bin_path());
cmd.env("SQLITE_GRAPHRAG_DB_PATH", tmp.path().join("test.sqlite"))
.env("SQLITE_GRAPHRAG_CACHE_DIR", tmp.path().join("cache"))
.env("SQLITE_GRAPHRAG_LOG_LEVEL", "error");
cmd
}
fn send_signal(child: &Child, signal: libc::c_int) -> Result<(), i32> {
let pid = child.id() as libc::pid_t;
let ret = unsafe { libc::kill(pid, signal) };
if ret == 0 {
Ok(())
} else {
Err(unsafe { *libc::__errno_location() })
}
}
fn db_integro(tmp: &TempDir) -> bool {
let db_path = tmp.path().join("test.sqlite");
if !db_path.exists() {
return false;
}
let conn = rusqlite::Connection::open(&db_path);
match conn {
Err(_) => false,
Ok(c) => {
let resultado: String = c
.query_row("PRAGMA integrity_check", [], |row| row.get(0))
.unwrap_or_else(|_| "falhou".to_string());
resultado.trim() == "ok"
}
}
}
#[test]
fn sigint_durante_health_exit_db_integro() {
let tmp = setup_db();
let mut child: Child = sqlite_graphrag_cmd(&tmp)
.arg("health")
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("spawn de health falhou");
std::thread::sleep(Duration::from_millis(50));
match send_signal(&child, libc::SIGINT) {
Ok(()) => {}
Err(3) => {} Err(e) => panic!("kill(SIGINT) falhou com errno={e}"),
}
let status = child.wait().expect("wait falhou");
let _ = status;
assert!(
db_integro(&tmp),
"DB deve estar íntegro após SIGINT em health"
);
}
#[test]
fn sigterm_durante_init_graceful_exit_db_integro() {
let tmp = TempDir::new().expect("TempDir falhou");
let mut child: Child = sqlite_graphrag_cmd(&tmp)
.arg("init")
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("spawn de init falhou");
std::thread::sleep(Duration::from_millis(100));
match send_signal(&child, libc::SIGTERM) {
Ok(()) => {}
Err(3) => {} Err(e) => panic!("kill(SIGTERM) falhou com errno={e}"),
}
let status = child.wait().expect("wait falhou");
let encerrou_ok =
status.success() || status.signal().is_some() || status.code().is_some_and(|c| c != 0);
assert!(
encerrou_ok,
"Processo deveria ter encerrado mas wait retornou status indefinido"
);
let db_path = tmp.path().join("test.sqlite");
if db_path.exists() {
assert!(
db_integro(&tmp),
"DB criado deve estar íntegro após SIGTERM"
);
}
}
#[test]
fn sigterm_apos_remember_nao_corrompe_db() {
let tmp = setup_db();
let status = sqlite_graphrag_cmd(&tmp)
.args([
"remember",
"--name",
"memoria-signal-test",
"--type",
"project",
"--description",
"Teste de signal handling",
"--body",
"Conteudo para testar integridade apos sinal",
])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.expect("remember falhou");
assert!(
status.success(),
"remember deve ter sucesso antes do teste de sinal"
);
let mut child: Child = sqlite_graphrag_cmd(&tmp)
.args([
"remember",
"--name",
"memoria-signal-test-2",
"--type",
"project",
"--description",
"Segundo remember durante sinal",
"--body",
"Conteudo do segundo remember",
])
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("spawn de segundo remember falhou");
std::thread::sleep(Duration::from_millis(50));
match send_signal(&child, libc::SIGTERM) {
Ok(()) => {}
Err(3) => {}
Err(e) => panic!("kill(SIGTERM) falhou com errno={e}"),
}
let _ = child.wait().expect("wait falhou");
assert!(
db_integro(&tmp),
"DB deve estar íntegro após SIGTERM durante remember"
);
}
#[test]
fn sigkill_processo_nao_vira_zombie() {
let tmp = setup_db();
let mut child: Child = sqlite_graphrag_cmd(&tmp)
.arg("health")
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("spawn de health falhou");
std::thread::sleep(Duration::from_millis(30));
match send_signal(&child, libc::SIGKILL) {
Ok(()) => {}
Err(3) => {}
Err(e) => panic!("kill(SIGKILL) falhou com errno={e}"),
}
let status = child.wait().expect("wait deve retornar apos SIGKILL");
let wait_retornou =
status.success() || status.signal().is_some_and(|s| s == 9) || !status.success();
assert!(
wait_retornou,
"Processo deveria ter encerrado mas wait bloqueou ou retornou estado indefinido: {status:?}"
);
}