Files
Marcos Paulo 7a70adc7ac feat(cli): wave 8
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 00:00:21 -03:00

130 lines
4.7 KiB
Rust

//! 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(),
}
}