#![cfg(all(feature = "embedded", feature = "ipc", unix))]
use std::io::{BufRead, BufReader};
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::sync::mpsc;
use std::time::Duration;
const PATIENCE: Duration = Duration::from_secs(90);
fn scratch(case: &str, lines: &[&str]) -> PathBuf {
let dir = std::env::temp_dir().join(format!("iki-np-{}-{case}", std::process::id()));
let config = dir.join("config").join("ikigai");
std::fs::create_dir_all(&config).expect("scratch config home");
let body: String = lines
.iter()
.map(|line| format!("mount = \"{line}\"\n"))
.collect();
std::fs::write(config.join("config.toml"), body).expect("scratch config.toml");
dir
}
fn serve(home: &Path, args: &[&str]) -> (String, Option<i32>) {
let sock = home.join("s.sock");
let _ = std::fs::remove_file(&sock);
let mut child: Child = Command::new(env!("CARGO_BIN_EXE_ikigai"))
.arg("serve")
.arg(&sock)
.args(args)
.env("HOME", home)
.env("XDG_CONFIG_HOME", home.join("config"))
.env("IKIGAI_FILES", home.join("files"))
.stdout(Stdio::null())
.stderr(Stdio::piped())
.spawn()
.expect("the binary under test starts");
let stderr = child.stderr.take().expect("piped stderr");
let (tx, rx) = mpsc::channel();
std::thread::spawn(move || {
let mut seen = String::new();
let mut serving = false;
for line in BufReader::new(stderr).lines().map_while(Result::ok) {
serving = line.contains("serving on");
seen.push_str(&line);
seen.push('\n');
if serving {
break;
}
}
let _ = tx.send((seen, serving));
});
let (seen, serving) = rx.recv_timeout(PATIENCE).unwrap_or_else(|e| {
let _ = child.kill();
panic!("`serve {args:?}` said nothing in {PATIENCE:?}: {e}")
});
let status = if serving {
let _ = child.kill();
let _ = child.wait();
None
} else {
child.wait().ok().and_then(|s| s.code())
};
(seen, status)
}
#[test]
fn no_flags_compose_the_config_homes_mount_lines() {
let home = scratch(
"inherit",
&[
"prefer urn:iki:store:=/tmp/iki-np-absent-a.sock",
"prefer urn:iki:ledger:=/tmp/iki-np-absent-b.sock",
],
);
let (seen, _) = serve(&home, &[]);
assert!(
seen.contains("mount prefer urn:iki:store: -> /tmp/iki-np-absent-a.sock"),
"the first config line must be named on the banner: {seen}"
);
assert!(
seen.contains("mount prefer urn:iki:ledger: -> /tmp/iki-np-absent-b.sock"),
"the second config line must be named on the banner: {seen}"
);
}
#[test]
fn declining_composes_nothing_and_the_banner_says_so() {
let home = scratch(
"decline",
&[
"prefer urn:iki:store:=/tmp/iki-np-absent-a.sock",
"prefer urn:iki:ledger:=/tmp/iki-np-absent-b.sock",
],
);
let (seen, _) = serve(&home, &["--no-config-mounts"]);
assert!(
seen.contains("declined (--no-config-mounts)"),
"the banner must name the declined posture: {seen}"
);
assert!(
!seen.contains("urn:iki:store:") && !seen.contains("urn:iki:ledger:"),
"the config home's mounts were declined and must not be named: {seen}"
);
assert!(
!seen.contains("none composed"),
"a decline is not an empty config home, and must not read like one: {seen}"
);
}
#[test]
fn declining_beside_a_mount_flag_refuses_to_start() {
let home = scratch(
"conflict",
&["prefer urn:iki:store:=/tmp/iki-np-absent-a.sock"],
);
let (seen, status) = serve(
&home,
&["--no-config-mounts", "--prefer", "urn:x:=/tmp/x.sock"],
);
assert_eq!(status, Some(2), "a usage refusal exits 2: {seen}");
assert!(
seen.contains("--no-config-mounts") && seen.contains("--prefer"),
"the refusal must name both flags: {seen}"
);
assert!(
!seen.contains("serving on"),
"it must not serve at all: {seen}"
);
}
#[test]
fn a_mount_flag_still_wins_wholesale_over_the_config_home() {
let home = scratch(
"wholesale",
&[
"prefer urn:iki:store:=/tmp/iki-np-absent-a.sock",
"prefer urn:iki:ledger:=/tmp/iki-np-absent-b.sock",
],
);
let (seen, _) = serve(&home, &["--prefer", "urn:x:=/tmp/iki-np-absent-c.sock"]);
assert!(
seen.contains("mount prefer urn:x: -> /tmp/iki-np-absent-c.sock"),
"the flag's mount must be named: {seen}"
);
assert!(
!seen.contains("urn:iki:store:") && !seen.contains("urn:iki:ledger:"),
"the flag is the WHOLE topology, not an addition to the file's two: {seen}"
);
}
#[test]
fn an_empty_config_home_says_it_composed_nothing() {
let home = scratch("empty", &[]);
let (seen, _) = serve(&home, &[]);
assert!(
seen.contains("none composed"),
"an empty topology is a fact worth stating: {seen}"
);
assert!(
!seen.contains("declined"),
"nothing was declined — the config home simply had no mount lines: {seen}"
);
}