use std::io::Write;
use std::path::Path;
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
use tempfile::TempDir;
fn snapper_binary() -> Command {
Command::new(env!("CARGO_BIN_EXE_snapper"))
}
fn fixture_with_formatter(argv: &[&str]) -> TempDir {
let dir = TempDir::new().expect("tempdir");
let mut toml = String::from(
"[code.rust]\nline_comment = \"//\"\nblock_comment = [\"/*\", \"*/\"]\nformatter = [",
);
for (i, arg) in argv.iter().enumerate() {
if i > 0 {
toml.push_str(", ");
}
toml.push('"');
toml.push_str(arg);
toml.push('"');
}
toml.push_str("]\n");
std::fs::write(dir.path().join(".snapperrc.toml"), &toml).expect("write rcfile");
dir
}
fn run_snapper(input: &str, dir: &Path, extra_args: &[&str]) -> std::process::Output {
let mut cmd = snapper_binary();
cmd.current_dir(dir);
cmd.args(["--format", "markdown", "--format-code"]);
cmd.args(extra_args);
cmd.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = cmd.spawn().expect("spawn snapper");
child
.stdin
.take()
.unwrap()
.write_all(input.as_bytes())
.unwrap();
child.wait_with_output().expect("wait snapper")
}
fn run_snapper_no_format(input: &str, dir: &Path) -> std::process::Output {
let mut cmd = snapper_binary();
cmd.current_dir(dir);
cmd.args(["--format", "markdown"]);
cmd.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = cmd.spawn().expect("spawn snapper");
child
.stdin
.take()
.unwrap()
.write_all(input.as_bytes())
.unwrap();
child.wait_with_output().expect("wait snapper")
}
fn assert_idempotent(input: &str, dir: &Path, extra_args: &[&str]) -> String {
let first = run_snapper(input, dir, extra_args);
assert!(
first.status.success(),
"first snapper run failed: stderr={}",
String::from_utf8_lossy(&first.stderr)
);
let first_out = String::from_utf8(first.stdout).expect("utf8");
let second = run_snapper(&first_out, dir, extra_args);
assert!(second.status.success(), "second pass failed");
let second_out = String::from_utf8(second.stdout).expect("utf8");
assert_eq!(
first_out, second_out,
"format_text(format_text(input)) must equal format_text(input)"
);
first_out
}
const SAMPLE_INPUT: &str = "\
```rust
// First sentence. Second sentence.
fn main() {}
```
";
#[test]
fn cat_formatter_round_trips_with_comment_reflow() {
let dir = fixture_with_formatter(&["cat"]);
let out = assert_idempotent(SAMPLE_INPUT, dir.path(), &[]);
assert!(out.contains("// First sentence.\n// Second sentence.\nfn main() {}\n"));
let no_fmt = run_snapper_no_format(SAMPLE_INPUT, dir.path());
assert!(no_fmt.status.success());
let no_fmt_out = String::from_utf8(no_fmt.stdout).unwrap();
assert_eq!(out, no_fmt_out);
}
#[test]
fn tr_uppercase_formatter_transforms_body() {
let dir = fixture_with_formatter(&["tr", "a-z", "A-Z"]);
let out = assert_idempotent(SAMPLE_INPUT, dir.path(), &[]);
assert!(out.starts_with("```rust\n"));
assert!(out.contains("// FIRST SENTENCE.\n// SECOND SENTENCE.\nFN MAIN() {}\n"));
assert!(out.ends_with("```\n"));
}
#[test]
fn sleep_formatter_times_out_within_bound() {
let dir = fixture_with_formatter(&["sleep", "60"]);
let start = Instant::now();
let out = run_snapper(SAMPLE_INPUT, dir.path(), &[]);
let elapsed = start.elapsed();
assert!(out.status.success(), "snapper exit non-zero on timeout");
assert!(
elapsed < Duration::from_secs(35),
"snapper waited {:?} for sleep-60 formatter; must be < 35s",
elapsed,
);
let stdout = String::from_utf8(out.stdout).unwrap();
let stderr = String::from_utf8(out.stderr).unwrap();
assert!(stdout.contains("// First sentence.\n// Second sentence."));
assert!(
stderr.contains("formatter") || stderr.contains("snapper:"),
"expected formatter diagnostic on stderr, got: {stderr}",
);
}
#[test]
fn missing_formatter_is_non_fatal() {
let dir = fixture_with_formatter(&["this-binary-does-not-exist-xyz-9k"]);
let out = run_snapper(SAMPLE_INPUT, dir.path(), &[]);
assert!(
out.status.success(),
"snapper must exit 0 on missing formatter; got status {:?}",
out.status.code(),
);
let stdout = String::from_utf8(out.stdout).unwrap();
let stderr = String::from_utf8(out.stderr).unwrap();
assert!(stdout.contains("// First sentence.\n// Second sentence.\nfn main() {}"));
assert!(
stderr.contains("formatter not found") || stderr.contains("snapper:"),
"expected `formatter not found` diagnostic on stderr, got: {stderr}",
);
let second = run_snapper(&stdout, dir.path(), &[]);
assert!(second.status.success());
assert_eq!(stdout, String::from_utf8(second.stdout).unwrap());
}
#[test]
fn non_zero_formatter_exit_is_non_fatal() {
let dir = fixture_with_formatter(&["sh", "-c", "exit 1"]);
let out = run_snapper(SAMPLE_INPUT, dir.path(), &[]);
assert!(out.status.success());
let stdout = String::from_utf8(out.stdout).unwrap();
let stderr = String::from_utf8(out.stderr).unwrap();
assert!(stdout.contains("// First sentence.\n// Second sentence."));
assert!(
stderr.contains("snapper:") || stderr.contains("formatter"),
"expected formatter diagnostic; got: {stderr}",
);
let second = run_snapper(&stdout, dir.path(), &[]);
assert!(second.status.success());
assert_eq!(stdout, String::from_utf8(second.stdout).unwrap());
}
#[test]
fn format_code_default_off_skips_formatter() {
let dir = fixture_with_formatter(&["false"]);
let out = run_snapper_no_format(SAMPLE_INPUT, dir.path());
assert!(out.status.success());
let stderr = String::from_utf8(out.stderr).unwrap();
assert!(
!stderr.contains("formatter"),
"expected no formatter diagnostic without --format-code; got: {stderr}",
);
}
#[test]
fn matrix_idempotence_cat_formatter() {
let dir = fixture_with_formatter(&["cat"]);
for tail in [
"Vec<...>",
"<.>",
"<..>",
"<a.>",
"<a.b>",
"<.b>",
"<a>",
"<>",
] {
let input = format!(
"```rust\nfn main() {{}}\n// e.g. {tail}\n```\n",
);
let out = assert_idempotent(&input, dir.path(), &[]);
assert_eq!(
out, input,
"shape {tail:?} did not round-trip under cat formatter: {out}",
);
}
}