use super::super::{Error, run};
use super::envelope;
use std::io::{self, Cursor, Read, Write};
use tempfile::TempDir;
fn run_json(input: &str) -> (Result<(), Error>, Vec<u8>) {
let mut stdin = Cursor::new(input.as_bytes().to_vec());
let mut stdout = Vec::new();
let got = run(&mut stdin, &mut stdout);
(got, stdout)
}
#[test]
fn a_patch_applies_and_the_report_lands_on_stdout() {
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("new.txt");
let body = format!("*** Add File: {}\n+hi", path.display());
let input = serde_json::json!({ "input": envelope(&body) }).to_string();
let (got, stdout) = run_json(&input);
got.expect("patch applies");
assert_eq!(std::fs::read_to_string(&path).unwrap(), "hi\n");
let report: serde_json::Value = serde_json::from_slice(&stdout).unwrap();
assert_eq!(report["status"], "applied");
assert_eq!(report["files"][0]["op"], "add");
assert_eq!(report["files"][0]["path"], path.display().to_string());
}
#[test]
fn an_update_report_carries_the_rung_per_hunk() {
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("a.txt");
std::fs::write(&path, "x\n").unwrap();
let body = format!("*** Update File: {}\n-x\n+y", path.display());
let input = serde_json::json!({ "input": envelope(&body) }).to_string();
let (got, stdout) = run_json(&input);
got.expect("patch applies");
let report: serde_json::Value = serde_json::from_slice(&stdout).unwrap();
let hunk = &report["files"][0]["hunks"][0];
assert_eq!(hunk["rung"], "exact");
assert_eq!(hunk["line"], 1);
assert!(hunk.get("matched").is_none(), "exact match omits `matched`");
}
#[test]
fn malformed_input_json_is_declined() {
let (got, _) = run_json("not json");
let err = got.unwrap_err();
assert!(matches!(err, Error::InvalidJson(_)), "{err}");
assert!(err.to_string().starts_with("invalid input JSON: "), "{err}");
}
#[test]
fn an_unparseable_envelope_is_a_parse_decline() {
let input = serde_json::json!({ "input": "no markers" }).to_string();
let (got, _) = run_json(&input);
let err = got.unwrap_err();
assert!(matches!(err, Error::Parse(_)), "{err}");
}
#[test]
fn a_refused_patch_is_an_apply_decline() {
let tmp = TempDir::new().unwrap();
let body = format!("*** Delete File: {}", tmp.path().join("absent").display());
let input = serde_json::json!({ "input": envelope(&body) }).to_string();
let (got, _) = run_json(&input);
let err = got.unwrap_err();
assert!(matches!(err, Error::Apply(_)), "{err}");
}
struct FailingReader;
impl Read for FailingReader {
fn read(&mut self, _buf: &mut [u8]) -> io::Result<usize> {
Err(io::Error::other("pipe broke"))
}
}
#[test]
fn a_failing_stdin_pipe_is_a_stdin_fault() {
let mut stdout = Vec::new();
let err = run(&mut FailingReader, &mut stdout).unwrap_err();
assert_eq!(err.to_string(), "read input from stdin: pipe broke");
}
struct FailingWriter;
impl Write for FailingWriter {
fn write(&mut self, _buf: &[u8]) -> io::Result<usize> {
Err(io::Error::other("stdout gone"))
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
#[test]
fn a_failing_stdout_pipe_is_a_stdout_fault_after_the_patch_applied() {
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("new.txt");
let body = format!("*** Add File: {}\n+hi", path.display());
let input = serde_json::json!({ "input": envelope(&body) }).to_string();
let mut stdin = Cursor::new(input.into_bytes());
let err = run(&mut stdin, &mut FailingWriter).unwrap_err();
assert_eq!(err.to_string(), "write to stdout: stdout gone");
assert!(path.exists());
FailingWriter.flush().unwrap();
}