use std::path::{Path, PathBuf};
use std::process::{Command, Output};
fn sandbox(tag: &str) -> (tempfile::TempDir, PathBuf) {
let dir = tempfile::Builder::new()
.prefix(&format!("semlith-schedule-{tag}-"))
.tempdir()
.expect("a temporary directory");
let home = dir.path().join("home");
std::fs::create_dir_all(&home).unwrap();
(dir, home)
}
fn schedule(home: &Path, args: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_semlith"))
.env("SEMLITH_HOME", home)
.env("HOME", home)
.env_remove("SEMLITH_STORE")
.arg("schedule")
.args(args)
.output()
.expect("semlith schedule runs")
}
fn text(out: &Output) -> String {
format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
)
}
#[test]
fn a_home_with_no_schedules_file_says_so_rather_than_failing() {
let (_dir, home) = sandbox("empty");
let out = schedule(&home, &["list"]);
assert!(
out.status.success(),
"listing nothing is not an error: {}",
text(&out)
);
assert!(
text(&out).contains("no schedules"),
"an empty list should say so and name the command that writes one, got: {}",
text(&out)
);
assert!(
!home.join("schedules.json").exists(),
"listing created a schedules file"
);
}
#[test]
fn the_four_verbs_reach_the_one_file_the_daemon_owns() {
let (dir, home) = sandbox("verbs");
let out = dir.path().join("out");
std::fs::create_dir_all(&out).unwrap();
let added = schedule(
&home,
&[
"add",
"change",
"--every",
"3600",
"--to",
out.to_str().unwrap(),
"--format",
"csv",
],
);
assert!(added.status.success(), "{}", text(&added));
let file = semlith::schedule::Schedules::load_from(&home.join("schedules.json"))
.expect("the schedules file parses");
assert_eq!(file.schedules.len(), 1);
let (id, held) = file.schedules.iter().next().unwrap();
assert_eq!(held.kind, "change");
assert_eq!(held.format, "csv");
assert_eq!(held.every_seconds, 3600);
assert!(held.enabled);
let listed = schedule(&home, &["list"]);
assert!(listed.status.success(), "{}", text(&listed));
let body = text(&listed);
assert!(
body.contains(id),
"the list does not name the id it just wrote"
);
assert!(
body.contains(out.to_str().unwrap()),
"the list does not print the destination in full, got: {body}"
);
assert!(
body.contains("every 1 hour"),
"3600 seconds should read as an hour, got: {body}"
);
let off = schedule(&home, &["set", id, "off"]);
assert!(off.status.success(), "{}", text(&off));
let file = semlith::schedule::Schedules::load_from(&home.join("schedules.json")).unwrap();
assert!(
!file.schedules[id].enabled,
"the record still says enabled after `set off`"
);
let removed = schedule(&home, &["remove", id]);
assert!(removed.status.success(), "{}", text(&removed));
let file = semlith::schedule::Schedules::load_from(&home.join("schedules.json")).unwrap();
assert!(
file.schedules.is_empty(),
"the schedule survived its removal"
);
}
#[test]
fn a_destination_the_daemon_could_not_find_is_refused_at_the_terminal() {
let (dir, home) = sandbox("relative");
let out = dir.path().join("out");
std::fs::create_dir_all(&out).unwrap();
let added = Command::new(env!("CARGO_BIN_EXE_semlith"))
.env("SEMLITH_HOME", &home)
.env("HOME", &home)
.current_dir(dir.path())
.args([
"schedule", "add", "change", "--every", "3600", "--to", "out",
])
.output()
.expect("semlith schedule runs");
assert!(added.status.success(), "{}", text(&added));
let file = semlith::schedule::Schedules::load_from(&home.join("schedules.json")).unwrap();
let held = file.schedules.values().next().unwrap();
assert!(
held.dir.is_absolute(),
"a relative --to reached the record as {:?}",
held.dir
);
let missing = schedule(
&home,
&[
"add",
"change",
"--every",
"3600",
"--to",
dir.path().join("nowhere").to_str().unwrap(),
],
);
assert!(
!missing.status.success(),
"a missing destination was accepted"
);
let why = text(&missing);
assert!(
why.contains("nowhere"),
"the refusal does not name the directory, got: {why}"
);
}
#[test]
fn a_cadence_below_the_floor_and_an_unknown_id_each_say_what_would_have_worked() {
let (dir, home) = sandbox("refusals");
let out = dir.path().join("out");
std::fs::create_dir_all(&out).unwrap();
let quick = schedule(
&home,
&[
"add",
"change",
"--every",
"5",
"--to",
out.to_str().unwrap(),
],
);
assert!(
!quick.status.success(),
"a five-second cadence was accepted"
);
let why = text(&quick);
assert!(
why.contains(&semlith::schedule::MIN_EVERY.to_string()),
"the refusal does not name the floor, got: {why}"
);
for verb in [vec!["remove", "s9"], vec!["set", "s9", "off"]] {
let out = schedule(&home, &verb);
assert!(!out.status.success(), "{verb:?} on an unknown id succeeded");
assert!(
text(&out).contains("semlith schedule list"),
"{verb:?} on an unknown id does not say how to find the ids, got: {}",
text(&out)
);
}
}
#[test]
fn a_pdf_to_a_terminal_is_refused_before_a_store_is_opened() {
let (_dir, home) = sandbox("pdf");
let out = Command::new(env!("CARGO_BIN_EXE_semlith"))
.env("SEMLITH_HOME", &home)
.env("HOME", &home)
.env_remove("SEMLITH_STORE")
.args(["report", "change", "--format", "pdf"])
.output()
.expect("semlith report runs");
assert!(!out.status.success());
assert!(
text(&out).contains("--out"),
"the refusal does not name the flag that would have worked, got: {}",
text(&out)
);
}