//! Shared helpers for the integration tests: temp vaults and the real binary. #![allow(dead_code)] // every helper is used by one of the two test binaries use std::io::Write; use std::path::{Path, PathBuf}; use std::process::{Command, Output, Stdio}; use serde_json::Value; /// What one CLI run produced. pub struct Run { /// The exit code: 0 ok, 1 command error, 2 usage error, 3 vault. pub code: i32, /// Standard output, as text. pub stdout: String, /// Standard error, as text. pub stderr: String, } impl Run { /// The `result` of the JSON envelope. pub fn result(&self) -> Value { let envelope = self.envelope(); envelope.get("result").cloned().unwrap_or(Value::Null) } /// The whole JSON envelope. pub fn envelope(&self) -> Value { serde_json::from_str(&self.stdout).unwrap_or_else(|error| panic!("stdout is not JSON ({error}): {}", self.stdout)) } /// The `error.code` of the JSON envelope. pub fn error_code(&self) -> String { self.envelope().get("error").and_then(|error| error.get("code")).and_then(Value::as_str).unwrap_or_default().to_owned() } } /// An empty vault under the system temp directory, unique per test process. pub fn empty_vault(name: &str) -> PathBuf { let root = std::env::temp_dir().join(format!("notescraft-cli-{}-{name}", std::process::id())); let _ = std::fs::remove_dir_all(&root); std::fs::create_dir_all(&root).expect("create the temp vault"); root } /// A vault with notes, links, tags, a duplicate name, a broken frontmatter /// block, an attachment and a template. pub fn sample_vault(name: &str) -> PathBuf { let root = empty_vault(name); note(&root, "Plan.md", "---\ntags: [work, work/q1]\n---\n# Plan\n\nSee [[Missing]] and [[Notes/Other]].\n"); note(&root, "Notes/Other.md", "Body mentioning Plan in plain text.\n\n![[photo.png]]\n"); note(&root, "Notes/plan.md", "A second note with the same name.\n"); note(&root, "Notes/Broken.md", "---\nbroken: true\nthe block is never closed\n"); note(&root, "Templates/Daily.md", "# {{title}}\n\n{{date}}\n"); write_file(&root, "photo.png", "not really a png"); root } /// Writes a note, creating its folders. pub fn note(root: &Path, path: &str, content: &str) { write_file(root, path, content); } /// Writes any file, creating its folders. pub fn write_file(root: &Path, path: &str, content: &str) { let full = root.join(path); if let Some(parent) = full.parent() { std::fs::create_dir_all(parent).expect("create the folder"); } std::fs::write(&full, content).expect("write the file"); } /// The bytes of a vault file. pub fn read_file(root: &Path, path: &str) -> String { std::fs::read_to_string(root.join(path)).expect("read the file") } /// Whether a vault file exists. pub fn exists(root: &Path, path: &str) -> bool { root.join(path).exists() } /// Runs the binary with `args` and no stdin, with `vault` in the environment. pub fn run(vault: &Path, args: &[&str]) -> Run { spawn(vault, args, None) } /// Runs the binary with `args` and feeds `stdin` to it. pub fn run_with_stdin(vault: &Path, args: &[&str], stdin: &str) -> Run { spawn(vault, args, Some(stdin)) } /// Runs the binary with no vault at all. pub fn run_without_vault(args: &[&str]) -> Run { let output = Command::new(env!("CARGO_BIN_EXE_notescraft-cli")).env_remove("NOTESCRAFT_VAULT").args(args).output().expect("run the binary"); done(&output) } /// Runs the binary with raw (possibly non-UTF-8) arguments and no vault. pub fn run_raw(args: &[std::ffi::OsString]) -> Run { let output = Command::new(env!("CARGO_BIN_EXE_notescraft-cli")).env_remove("NOTESCRAFT_VAULT").args(args).output().expect("run the binary"); done(&output) } /// One process: `NOTESCRAFT_VAULT` is set, `stdin` is optional. fn spawn(vault: &Path, args: &[&str], stdin: Option<&str>) -> Run { let mut child = Command::new(env!("CARGO_BIN_EXE_notescraft-cli")) .env("NOTESCRAFT_VAULT", vault) .args(args) .stdin(if stdin.is_some() { Stdio::piped() } else { Stdio::null() }) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() .expect("spawn the binary"); if let Some(text) = stdin { child.stdin.as_mut().expect("stdin is piped").write_all(text.as_bytes()).expect("write stdin"); } done(&child.wait_with_output().expect("wait for the binary")) } /// The exit code and the two streams as text. fn done(output: &Output) -> Run { Run { code: output.status.code().unwrap_or(-1), stdout: String::from_utf8_lossy(&output.stdout).into_owned(), stderr: String::from_utf8_lossy(&output.stderr).into_owned(), } }