Files

248 lines
6.4 KiB
Rust

//! Integration tests for the `cjson` command.
use std::{
fs,
path::PathBuf,
sync::atomic::{AtomicU64, Ordering},
};
use assert_cmd::Command;
use predicates::prelude::*;
use serde_json::Value;
fn cargo_command() -> Command {
Command::cargo_bin("cjson").expect("binary")
}
fn cargo_binary() -> PathBuf {
assert_cmd::cargo::cargo_bin("cjson")
}
fn powershell_command(script: String) -> Command {
let mut command = Command::new("pwsh");
command.args(["-NoProfile", "-Command"]).arg(script);
command
}
fn ps_quote(value: impl std::fmt::Display) -> String {
format!("'{}'", value.to_string().replace('\'', "''"))
}
fn workspace_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("..")
}
fn fixture(path: &str) -> PathBuf {
let fixture = workspace_root().join("fixtures").join(path);
assert!(
fixture.exists(),
"missing fixture `{path}` at {}",
fixture.display()
);
fixture
}
fn json_stdout(output: &[u8]) -> Value {
serde_json::from_slice(output).unwrap_or_else(|error| {
panic!(
"stdout should be valid JSON: {error}\n{}",
String::from_utf8_lossy(output)
)
})
}
struct TempTestDir {
path: PathBuf,
}
impl TempTestDir {
fn path(&self) -> &std::path::Path {
&self.path
}
}
impl Drop for TempTestDir {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.path);
}
}
fn temp_test_dir(name: &str) -> TempTestDir {
static NEXT_ID: AtomicU64 = AtomicU64::new(0);
loop {
let unique = NEXT_ID.fetch_add(1, Ordering::Relaxed);
let path =
std::env::temp_dir().join(format!("cjson-{}-{name}-{unique}", std::process::id()));
match fs::create_dir(&path) {
Ok(()) => return TempTestDir { path },
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {}
Err(error) => panic!("temp test dir: {error}"),
}
}
}
#[test]
fn compacts_json_file_in_text_mode() {
let output = cargo_command()
.arg(fixture("cjson/sample.json"))
.assert()
.success()
.get_output()
.stdout
.clone();
let compacted: Value = json_stdout(&output);
assert_eq!(
compacted["name"],
"Ada",
"stdout={}",
String::from_utf8_lossy(&output)
);
assert_eq!(
compacted["a"]["x"],
1,
"stdout={}",
String::from_utf8_lossy(&output)
);
}
#[test]
fn sort_keys_reorders_objects_recursively() {
cargo_command()
.arg("--sort-keys")
.write_stdin(
"{\"z\":3,\"a\":{\"y\":2,\"x\":1},\"items\":[{\"b\":2,\"a\":1}],\"name\":\"Ada\"}",
)
.assert()
.success()
.stdout(
"{\"a\":{\"x\":1,\"y\":2},\"items\":[{\"a\":1,\"b\":2}],\"name\":\"Ada\",\"z\":3}\n",
);
}
#[test]
fn explicit_path_wins_over_piped_stdin_noise() {
cargo_command()
.arg(fixture("cjson/sample.json"))
.write_stdin("not json from upstream pipeline")
.assert()
.success()
.stdout(predicate::str::contains("\"name\":\"Ada\""));
}
#[test]
fn json_wrapper_reports_jsonl_documents() {
let output = cargo_command()
.arg("--input-format")
.arg("jsonl")
.arg("--sort-keys")
.arg("--json")
.arg(fixture("cjson/records.jsonl"))
.assert()
.success()
.get_output()
.stdout
.clone();
let payload = json_stdout(&output);
assert_eq!(
payload["format"],
"jsonl",
"stdout={}",
String::from_utf8_lossy(&output)
);
assert_eq!(
payload["documents"],
2,
"stdout={}",
String::from_utf8_lossy(&output)
);
assert!(
payload["text"]
.as_str()
.is_some_and(|text| text.contains("\"event\":\"login\",\"ok\":true")),
"stdout={}",
String::from_utf8_lossy(&output)
);
}
#[test]
fn auto_mode_treats_explicit_jsonl_and_ndjson_paths_as_line_streams() {
let temp = temp_test_dir("auto-jsonl-paths");
let jsonl = temp.path().join("single.jsonl");
let ndjson = temp.path().join("single.ndjson");
fs::write(&jsonl, "{\"ok\":true}\n").expect("jsonl fixture");
fs::write(&ndjson, "{\"ok\":true}\n").expect("ndjson fixture");
for path in [&jsonl, &ndjson] {
let mut command = cargo_command();
command
.arg("--json")
.arg(path)
.assert()
.success()
.stdout(predicate::str::contains("\"format\":\"jsonl\""))
.stdout(predicate::str::contains("\"documents\":1"));
}
}
#[test]
fn supports_powershell_pipeline() {
let binary = cargo_binary();
let input = fixture("cjson/records.jsonl");
let script = format!(
"[System.IO.File]::ReadLines({}) | & {} --input-format jsonl --sort-keys",
ps_quote(input.display()),
ps_quote(binary.display())
);
powershell_command(script)
.assert()
.success()
.stdout(predicate::str::contains(
"{\"event\":\"login\",\"ok\":true}",
))
.stdout(predicate::str::contains(
"{\"event\":\"logout\",\"ok\":false}",
));
}
#[test]
fn accepts_single_stdin_path_stream_in_lines_mode() {
cargo_command()
.args(["--input-format", "lines"])
.write_stdin(format!("{}\n", fixture("cjson/sample.json").display()))
.assert()
.success()
.stdout(predicate::str::contains("\"name\":\"Ada\""));
}
#[test]
fn lines_mode_accepts_windows_paths_with_quotes_and_spaces() {
let temp = temp_test_dir("quoted path");
let path = temp.path().join("Ada's sample.json");
fs::write(&path, "{\"name\":\"Ada\",\"ok\":true}\n").expect("quoted path fixture");
cargo_command()
.args(["--input-format", "lines"])
.write_stdin(format!("{}\n", path.display()))
.assert()
.success()
.stdout(predicate::str::contains("\"name\":\"Ada\""));
}
#[test]
fn help_includes_examples_and_sort_keys_flag() {
cargo_command()
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("--sort-keys"))
.stdout(predicate::str::contains(
"bat --style=plain --paging=never .\\fixtures\\cjson\\records.jsonl",
))
.stdout(predicate::str::contains("ConvertFrom-Json"));
}