mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-07 03:01:55 +00:00
112 lines
4.9 KiB
Rust
112 lines
4.9 KiB
Rust
//! The exit codes the CLI has today: `ExitCode::SUCCESS` (0) or `ExitCode::FAILURE` (1).
|
|
//!
|
|
//! Pinned here so that the exit-code contract is a visible, reviewed change when it comes: a
|
|
//! failed file, an outside-root path and a usage error all exit 1 today.
|
|
|
|
use serde_json::Value;
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::{Command, Output};
|
|
|
|
const BIN: &str = env!("CARGO_BIN_EXE_pdfcraft-cli");
|
|
|
|
/// A fresh folder for one test, so the tests can run in parallel and leave nothing behind.
|
|
fn workdir(name: &str) -> PathBuf {
|
|
let dir = std::env::temp_dir().join(format!("pdfcraft-cli-exit-{}-{name}", std::process::id()));
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
dir
|
|
}
|
|
|
|
/// `batch` with `args`, run from the working directory `dir`.
|
|
fn batch(dir: &Path, args: &[&str]) -> Output {
|
|
Command::new(BIN).current_dir(dir).arg("batch").args(args).output().unwrap()
|
|
}
|
|
|
|
/// `run --script --report` from the working directory `dir`: one JSON document for the run.
|
|
fn run_report(dir: &Path, script: &Path) -> Output {
|
|
Command::new(BIN).current_dir(dir).args(["run", "--script"]).arg(script).arg("--report").output().unwrap()
|
|
}
|
|
|
|
/// The one JSON document a command prints on stdout.
|
|
fn reported(output: &Output) -> Value {
|
|
serde_json::from_slice(&output.stdout).unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn batch_exits_0_when_every_file_succeeds() {
|
|
let dir = workdir("batch-ok");
|
|
std::fs::write(dir.join("page.txt"), "Exit code page").unwrap();
|
|
let output = batch(&dir, &["page.txt", "--steps", "convert", "--out-dir", "out"]);
|
|
assert_eq!(output.status.code(), Some(0), "{}", String::from_utf8_lossy(&output.stderr));
|
|
let report = reported(&output);
|
|
assert_eq!(report["completed"], 1, "{report}");
|
|
assert_eq!(report["failed"], 0, "{report}");
|
|
assert!(dir.join("out/page.pdf").exists(), "the converted page is written");
|
|
std::fs::remove_dir_all(&dir).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn batch_exits_1_when_one_file_fails() {
|
|
let dir = workdir("batch-fail");
|
|
std::fs::write(dir.join("bad.pdf"), "broken").unwrap();
|
|
let output = batch(&dir, &["bad.pdf", "--steps", "convert", "--out-dir", "out"]);
|
|
assert_eq!(output.status.code(), Some(1), "{}", String::from_utf8_lossy(&output.stderr));
|
|
let report = reported(&output);
|
|
assert_eq!(report["failed"], 1, "{report}");
|
|
std::fs::remove_dir_all(&dir).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn batch_usage_error_exits_1_and_creates_nothing() {
|
|
let dir = workdir("batch-usage");
|
|
std::fs::write(dir.join("page.txt"), "Exit code page").unwrap();
|
|
// Baseline: a usage error exits 1 today. A later change to the exit-code contract will
|
|
// update this assertion on purpose.
|
|
let output = batch(&dir, &["page.txt", "--out-dir", "out"]);
|
|
assert_eq!(output.status.code(), Some(1), "{}", String::from_utf8_lossy(&output.stderr));
|
|
assert!(output.stdout.is_empty(), "a usage error prints to stderr, not a report on stdout");
|
|
assert!(!output.stderr.is_empty(), "a usage error says what is wrong");
|
|
assert!(!dir.join("out").exists(), "a refused batch creates no output");
|
|
std::fs::remove_dir_all(&dir).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn batch_outside_root_exits_1_and_creates_nothing() {
|
|
let dir = workdir("batch-outside-root");
|
|
let output = Command::new(BIN)
|
|
.current_dir(&dir)
|
|
.args(["batch", "../outside.pdf", "--steps", "convert", "--out-dir", "out", "--root"])
|
|
.arg(&dir)
|
|
.output()
|
|
.unwrap();
|
|
assert_eq!(output.status.code(), Some(1), "{}", String::from_utf8_lossy(&output.stderr));
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
assert!(stderr.contains("is outside the allowed directory"), "the input is refused by the root: {stderr}");
|
|
assert!(output.stdout.is_empty(), "a refused batch prints no report");
|
|
assert!(!dir.join("out").exists(), "a refused batch creates no output");
|
|
std::fs::remove_dir_all(&dir).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn run_script_report_exits_0_when_ok_and_1_when_a_step_fails() {
|
|
let dir = workdir("run-report");
|
|
let script = dir.join("steps.json");
|
|
let blank = serde_json::json!({ "tool": "doc_create", "args": { "from": "blank", "width": 100, "height": 100 } });
|
|
let steps = serde_json::json!([blank.clone(), { "tool": "no_such_tool", "args": {} }]);
|
|
std::fs::write(&script, steps.to_string()).unwrap();
|
|
|
|
let failing = run_report(&dir, &script);
|
|
assert_eq!(failing.status.code(), Some(1), "{}", String::from_utf8_lossy(&failing.stderr));
|
|
let report = reported(&failing);
|
|
assert_eq!(report["ok"], false, "{report}");
|
|
assert_eq!(report["steps"][1]["status"], "error", "{report}");
|
|
|
|
std::fs::write(&script, serde_json::json!([blank]).to_string()).unwrap();
|
|
let good = run_report(&dir, &script);
|
|
assert_eq!(good.status.code(), Some(0), "{}", String::from_utf8_lossy(&good.stderr));
|
|
let report = reported(&good);
|
|
assert_eq!(report["ok"], true, "{report}");
|
|
assert_eq!(report["steps"][0]["status"], "ok", "{report}");
|
|
std::fs::remove_dir_all(&dir).unwrap();
|
|
}
|