Merge pull request #126 from vbasky/fix/96-closed-stdout

Keep going when the CLI's stdout is closed.
This commit is contained in:
Brandon Thomas 2026-10-11 19:50:30 -04:00 committed by GitHub
commit ceae137642
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 127 additions and 2 deletions

View File

@ -20,11 +20,12 @@ use soundcraft_engine::Engine;
use std::io::Write;
use std::process::ExitCode;
/// Print without panicking on a closed pipe.
/// Print without panicking on a closed pipe. A write error must not skip later work
/// such as `--bounce` or `--save`, and must not by itself report success.
macro_rules! outln {
($($t:tt)*) => {{
let mut o = std::io::stdout().lock();
if writeln!(o, $($t)*).is_err() { return ExitCode::SUCCESS; }
let _ = writeln!(o, $($t)*);
}};
}

View File

@ -0,0 +1,124 @@
//! A closed stdout must not abort `run` or `script` before `--bounce` and `--save`.
use std::process::{Command, Stdio};
fn bin() -> Command {
let mut cmd = Command::new(env!("CARGO_BIN_EXE_soundcraft-cli"));
cmd.env("SOUNDCRAFT_NO_PREFS", "1");
cmd
}
/// Drop the read end so the child's stdout writes fail with `EPIPE`.
fn closed_stdout() -> std::process::Stdio {
let (reader, writer) = std::io::pipe().expect("pipe");
drop(reader);
Stdio::from(writer)
}
fn temp_dir(name: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!("sc-cli-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
dir
}
#[test]
fn run_with_closed_stdout_still_bounces_and_saves() {
let dir = temp_dir("run");
let wav = dir.join("mix.wav");
let session = dir.join("Synthetic.scraft");
let output = bin()
.arg("run")
.arg("--cmd")
.arg(r#"track.new={"name":"Synthetic","count":1,"format":"stereo"}"#)
.arg("--bounce")
.arg(&wav)
.arg("--save")
.arg(&session)
.arg("--start")
.arg("0")
.arg("--end")
.arg("0.01")
.stdout(closed_stdout())
.stderr(Stdio::piped())
.output()
.unwrap();
assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr));
assert!(wav.is_file(), "bounce was skipped");
assert!(session.is_file(), "save was skipped");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn script_with_closed_stdout_still_bounces_and_saves() {
let dir = temp_dir("script");
let script = dir.join("edit.scraft.txt");
std::fs::write(&script, "track.new {\"name\":\"Synthetic\",\"count\":1,\"format\":\"stereo\"}\n").unwrap();
let wav = dir.join("mix.wav");
let session = dir.join("Synthetic.scraft");
let output = bin()
.arg("script")
.arg(&script)
.arg("--bounce")
.arg(&wav)
.arg("--save")
.arg(&session)
.arg("--start")
.arg("0")
.arg("--end")
.arg("0.01")
.stdout(closed_stdout())
.stderr(Stdio::piped())
.output()
.unwrap();
assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr));
assert!(wav.is_file());
assert!(session.is_file());
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn bounce_then_save_with_closed_stdout_writes_the_wav() {
let dir = temp_dir("bounce");
let wav = dir.join("mix.wav");
let session = dir.join("Empty.scraft");
let output = bin()
.arg("run")
.arg("--bounce")
.arg(&wav)
.arg("--save")
.arg(&session)
.arg("--start")
.arg("0")
.arg("--end")
.arg("0.01")
.stdout(closed_stdout())
.stderr(Stdio::piped())
.output()
.unwrap();
assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr));
assert!(wav.is_file());
assert!(session.is_file());
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn invalid_bounce_destination_still_fails() {
let dir = temp_dir("bad-bounce");
let missing = dir.join("no-such-dir").join("mix.wav");
let output = bin()
.arg("run")
.arg("--bounce")
.arg(&missing)
.arg("--start")
.arg("0")
.arg("--end")
.arg("0.01")
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.unwrap();
assert!(!output.status.success(), "a bounce that cannot be written must fail");
assert!(!missing.exists());
let _ = std::fs::remove_dir_all(&dir);
}