185 lines
7.7 KiB
Rust
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);
|
|
}
|