use super::*;
#[cfg(unix)]
#[test]
fn reload_swaps_a_sheep_for_a_fresh_instance_under_a_new_id() {
let dir = tempfile::tempdir().unwrap();
let script = write_test_script(&dir);
let mut guard = DaemonGuard::default();
let started = shep(dir.path())
.arg("--format")
.arg("json")
.arg("start")
.arg(&script)
.output()
.unwrap();
guard.adopt_home(dir.path());
assert_success(&started);
let envelope: serde_json::Value = serde_json::from_slice(&started.stdout).unwrap();
let original_id = envelope["data"][0]["id"]
.as_u64()
.unwrap_or_else(|| panic!("a started sheep must carry an id: {envelope}"));
let reloaded = shep(dir.path())
.arg("--format")
.arg("json")
.arg("reload")
.arg("sheep")
.output()
.unwrap();
assert_success(&reloaded);
let envelope: serde_json::Value = serde_json::from_slice(&reloaded.stdout).unwrap();
assert_eq!(
envelope["command"], "reload",
"`shep reload` must reach the reload verb and no other: {envelope}"
);
assert_eq!(
envelope["data"][0]["id"], original_id,
"the answer is the flock as it stood when the reload was accepted: {envelope}"
);
let after = poll_flock(dir.path(), |info| info["id"] != original_id);
assert_ne!(
after["id"], original_id,
"the swap must finish, leaving one entry under a new id: {after}"
);
assert_eq!(after["status"], "online", "{after}");
graceful_kill(dir.path());
}
#[cfg(unix)]
#[test]
fn trigger_reaches_the_trigger_verb_and_names_the_missing_channel() {
let dir = tempfile::tempdir().unwrap();
let script = write_test_script(&dir);
let mut guard = DaemonGuard::default();
let started = shep(dir.path())
.arg("start")
.arg(&script)
.arg("--name")
.arg("sheep")
.output()
.unwrap();
guard.adopt_home(dir.path());
assert_success(&started);
let triggered = shep(dir.path())
.arg("--format")
.arg("json")
.arg("trigger")
.arg("sheep")
.arg("reload-config")
.output()
.unwrap();
assert_success(&triggered);
let envelope: serde_json::Value = serde_json::from_slice(&triggered.stdout).unwrap();
assert_eq!(
envelope["command"], "trigger",
"`shep trigger` must reach the trigger verb and no other: {envelope}"
);
assert_eq!(envelope["data"][0]["name"], "sheep", "{envelope}");
assert_eq!(
envelope["data"][0]["outcome"]["kind"], "no_channel",
"a sheep with no channel/wait_ready/shutdown_with_message must answer \
no_channel, never a reply it never opened a pipe to receive: {envelope}"
);
graceful_kill(dir.path());
}
#[cfg(unix)]
#[test]
fn signal_reaches_the_signal_verb_and_delivers() {
let dir = tempfile::tempdir().unwrap();
let script = write_test_script(&dir);
let mut guard = DaemonGuard::default();
let started = shep(dir.path())
.arg("start")
.arg(&script)
.arg("--name")
.arg("sheep")
.output()
.unwrap();
guard.adopt_home(dir.path());
assert_success(&started);
let signalled = shep(dir.path())
.arg("--format")
.arg("json")
.arg("signal")
.arg("sheep")
.arg("SIGWINCH")
.output()
.unwrap();
assert_success(&signalled);
let envelope: serde_json::Value = serde_json::from_slice(&signalled.stdout).unwrap();
assert_eq!(
envelope["command"], "signal",
"`shep signal` must reach the signal verb and no other: {envelope}"
);
assert_eq!(envelope["data"][0]["name"], "sheep", "{envelope}");
assert_eq!(
envelope["data"][0]["outcome"]["kind"], "delivered",
"a running sheep must answer delivered for a signal the kernel accepted: {envelope}"
);
graceful_kill(dir.path());
}
#[cfg(unix)]
#[test]
fn stock_reaches_the_stock_verb_and_settles_the_flock() {
let dir = tempfile::tempdir().unwrap();
let script = write_test_script(&dir);
let mut guard = DaemonGuard::default();
let started = shep(dir.path())
.arg("start")
.arg(&script)
.arg("--name")
.arg("sheep")
.output()
.unwrap();
guard.adopt_home(dir.path());
assert_success(&started);
let stocked_up = shep(dir.path())
.arg("--format")
.arg("json")
.arg("stock")
.arg("sheep")
.arg("3")
.output()
.unwrap();
assert_success(&stocked_up);
let envelope: serde_json::Value = serde_json::from_slice(&stocked_up.stdout).unwrap();
assert_eq!(
envelope["command"], "stock",
"`shep stock` must reach the stock verb and no other: {envelope}"
);
let grown = poll_flock_data(dir.path(), FLOCK_DEADLINE, |data| {
data.as_array().is_some_and(|rows| rows.len() == 3)
});
assert_eq!(
grown.as_array().unwrap().len(),
3,
"stocking up must settle at three instances: {grown}"
);
assert!(
grown
.as_array()
.unwrap()
.iter()
.all(|row| row["name"] == "sheep"),
"every instance must still belong to `sheep`: {grown}"
);
let stocked_down = shep(dir.path())
.arg("stock")
.arg("sheep")
.arg("1")
.output()
.unwrap();
assert_success(&stocked_down);
let settled = poll_flock_data(dir.path(), FLOCK_DEADLINE, |data| {
data.as_array().is_some_and(|rows| rows.len() == 1)
});
assert_eq!(
settled.as_array().unwrap().len(),
1,
"stocking down must settle back to one instance: {settled}"
);
graceful_kill(dir.path());
}
#[cfg(unix)]
#[test]
fn scale_alias_reaches_stock_against_a_real_daemon() {
let dir = tempfile::tempdir().unwrap();
let script = write_test_script(&dir);
let mut guard = DaemonGuard::default();
let started = shep(dir.path())
.arg("start")
.arg(&script)
.arg("--name")
.arg("sheep")
.output()
.unwrap();
guard.adopt_home(dir.path());
assert_success(&started);
let scaled = shep(dir.path())
.arg("--format")
.arg("json")
.arg("scale")
.arg("sheep")
.arg("2")
.output()
.unwrap();
assert_success(&scaled);
let envelope: serde_json::Value = serde_json::from_slice(&scaled.stdout).unwrap();
assert_eq!(
envelope["command"], "stock",
"`shep scale` is an alias for `stock`, and must reach it: {envelope}"
);
graceful_kill(dir.path());
}
#[cfg(unix)]
#[test]
fn describe_renders_a_real_sheeps_lamb_tree() {
let dir = tempfile::tempdir().unwrap();
let script = write_forking_script(&dir);
let mut guard = DaemonGuard::default();
let started = shep(dir.path())
.arg("start")
.arg(&script)
.arg("--name")
.arg("sheep")
.output()
.unwrap();
guard.adopt_home(dir.path());
assert_success(&started);
let start = Instant::now();
let described = loop {
let output = shep(dir.path())
.arg("describe")
.arg("sheep")
.output()
.unwrap();
assert_success(&output);
let text = String::from_utf8_lossy(&output.stdout).into_owned();
if text.contains("sleep") || start.elapsed() >= FLOCK_DEADLINE {
break text;
}
std::thread::sleep(FLOCK_POLL_INTERVAL);
};
assert!(described.contains("Lambs of"), "{described}");
assert!(described.contains("sleep"), "{described}");
assert!(
described.contains("not exactly the set a stop kills"),
"{described}"
);
graceful_kill(dir.path());
}
#[cfg(unix)]
#[test]
fn a_sheep_labels_one_of_its_lambs_over_its_channel() {
let dir = tempfile::tempdir().unwrap();
let home = dir.path();
let script = write_script(
&dir,
"labeller.sh",
&format!(
"#!/bin/sh\n{}sleep 300 &\n\
printf '{{\"kind\":\"lamb-label\",\"pid\":%s,\"label\":\"worker 1\"}}\\n' \"$!\" >&3\n\
sleep 300 &\nwait\n",
record_pid_line(&dir)
),
);
let flockfile = write_flockfile(
&dir,
&format!(
"[[app]]\nname = \"labeller\"\nscript = '{}'\nchannel = true\n",
script.display()
),
);
let mut guard = DaemonGuard::default();
let started = shep(home).arg("start").arg(&flockfile).output().unwrap();
guard.adopt_home(home);
assert_success(&started);
let start = Instant::now();
let lambs = loop {
let output = shep(home)
.args(["--format", "json", "describe", "labeller"])
.output()
.unwrap();
assert_success(&output);
let envelope: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let lambs = envelope["data"][0]["lambs"].clone();
let sleeps = lambs.as_array().map_or(0, |all| {
all.iter().filter(|lamb| lamb["name"] == "sleep").count()
});
let labelled = lambs
.as_array()
.is_some_and(|all| all.iter().any(|lamb| lamb["label"] == "worker 1"));
if (sleeps == 2 && labelled) || start.elapsed() >= FLOCK_DEADLINE {
break lambs;
}
std::thread::sleep(FLOCK_POLL_INTERVAL);
};
let mut rows: Vec<(String, Option<String>)> = lambs
.as_array()
.unwrap_or_else(|| panic!("describe walked no lambs: {lambs}"))
.iter()
.map(|lamb| {
(
lamb["name"].as_str().unwrap_or_default().to_string(),
lamb["label"].as_str().map(str::to_string),
)
})
.collect();
rows.sort();
assert_eq!(
rows,
vec![
("sleep".to_string(), None),
("sleep".to_string(), Some("worker 1".to_string())),
],
"{lambs}"
);
let table = shep(home).args(["describe", "labeller"]).output().unwrap();
assert_success(&table);
let table = String::from_utf8_lossy(&table.stdout);
assert!(table.contains("LABEL"), "{table}");
assert!(table.contains("worker 1"), "{table}");
graceful_kill(home);
}
#[cfg(unix)]
#[test]
fn saving_the_roll_then_mustering_reports_the_same_flock() {
let dir = tempfile::tempdir().unwrap();
let home = dir.path();
let script = write_test_script(&dir);
let mut guard = DaemonGuard::default();
let started = shep(home)
.arg("--format")
.arg("json")
.arg("start")
.arg(&script)
.arg("--name")
.arg("roundtrip")
.output()
.unwrap();
guard.adopt_home(home);
assert_success(&started);
let start_envelope: serde_json::Value = serde_json::from_slice(&started.stdout).unwrap();
assert_eq!(
start_envelope["data"][0]["status"], "online",
"{start_envelope}"
);
let original_pid = start_envelope["data"][0]["pid"]
.as_i64()
.unwrap_or_else(|| panic!("pid must be a real positive OS pid: {start_envelope}"));
let saved = shep(home)
.arg("--format")
.arg("json")
.arg("save")
.output()
.unwrap();
assert_success(&saved);
let save_envelope: serde_json::Value = serde_json::from_slice(&saved.stdout).unwrap();
assert_eq!(
save_envelope["command"], "save",
"`shep save` must reach the save verb and no other: {save_envelope}"
);
assert_eq!(
save_envelope["data"]["apps"], 1,
"the roll must record the one app started above: {save_envelope}"
);
let mustered = shep(home)
.arg("--format")
.arg("json")
.arg("muster")
.output()
.unwrap();
assert_success(&mustered);
let muster_envelope: serde_json::Value = serde_json::from_slice(&mustered.stdout).unwrap();
assert_eq!(
muster_envelope["command"], "muster",
"`shep muster` must reach the muster verb and no other: {muster_envelope}"
);
let flock = muster_envelope["data"]
.as_array()
.unwrap_or_else(|| panic!("muster data must be an array: {muster_envelope}"));
assert_eq!(
flock.len(),
1,
"muster against a daemon already running the flock the roll \
describes must not spawn a duplicate: {muster_envelope}"
);
assert_eq!(flock[0]["name"], "roundtrip", "{muster_envelope}");
assert_eq!(
flock[0]["pid"].as_i64().unwrap(),
original_pid,
"muster must leave an already-running sheep alone and report the \
SAME process, never restart it: {muster_envelope}"
);
graceful_kill(home);
}