use std::{
env, fs,
io::Write,
path::PathBuf,
process::{Command, Stdio},
time::{SystemTime, UNIX_EPOCH},
};
fn bin() -> &'static str {
env!("CARGO_BIN_EXE_jsonrepair")
}
fn temp_path(name: &str) -> PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
env::temp_dir().join(format!(
"jsonrepair-rs-{name}-{}-{nanos}",
std::process::id()
))
}
#[test]
fn repairs_stdin_to_stdout() {
let mut child = Command::new(bin())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
child
.stdin
.as_mut()
.unwrap()
.write_all(b"{name: 'Ada', active: True}")
.unwrap();
let output = child.wait_with_output().unwrap();
assert!(output.status.success(), "{output:?}");
assert_eq!(
String::from_utf8_lossy(&output.stdout),
r#"{"name": "Ada", "active": true}"#
);
assert!(output.stderr.is_empty(), "{output:?}");
}
#[test]
fn repairs_file_to_output_file() {
let input_path = temp_path("input");
let output_path = temp_path("output");
fs::write(&input_path, "{skills: ['Rust',], ok: False}").unwrap();
let output = Command::new(bin())
.arg(&input_path)
.arg("--output")
.arg(&output_path)
.output()
.unwrap();
assert!(output.status.success(), "{output:?}");
assert_eq!(
fs::read_to_string(&output_path).unwrap(),
r#"{"skills": ["Rust"], "ok": false}"#
);
assert!(output.stdout.is_empty(), "{output:?}");
let _ = fs::remove_file(input_path);
let _ = fs::remove_file(output_path);
}
#[test]
fn reports_repair_errors() {
let mut child = Command::new(bin())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
child
.stdin
.as_mut()
.unwrap()
.write_all(br#""\u00""#)
.unwrap();
let output = child.wait_with_output().unwrap();
assert!(!output.status.success(), "{output:?}");
assert!(output.stdout.is_empty(), "{output:?}");
assert!(String::from_utf8_lossy(&output.stderr).contains("JSON repair error"));
}
#[test]
fn repair_error_does_not_truncate_existing_output_file() {
let input_path = temp_path("invalid-input");
let output_path = temp_path("existing-output");
fs::write(&input_path, br#""\u00""#).unwrap();
fs::write(&output_path, "keep me").unwrap();
let output = Command::new(bin())
.arg(&input_path)
.arg("--output")
.arg(&output_path)
.output()
.unwrap();
assert!(!output.status.success(), "{output:?}");
assert!(output.stdout.is_empty(), "{output:?}");
assert!(String::from_utf8_lossy(&output.stderr).contains("JSON repair error"));
assert_eq!(fs::read_to_string(&output_path).unwrap(), "keep me");
let _ = fs::remove_file(input_path);
let _ = fs::remove_file(output_path);
}
#[test]
fn help_documents_exit_codes() -> Result<(), Box<dyn std::error::Error>> {
let output = Command::new(bin()).arg("--help").output()?;
assert!(output.status.success(), "{output:?}");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Exit Codes:"), "{stdout}");
assert!(
stdout.contains(" 2 Command-line usage error."),
"{stdout}"
);
Ok(())
}
#[test]
fn usage_error_exits_2() -> Result<(), Box<dyn std::error::Error>> {
let output = Command::new(bin()).arg("--unknown").output()?;
assert_eq!(output.status.code(), Some(2), "{output:?}");
assert!(output.stdout.is_empty(), "{output:?}");
assert!(String::from_utf8_lossy(&output.stderr).contains("unknown option"));
Ok(())
}
#[test]
fn output_requires_path_exits_2() -> Result<(), Box<dyn std::error::Error>> {
let output = Command::new(bin()).arg("--output").output()?;
assert_eq!(output.status.code(), Some(2), "{output:?}");
assert!(output.stdout.is_empty(), "{output:?}");
assert!(String::from_utf8_lossy(&output.stderr).contains("requires a path"));
Ok(())
}
#[test]
fn dash_reads_stdin_to_output_file() -> Result<(), Box<dyn std::error::Error>> {
let output_path = temp_path("dash-output");
let mut child = Command::new(bin())
.arg("-")
.arg("--output")
.arg(&output_path)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(b"{name: 'Ada', active: True}")?;
} else {
return Err(
std::io::Error::new(std::io::ErrorKind::Other, "stdin pipe was unavailable").into(),
);
}
let output = child.wait_with_output()?;
assert!(output.status.success(), "{output:?}");
assert!(output.stdout.is_empty(), "{output:?}");
assert_eq!(
fs::read_to_string(&output_path)?,
r#"{"name": "Ada", "active": true}"#
);
let _ = fs::remove_file(output_path);
Ok(())
}
#[test]
fn output_equals_writes_file() -> Result<(), Box<dyn std::error::Error>> {
let output_path = temp_path("equals-output");
let output_arg = format!("--output={}", output_path.display());
let mut child = Command::new(bin())
.arg(output_arg)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(b"{skills: ['Rust',], ok: False}")?;
} else {
return Err(
std::io::Error::new(std::io::ErrorKind::Other, "stdin pipe was unavailable").into(),
);
}
let output = child.wait_with_output()?;
assert!(output.status.success(), "{output:?}");
assert!(output.stdout.is_empty(), "{output:?}");
assert_eq!(
fs::read_to_string(&output_path)?,
r#"{"skills": ["Rust"], "ok": false}"#
);
let _ = fs::remove_file(output_path);
Ok(())
}