//! 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")); }