use forjar::core::types::{BackupSpec, MachineTarget, Resource, ResourceType};
use forjar::resources::backup_sync;
use std::collections::HashMap;
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::process::Command;
fn tmpdir(tag: &str) -> PathBuf {
let p = std::env::temp_dir().join(format!(
"forjar-bkp-{tag}-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
fs::create_dir_all(&p).unwrap();
p
}
struct Stub {
remotes: &'static str,
combined: Option<&'static str>,
}
fn install_rclone_stub(bin: &Path, stub: &Stub) {
fs::create_dir_all(bin).unwrap();
let combined = match stub.combined {
Some(c) => format!(
" # emit the caller's --combined file\n \
for a in \"$@\"; do\n \
if [ \"$prev\" = \"--combined\" ]; then printf '%b' {c:?} > \"$a\"; fi\n \
prev=\"$a\"\n done\n"
),
None => " :\n".to_string(),
};
let script = format!(
"#!/bin/sh\n\
cmd=\"$1\"; shift\n\
prev=\"\"\n\
case \"$cmd\" in\n\
\x20 listremotes) printf '%b' {remotes:?}; exit 0 ;;\n\
\x20 about|lsd) exit 0 ;;\n\
\x20 sync) exit 0 ;;\n\
\x20 check)\n{combined} exit 0 ;;\n\
esac\n\
exit 0\n",
remotes = stub.remotes,
);
let p = bin.join("rclone");
fs::write(&p, script).unwrap();
fs::set_permissions(&p, fs::Permissions::from_mode(0o755)).unwrap();
}
fn resource(root: &Path, src: &Path) -> Resource {
Resource {
resource_type: ResourceType::BackupSync,
machine: MachineTarget::Single("test".into()),
home: Some(root.to_string_lossy().into_owned()),
backup: BackupSpec {
remote: Some("gdrive:media".into()),
remote_config: HashMap::new(),
source: vec![src.to_string_lossy().into_owned()],
..Default::default()
},
..Default::default()
}
}
fn sync_body(r: &Resource) -> String {
const OPEN: &str = "<<'FORJAR_BACKUP_EOF'\n";
const CLOSE: &str = "\nFORJAR_BACKUP_EOF";
let apply = backup_sync::apply_script(r);
let s = apply.find(OPEN).expect("sync heredoc open") + OPEN.len();
let e = apply[s..].find(CLOSE).expect("sync heredoc close") + s;
apply[s..e].to_string()
}
struct Run {
code: i32,
out: String,
status: String,
}
fn run(r: &Resource, root: &Path, bin: &Path) -> Run {
let script = root.join("sync.sh");
fs::write(&script, sync_body(r)).unwrap();
fs::set_permissions(&script, fs::Permissions::from_mode(0o755)).unwrap();
let status = root.join("status.json");
let out = Command::new("/bin/sh")
.arg(&script)
.env(
"PATH",
format!(
"{}:{}",
bin.display(),
std::env::var("PATH").unwrap_or_default()
),
)
.env("FORJAR_BACKUP_STATUS", &status)
.output()
.expect("run sync");
Run {
code: out.status.code().unwrap_or(-1),
out: String::from_utf8_lossy(&out.stdout).into_owned(),
status: fs::read_to_string(&status).unwrap_or_default(),
}
}
fn setup(tag: &str, stub: &Stub) -> (PathBuf, PathBuf, PathBuf) {
let root = tmpdir(tag);
let bin = root.join("bin");
let src = root.join("media");
fs::create_dir_all(&src).unwrap();
fs::write(src.join("a.mp4"), b"aaaa").unwrap();
install_rclone_stub(&bin, stub);
(root, bin, src)
}
#[test]
fn a_fully_matched_backup_is_verified_and_exits_zero() {
let (root, bin, src) = setup(
"ok",
&Stub {
remotes: "gdrive:\n",
combined: Some("= a.mp4\n= b.mp4\n"),
},
);
let r = run(&resource(&root, &src), &root, &bin);
assert_eq!(r.code, 0, "verified backup must exit 0\n{}", r.out);
assert!(r.status.contains(r#""health":"verified""#), "{}", r.status);
assert!(r.status.contains(r#""coverage_pct":100"#), "{}", r.status);
fs::remove_dir_all(&root).ok();
}
#[test]
fn zero_matched_files_fails_even_though_rclone_exited_zero() {
let (root, bin, src) = setup(
"nomatch",
&Stub {
remotes: "gdrive:\n",
combined: Some("+ a.mp4\n+ b.mp4\n+ c.mp4\n"),
},
);
let r = run(&resource(&root, &src), &root, &bin);
assert_eq!(
r.code, 1,
"a backup holding nothing must FAIL\n{}\n{}",
r.out, r.status
);
assert!(
r.status.contains(r#""health":"unverified""#),
"{}",
r.status
);
assert!(r.status.contains(r#""missing":3"#), "{}", r.status);
fs::remove_dir_all(&root).ok();
}
#[test]
fn an_empty_verification_is_a_broken_check_not_an_empty_backup() {
let (root, bin, src) = setup(
"empty",
&Stub {
remotes: "gdrive:\n",
combined: Some(""),
},
);
let r = run(&resource(&root, &src), &root, &bin);
assert_eq!(r.code, 1, "0 examined files must fail\n{}", r.out);
assert!(
r.out.contains("broken check, not an empty backup"),
"{}",
r.out
);
fs::remove_dir_all(&root).ok();
}
#[test]
fn a_crashed_check_that_writes_nothing_counts_as_an_error() {
let (root, bin, src) = setup(
"nofile",
&Stub {
remotes: "gdrive:\n",
combined: None,
},
);
let r = run(&resource(&root, &src), &root, &bin);
assert_eq!(
r.code, 1,
"a check producing no output must fail\n{}",
r.out
);
assert!(r.out.contains("produced NO output"), "{}", r.out);
assert!(r.status.contains(r#""errors":1"#), "{}", r.status);
fs::remove_dir_all(&root).ok();
}
#[test]
fn coverage_below_the_threshold_fails() {
let (root, bin, src) = setup(
"partial",
&Stub {
remotes: "gdrive:\n",
combined: Some("= a.mp4\n= b.mp4\n+ c.mp4\n* d.mp4\n"),
},
);
let r = run(&resource(&root, &src), &root, &bin);
assert_eq!(r.code, 1, "partial coverage must fail\n{}", r.out);
assert!(r.status.contains(r#""health":"partial""#), "{}", r.status);
assert!(r.status.contains(r#""coverage_pct":50"#), "{}", r.status);
fs::remove_dir_all(&root).ok();
}
#[test]
fn an_unconfigured_remote_stops_the_run_before_any_sync() {
let (root, bin, src) = setup(
"noremote",
&Stub {
remotes: "",
combined: Some("= a.mp4\n"),
},
);
let r = run(&resource(&root, &src), &root, &bin);
assert_eq!(r.code, 1, "unconfigured remote must abort\n{}", r.out);
assert!(r.out.contains("is not configured"), "{}", r.out);
assert!(!r.out.contains("sync media: starting"), "{}", r.out);
assert!(
r.status.is_empty(),
"no status may be written: {}",
r.status
);
fs::remove_dir_all(&root).ok();
}
#[test]
fn a_missing_source_stops_the_run() {
let (root, bin, src) = setup(
"nosrc",
&Stub {
remotes: "gdrive:\n",
combined: Some("= a.mp4\n"),
},
);
fs::remove_dir_all(&src).unwrap();
let r = run(&resource(&root, &src), &root, &bin);
assert_eq!(r.code, 1, "missing source must abort\n{}", r.out);
assert!(r.out.contains("does not exist"), "{}", r.out);
fs::remove_dir_all(&root).ok();
}
#[test]
fn a_missing_rclone_binary_stops_the_run() {
let root = tmpdir("norclone");
let bin = root.join("bin");
fs::create_dir_all(&bin).unwrap();
let src = root.join("media");
fs::create_dir_all(&src).unwrap();
for tool in [
"sh", "mktemp", "grep", "find", "rmdir", "head", "awk", "sed", "cat", "du", "stat",
] {
for dir in ["/usr/bin", "/bin", "/usr/local/bin"] {
let p = Path::new(dir).join(tool);
if p.exists() {
let _ = std::os::unix::fs::symlink(&p, bin.join(tool));
break;
}
}
}
assert!(
!bin.join("rclone").exists(),
"the hermetic bin must not contain rclone"
);
let script = root.join("sync.sh");
fs::write(&script, sync_body(&resource(&root, &src))).unwrap();
fs::set_permissions(&script, fs::Permissions::from_mode(0o755)).unwrap();
let out = Command::new("/bin/sh")
.arg(&script)
.env("PATH", bin.display().to_string())
.env("FORJAR_BACKUP_STATUS", root.join("status.json"))
.output()
.unwrap();
assert_eq!(out.status.code(), Some(1));
assert!(String::from_utf8_lossy(&out.stdout).contains("rclone is not installed"));
fs::remove_dir_all(&root).ok();
}
#[test]
fn files_only_in_the_remote_are_stale_and_do_not_fail_the_backup() {
let (root, bin, src) = setup(
"stale",
&Stub {
remotes: "gdrive:\n",
combined: Some("= a.mp4\n= b.mp4\n- old.mp4\n"),
},
);
let r = run(&resource(&root, &src), &root, &bin);
assert_eq!(
r.code, 0,
"stale remote files must not fail a fully-covered backup\n{}\n{}",
r.out, r.status
);
assert!(r.status.contains(r#""health":"verified""#), "{}", r.status);
assert!(r.status.contains(r#""coverage_pct":100"#), "{}", r.status);
assert!(r.status.contains(r#""stale_in_remote":1"#), "{}", r.status);
fs::remove_dir_all(&root).ok();
}