Files
Marcos Paulo ca0d68f9ab
ci / ci (macos-latest) (push) Has been cancelled
ci / ci (ubuntu-latest) (push) Has been cancelled
ci / ci (windows-latest) (push) Has been cancelled
feat: export.pandoc, properties.rename_all, first wasm plugin (dice) (MiniMax M3.1 Flash)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 06:39:51 -03:00

185 lines
7.7 KiB
Rust

//! The `export.pandoc` command: the argv, the timeout, the errors and the vault.
//!
//! A fake `pandoc` (a shell script) stands in for the real one, so the tests
//! never need pandoc installed: the script records its arguments and its
//! input and writes the file the command asked for.
mod common;
use std::path::PathBuf;
use std::time::{Duration, Instant};
use common::{on_disk, seed, temp_vault};
use notescraft_engine::Session;
use notescraft_engine::pandoc::{PandocFormat, PandocRequest, export_note, find_pandoc_in, locate_pandoc, run_pandoc};
use serde_json::json;
/// Writes a fake `pandoc` in `dir` and returns its path.
#[cfg(unix)]
fn fake_pandoc(dir: &std::path::Path, body: &str) -> PathBuf {
use std::os::unix::fs::PermissionsExt;
let script = dir.join("pandoc");
std::fs::write(&script, body).expect("write the fake pandoc");
std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).expect("make it runnable");
script
}
/// A fake `pandoc` that records its arguments and its input, then writes a file.
#[cfg(unix)]
fn recorder(dir: &std::path::Path) -> (PathBuf, PathBuf, PathBuf) {
let args = dir.join("args.txt");
let input = dir.join("input.txt");
let script = format!(
"#!/bin/sh\nprintf '%s\\n' \"$@\" > '{args}'\ncat > '{input}'\nfor last; do :; done\nprintf 'fake pandoc' > \"$last\"\n",
args = args.display(),
input = input.display()
);
(fake_pandoc(dir, &script), args, input)
}
/// A request that runs the fake in `dir` and writes `out.docx` there.
#[cfg(unix)]
fn request(dir: &std::path::Path, input: &str) -> PandocRequest {
PandocRequest {
format: PandocFormat::Docx,
out: dir.join("out.docx"),
input: input.to_owned(),
work_dir: dir.to_path_buf(),
timeout: Duration::from_secs(10),
}
}
/// A vault with a note that links to and embeds another one.
fn vault(name: &str) -> (Session, common::TempVault) {
let vault = temp_vault(name);
seed(vault.path(), &[("Note.md", "# Note\n\n[[Other]] ![[Other]]\n"), ("Other.md", "# Other\n")]);
let session = Session::open(vault.path()).expect("open the vault");
(session, vault)
}
/// The argv is exactly the documented one, and the note arrives on stdin.
#[cfg(unix)]
#[test]
fn the_argv_is_exactly_as_specified() {
let dir = temp_vault("pandoc-argv");
let (bin, args_file, input_file) = recorder(dir.path());
let request = request(dir.path(), "# Note\n");
let outcome = run_pandoc(&bin, &request).expect("the fake pandoc runs");
assert_eq!(outcome.bytes, 11, "the file the fake wrote");
let args = std::fs::read_to_string(&args_file).expect("the recorded arguments");
assert_eq!(
args.lines().collect::<Vec<_>>(),
vec!["--sandbox", "-f", "gfm+wikilinks_title_after_pipe", "-t", "docx", "-o", request.out.to_str().expect("the output path"),]
);
assert_eq!(std::fs::read_to_string(&input_file).expect("the recorded input"), "# Note\n");
}
/// A pandoc that never finishes is killed when the timeout passes.
#[cfg(unix)]
#[test]
fn a_sleeping_pandoc_is_killed_at_the_timeout() {
let dir = temp_vault("pandoc-timeout");
let bin = fake_pandoc(dir.path(), "#!/bin/sh\nexec /bin/sleep 30\n");
let mut request = request(dir.path(), "body");
request.timeout = Duration::from_millis(300);
let start = Instant::now();
let error = run_pandoc(&bin, &request).expect_err("the timeout kills the fake");
assert!(start.elapsed() < Duration::from_secs(20), "the run was killed, not waited for");
assert_eq!(error.code, "export_failed");
assert!(error.message.contains("did not finish"), "{}", error.message);
}
/// What pandoc wrote on stderr reaches the error.
#[cfg(unix)]
#[test]
fn the_stderr_of_a_failing_pandoc_is_surfaced() {
let dir = temp_vault("pandoc-stderr");
let bin = fake_pandoc(dir.path(), "#!/bin/sh\nprintf 'pandoc: unknown filter' >&2\nexit 1\n");
let request = request(dir.path(), "body");
let error = run_pandoc(&bin, &request).expect_err("the fake fails");
assert_eq!(error.code, "export_failed");
assert!(error.message.contains("unknown filter"), "{}", error.message);
}
/// Without pandoc the command says how to install it.
#[test]
fn a_missing_pandoc_is_reported() {
let error = locate_pandoc(&[]).expect_err("no folder holds a pandoc");
assert_eq!(error.code, "tool_missing");
assert!(error.message.contains("pandoc"), "{}", error.message);
assert!(error.message.contains("https://pandoc.org/installing.html"), "{}", error.message);
}
/// The binary is found in the folder that comes first.
#[cfg(unix)]
#[test]
fn the_binary_is_found_in_the_first_folder() {
let dir = temp_vault("pandoc-find");
let bin = fake_pandoc(dir.path(), "#!/bin/sh\nexit 0\n");
let other = temp_vault("pandoc-find-other");
let dirs = [dir.path().to_path_buf(), other.path().to_path_buf()];
assert_eq!(find_pandoc_in(&dirs), Some(bin));
assert!(find_pandoc_in(&[]).is_none());
}
/// The export lands beside the note, and a second one never overwrites it.
#[cfg(unix)]
#[test]
fn an_export_never_overwrites_an_older_one() {
let dir = temp_vault("pandoc-out");
let (bin, _args, _input) = recorder(dir.path());
let (session, vault) = vault("pandoc-out-vault");
let first = export_note(&session, &bin, &json!({ "path": "Note.md", "format": "docx" })).expect("the export runs");
assert_eq!(first.get("path"), Some(&json!("Note.docx")));
assert_eq!(on_disk(vault.path(), "Note.docx"), "fake pandoc");
let second = export_note(&session, &bin, &json!({ "path": "Note.md", "format": "docx" })).expect("the second export runs");
assert_eq!(second.get("path"), Some(&json!("Note 2.docx")));
assert_eq!(on_disk(vault.path(), "Note.docx"), "fake pandoc", "the first export is untouched");
assert_eq!(on_disk(vault.path(), "Note 2.docx"), "fake pandoc");
}
/// An output path outside the vault is refused.
#[cfg(unix)]
#[test]
fn an_output_outside_the_vault_is_refused() {
let dir = temp_vault("pandoc-outside");
let (bin, _args, _input) = recorder(dir.path());
let (session, _vault) = vault("pandoc-outside-vault");
let error = export_note(&session, &bin, &json!({ "path": "Note.md", "format": "docx", "out": "/tmp/note.docx" })).expect_err("refused");
assert_eq!(error.code, "invalid_params");
assert!(error.message.contains("vault-relative"), "{}", error.message);
}
/// A format pandoc does not know is refused.
#[cfg(unix)]
#[test]
fn an_unknown_format_is_refused() {
let dir = temp_vault("pandoc-format");
let (bin, _args, _input) = recorder(dir.path());
let (session, _vault) = vault("pandoc-format-vault");
let error = export_note(&session, &bin, &json!({ "path": "Note.md", "format": "pdf" })).expect_err("refused");
assert_eq!(error.code, "invalid_params");
assert!(error.message.contains("docx"), "{}", error.message);
}
/// The embeds become the text of the note they embed, when asked for.
#[cfg(unix)]
#[test]
fn the_embeds_become_the_text_of_the_note_they_embed() {
let dir = temp_vault("pandoc-embeds");
let (bin, _args, input_file) = recorder(dir.path());
let (session, _vault) = vault("pandoc-embeds-vault");
export_note(&session, &bin, &json!({ "path": "Note.md", "format": "html" })).expect("the export runs");
let plain = std::fs::read_to_string(&input_file).expect("the recorded input");
assert!(plain.contains("![[Other]]"), "{}", plain);
export_note(&session, &bin, &json!({ "path": "Note.md", "format": "html", "resolve_embeds": true })).expect("the export runs");
let resolved = std::fs::read_to_string(&input_file).expect("the recorded input");
assert!(resolved.contains("# Other"), "{}", resolved);
assert!(!resolved.contains("![[Other]]"), "{}", resolved);
}