mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-14 19:27:04 -04:00
* Release CI: macOS job on macos-15-xlarge Custom macOS larger runners in a runner group do not pick up jobs; the built-in M2 Pro label does. Only release.yml uses it, and the actions_macos_xl budget caps the monthly spend. Co-Authored-By: Claude Opus 5.5 <[email protected]> (cherry picked from commit90d60b0c98) * CI: tell actionlint about the org's macos-release runner label (#202) Co-authored-by: Claude Opus 5.5 <[email protected]> (cherry picked from commitddea3c72e0) * About: contributor and model credits compiled in (grab bag + table, self-submitted names) (#196) * About: contributors compiled in from contributors.json (grab bag + table, sortable, name toggle) Adds About ▸ Contributors and About ▸ Models tabs per the craftrules standard (standards/contributors.md): contributors/contributors.json is generated by craftrules/scripts/contributors.py and baked into the binary by crates/ui-egui/build.rs as static tables; nothing is read at run time. Each contributor line: @username, opt-in display/real name, PRs, commits, lines +/−/Δ, binary assets +/−, first/last commit. Sortable by every column; alphabetical ignores case and the leading @. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Contributors: self-submitted names in contributors/people.toml; AGENTS.md credit-yourself section build.rs reads contributors/people.toml next to the generated contributors.json and shows a name only for entries the generator verified as written by that person (craftrules standards/contributors.md). Co-Authored-By: Claude Opus 5.5 <[email protected]> * Contributors: regenerate (verified people.toml entries) Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]> (cherry picked from commit41e214c318) * M13: exit cleanly when stdout pipe closes (#203) Co-authored-by: Brandon Thomas <[email protected]> (cherry picked from commit1cf0d319e4) --------- Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Ayan <[email protected]>
85 lines
3.6 KiB
Rust
85 lines
3.6 KiB
Rust
//! A closed output pipe is normal when a consumer such as `head` exits early (#135).
|
|
|
|
use std::io::{Read, pipe};
|
|
use std::process::{Command, Output, Stdio};
|
|
|
|
const BIN: &str = env!("CARGO_BIN_EXE_pdfcraft-cli");
|
|
|
|
fn closed_stdout(args: &[&str]) -> Output {
|
|
let (reader, writer) = pipe().unwrap();
|
|
drop(reader);
|
|
Command::new(BIN).args(args).stdout(writer).stderr(Stdio::piped()).spawn().unwrap().wait_with_output().unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn output_commands_exit_cleanly_when_the_reader_has_closed() {
|
|
let base = std::env::temp_dir().join(format!("pdfcraft-cli-stdout-{}", std::process::id()));
|
|
std::fs::create_dir_all(&base).unwrap();
|
|
let pdf = base.join("text.pdf");
|
|
let bytes = pdfcraft_engine::Session::new().create_from_text("Pipe test", "Hello from PdfCraft").unwrap();
|
|
std::fs::write(&pdf, bytes.as_slice()).unwrap();
|
|
let path = pdf.to_str().unwrap();
|
|
let script = base.join("steps.json");
|
|
let saved = base.join("should-not-be-written.pdf");
|
|
let steps = serde_json::json!([
|
|
{ "tool": "doc_create", "args": { "from": "blank" } },
|
|
{ "tool": "doc_save", "args": { "doc": 1, "path": saved } },
|
|
]);
|
|
std::fs::write(&script, steps.to_string()).unwrap();
|
|
for args in [
|
|
vec!["tools"],
|
|
vec!["--version"],
|
|
vec!["info", path],
|
|
vec!["text", path],
|
|
vec!["check-one", path],
|
|
vec!["run", "command_list"],
|
|
vec!["run", "--script", script.to_str().unwrap()],
|
|
vec!["split", path, "--every", "1", "--out-dir", base.to_str().unwrap()],
|
|
] {
|
|
let out = closed_stdout(&args);
|
|
assert!(out.status.success(), "{args:?}: {}: {}", out.status, String::from_utf8_lossy(&out.stderr));
|
|
assert!(out.stderr.is_empty(), "{args:?}: {}", String::from_utf8_lossy(&out.stderr));
|
|
}
|
|
assert!(!saved.exists(), "a closed output pipe stops the script before the next edit");
|
|
let _ = std::fs::remove_dir_all(base);
|
|
}
|
|
|
|
#[test]
|
|
fn tools_exits_cleanly_when_the_reader_stops_after_one_line() {
|
|
let mut child = Command::new(BIN).arg("tools").stdout(Stdio::piped()).stderr(Stdio::piped()).spawn().unwrap();
|
|
let mut stdout = child.stdout.take().unwrap();
|
|
// Read through the first newline without buffering the rest, then close the pipe.
|
|
let mut byte = [0];
|
|
while stdout.read_exact(&mut byte).is_ok() && byte[0] != b'\n' {}
|
|
drop(stdout);
|
|
let out = child.wait_with_output().unwrap();
|
|
assert!(out.status.success(), "{}: {}", out.status, String::from_utf8_lossy(&out.stderr));
|
|
assert!(out.stderr.is_empty(), "{}", String::from_utf8_lossy(&out.stderr));
|
|
}
|
|
|
|
#[test]
|
|
fn a_closed_stdout_does_not_hide_command_errors() {
|
|
let out = closed_stdout(&["info"]);
|
|
assert!(!out.status.success());
|
|
assert!(String::from_utf8_lossy(&out.stderr).contains("info: missing file"));
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
#[test]
|
|
fn other_stdout_errors_fail_without_panicking() {
|
|
let full = std::fs::OpenOptions::new().write(true).open("/dev/full").unwrap();
|
|
let out = Command::new(BIN).arg("--version").stdout(full).stderr(Stdio::piped()).spawn().unwrap().wait_with_output().unwrap();
|
|
assert_eq!(out.status.code(), Some(1));
|
|
let stderr = String::from_utf8_lossy(&out.stderr);
|
|
assert!(stderr.contains("stdout") && !stderr.contains("panicked"), "{stderr}");
|
|
}
|
|
|
|
#[test]
|
|
fn ordinary_output_keeps_its_format() {
|
|
let out = Command::new(BIN).arg("tools").output().unwrap();
|
|
assert!(out.status.success() && out.stderr.is_empty());
|
|
let tools: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
|
|
assert!(tools.as_array().unwrap().iter().any(|t| t["name"] == "doc_open"));
|
|
assert!(out.stdout.ends_with(b"\n"));
|
|
}
|