Files
Loki/rust/loki-rs/tests/engine.rs
T
Claude cbd0d5e356 Tests : scripts d'essai écrits par un sh à part (plus d'ETXTBSY)
tests/engine.rs (redemarre) a échoué en CI sur « Text file busy » : un
test écrivait son faux binaire pendant qu'un test voisin, dans un autre
thread du même process, lançait un enfant — qui héritait du descripteur
d'écriture le temps de son exec, et l'exec du script échouait. Les faux
binaires (moteur, serveur de dictée) sont désormais écrits par un sh à
part : le process des tests ne tient jamais le fichier ouvert en
écriture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
2026-10-15 16:50:25 +00:00

125 lines
3.9 KiB
Rust

//! Supervision du moteur (lot 3) — Linux seulement : /proc, setsid, signaux.
#![cfg(target_os = "linux")]
use loki_rs::engine::proctree::linux::pid_alive;
use loki_rs::engine::{self, Opts, Paths};
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::time::Duration;
/// Faux binaire Go : `serve` lance un « llama-server » dans sa session et un
/// « moteur Strata » qui prend SA PROPRE session (comme le vrai), puis attend.
///
/// Écrit par un `sh` à part : si ce process tenait lui-même le fichier ouvert
/// en écriture, un `fork` lancé au même moment par un test voisin (les tests
/// tournent en parallèle) en hériterait, et l'`exec` du script échouerait sur
/// ETXTBSY (« Text file busy »).
fn faux_go(dir: &Path, corps: &str) -> PathBuf {
let p = dir.join("faux-loki");
let ok = std::process::Command::new("sh")
.args(["-c", r#"printf '%s' "$1" > "$2" && chmod 755 "$2""#, "sh"])
.arg(format!("#!/bin/sh\n{corps}\n"))
.arg(&p)
.status()
.unwrap()
.success();
assert!(ok, "faux binaire non écrit");
assert!(std::fs::metadata(&p).unwrap().permissions().mode() & 0o111 != 0);
p
}
fn opts(go: PathBuf) -> Opts {
Opts {
go_bin: go,
check_delay: Duration::from_millis(500),
grace: Duration::from_secs(2),
settle: Duration::from_secs(5),
}
}
fn paths(home: &Path) -> Paths {
Paths {
home: home.to_path_buf(),
service: "loki-engine".into(),
}
}
fn lire_pid(p: &Path) -> i32 {
std::fs::read_to_string(p).unwrap().trim().parse().unwrap()
}
#[test]
fn demarre_puis_arrete_tout_l_arbre() {
let t = tempfile::tempdir().unwrap();
let home = t.path();
let go = faux_go(
home,
r#"[ "$1" = serve ] || exit 9
echo "serve lancé dans $(pwd)"
sleep 300 &
echo $! > llama.pid
setsid sleep 300 &
echo $! > strata.pid
wait"#,
);
let (p, o) = (paths(home), opts(go));
engine::run("start", &p, &o).unwrap();
let chef = lire_pid(&p.pid());
assert!(pid_alive(chef));
std::thread::sleep(Duration::from_millis(300));
let llama = lire_pid(&home.join("llama.pid"));
let strata = lire_pid(&home.join("strata.pid"));
assert!(pid_alive(llama) && pid_alive(strata));
let log = std::fs::read_to_string(p.log()).unwrap();
assert!(log.contains("serve lancé"), "journal : {log}");
// Déjà démarré : rien de relancé.
engine::run("start", &p, &o).unwrap();
assert_eq!(lire_pid(&p.pid()), chef);
engine::run("stop", &p, &o).unwrap();
assert!(!p.pid().exists());
for (nom, pid) in [("chef", chef), ("llama", llama), ("strata", strata)] {
assert!(!pid_alive(pid), "{nom} (PID {pid}) survit à l'arrêt");
}
// Rien à arrêter : pas d'erreur.
engine::run("stop", &p, &o).unwrap();
}
#[test]
fn redemarre() {
let t = tempfile::tempdir().unwrap();
let go = faux_go(t.path(), "exec sleep 300");
let (p, o) = (paths(t.path()), opts(go));
engine::run("start", &p, &o).unwrap();
let avant = lire_pid(&p.pid());
engine::run("restart", &p, &o).unwrap();
let apres = lire_pid(&p.pid());
assert_ne!(avant, apres);
assert!(!pid_alive(avant) && pid_alive(apres));
engine::run("stop", &p, &o).unwrap();
}
#[test]
fn echec_au_demarrage() {
let t = tempfile::tempdir().unwrap();
let go = faux_go(t.path(), "echo 'modèle introuvable'; exit 1");
let (p, o) = (paths(t.path()), opts(go));
let err = engine::run("start", &p, &o).unwrap_err();
assert_eq!(err, "service loki-engine non démarré");
assert!(!p.pid().exists(), "PID laissé après un échec");
assert!(
std::fs::read_to_string(p.log())
.unwrap()
.contains("modèle introuvable")
);
}
#[test]
fn action_inconnue() {
let t = tempfile::tempdir().unwrap();
let (p, o) = (paths(t.path()), opts(PathBuf::from("/bin/true")));
assert!(engine::run("enable", &p, &o).is_err());
}