use std::path::Path;
use std::process::Command;
use tempfile::TempDir;
const DBMD: &str = env!("CARGO_BIN_EXE_dbmd");
struct Store {
dir: TempDir,
}
impl Store {
fn new() -> Self {
let dir = TempDir::new().expect("tempdir");
std::fs::write(
dir.path().join("DB.md"),
"---\ntype: db-md\nscope: company\nowner: T\n---\n\n# Store\n",
)
.expect("write DB.md");
Store { dir }
}
fn root(&self) -> &Path {
self.dir.path()
}
fn abs(&self, rel: &str) -> std::path::PathBuf {
self.dir.path().join(rel)
}
fn seed(&self, rel: &str, contents: &str) {
let abs = self.abs(rel);
std::fs::create_dir_all(abs.parent().unwrap()).unwrap();
std::fs::write(abs, contents).unwrap();
}
fn seed_bytes(&self, rel: &str, bytes: &[u8]) {
let abs = self.abs(rel);
std::fs::create_dir_all(abs.parent().unwrap()).unwrap();
std::fs::write(abs, bytes).unwrap();
}
fn run(&self, args: &[&str]) -> Output {
let mut cmd = Command::new(DBMD);
cmd.args(args).arg("--dir").arg(self.root());
let out = cmd.output().expect("spawn dbmd");
Output {
code: out.status.code(),
stdout: String::from_utf8_lossy(&out.stdout).into_owned(),
stderr: String::from_utf8_lossy(&out.stderr).into_owned(),
}
}
}
struct Output {
code: Option<i32>,
stdout: String,
stderr: String,
}
impl Output {
fn stdout_json(&self) -> serde_json::Value {
serde_json::from_str(self.stdout.trim())
.unwrap_or_else(|e| panic!("stdout is not JSON ({e}): {:?}", self.stdout))
}
}
#[test]
fn regression_rename_skips_non_utf8_linker_and_completes_consistently() {
let store = Store::new();
store.seed(
"records/contacts/sarah.md",
"---\ntype: contact\nsummary: x\n---\n# Sarah\n",
);
store.seed(
"wiki/topics/clean.md",
"---\ntype: wiki-page\nsummary: s\n---\nSee [[records/contacts/sarah]].\n",
);
let mut bad: Vec<u8> = Vec::new();
bad.extend_from_slice(b"---\ntype: source\nsummary: s\n---\n");
bad.extend_from_slice(b"Ref [[records/contacts/sarah]] here. caf");
bad.push(0xE9); bad.extend_from_slice(b"\n");
store.seed_bytes("sources/import/dropped.md", &bad);
let out = store.run(&[
"--json",
"rename",
"records/contacts/sarah.md",
"records/contacts/sarah-chen.md",
]);
assert_eq!(
out.code,
Some(0),
"rename must complete despite a non-UTF8 linker; stderr: {}",
out.stderr
);
assert!(
!store.abs("records/contacts/sarah.md").exists(),
"source must be moved away from <old>"
);
assert!(
store.abs("records/contacts/sarah-chen.md").exists(),
"destination must exist at <new>"
);
let clean = std::fs::read_to_string(store.abs("wiki/topics/clean.md")).unwrap();
assert!(
clean.contains("[[records/contacts/sarah-chen]]"),
"clean linker must be retargeted; got: {clean}"
);
assert!(
!clean.contains("[[records/contacts/sarah]]"),
"clean linker must no longer reference the old path; got: {clean}"
);
let bad_after = std::fs::read(store.abs("sources/import/dropped.md")).unwrap();
assert_eq!(
bad_after, bad,
"the skipped non-UTF8 linker must be left byte-for-byte unchanged"
);
assert!(
out.stderr.contains("non-UTF8") && out.stderr.contains("sources/import/dropped.md"),
"a skipped non-UTF8 linker must surface a warning naming it; stderr: {}",
out.stderr
);
let v = out.stdout_json();
assert_eq!(
v["links_rewritten"], 1,
"only the clean linker counts as rewritten"
);
}
#[test]
fn regression_rename_non_utf8_linker_does_not_strand_later_linkers() {
let store = Store::new();
store.seed(
"records/contacts/sarah.md",
"---\ntype: contact\nsummary: x\n---\n# Sarah\n",
);
let mut bad: Vec<u8> = Vec::new();
bad.extend_from_slice(b"---\ntype: source\nsummary: s\n---\n");
bad.extend_from_slice(b"[[records/contacts/sarah]] ");
bad.push(0xFF); bad.extend_from_slice(b"\n");
store.seed_bytes("sources/a-import.md", &bad);
store.seed(
"wiki/topics/late.md",
"---\ntype: wiki-page\nsummary: s\n---\nMentions [[records/contacts/sarah|Sarah]].\n",
);
let out = store.run(&[
"rename",
"records/contacts/sarah.md",
"records/contacts/sarah-chen.md",
]);
assert_eq!(
out.code,
Some(0),
"rename must complete; stderr: {}",
out.stderr
);
let late = std::fs::read_to_string(store.abs("wiki/topics/late.md")).unwrap();
assert!(
late.contains("[[records/contacts/sarah-chen|Sarah]]"),
"a clean linker sorting after a non-UTF8 linker must still be rewritten; got: {late}"
);
assert!(!store.abs("records/contacts/sarah.md").exists());
assert!(store.abs("records/contacts/sarah-chen.md").exists());
}
#[test]
fn regression_rename_rewrites_self_link_through_the_deferred_move() {
let store = Store::new();
store.seed(
"records/contacts/sarah.md",
"---\ntype: contact\nsummary: x\nlinks:\n - [[records/contacts/sarah]]\n---\nI am [[records/contacts/sarah]].\n",
);
let out = store.run(&[
"--json",
"rename",
"records/contacts/sarah.md",
"records/contacts/sarah-chen.md",
]);
assert_eq!(
out.code,
Some(0),
"self-link rename must succeed; stderr: {}",
out.stderr
);
assert!(!store.abs("records/contacts/sarah.md").exists());
let moved = std::fs::read_to_string(store.abs("records/contacts/sarah-chen.md")).unwrap();
assert!(
moved.contains("[[records/contacts/sarah-chen]]"),
"the self-link must be retargeted to the new path; got: {moved}"
);
assert!(
!moved.contains("[[records/contacts/sarah]]"),
"no stale self-link to the old path may remain; got: {moved}"
);
}