diff --git a/src/commands/backup.rs b/src/commands/backup.rs index 336c920bf..1257e2283 100644 --- a/src/commands/backup.rs +++ b/src/commands/backup.rs @@ -467,7 +467,8 @@ impl BackupCmd { write_json_progress_summary(&snap)?; } else if self.json { let mut stdout = std::io::stdout(); - serde_json::to_writer_pretty(&mut stdout, &snap)?; + serde_json::to_writer(&mut stdout, &snap)?; + println!(); } else if self.long { let mut table = table(); diff --git a/src/commands/forget.rs b/src/commands/forget.rs index babe6508f..d92649327 100644 --- a/src/commands/forget.rs +++ b/src/commands/forget.rs @@ -134,7 +134,8 @@ impl ForgetCmd { if self.json { let mut stdout = std::io::stdout(); - serde_json::to_writer_pretty(&mut stdout, &groups)?; + serde_json::to_writer(&mut stdout, &groups)?; + println!(); } else { print_groups(&groups); } diff --git a/src/commands/merge.rs b/src/commands/merge.rs index be2613df1..79f701233 100644 --- a/src/commands/merge.rs +++ b/src/commands/merge.rs @@ -65,7 +65,8 @@ impl MergeCmd { if self.json { let mut stdout = std::io::stdout(); - serde_json::to_writer_pretty(&mut stdout, &snap)?; + serde_json::to_writer(&mut stdout, &snap)?; + println!(); } info!("saved new snapshot as {}.", snap.id); diff --git a/src/commands/repoinfo.rs b/src/commands/repoinfo.rs index 69bd960af..a4aeef567 100644 --- a/src/commands/repoinfo.rs +++ b/src/commands/repoinfo.rs @@ -70,7 +70,8 @@ impl RepoInfoCmd { if self.json { let mut stdout = std::io::stdout(); - serde_json::to_writer_pretty(&mut stdout, &infos)?; + serde_json::to_writer(&mut stdout, &infos)?; + println!(); return Ok(()); } diff --git a/src/commands/snapshots.rs b/src/commands/snapshots.rs index a363b42b1..e7b1b83ca 100644 --- a/src/commands/snapshots.rs +++ b/src/commands/snapshots.rs @@ -94,7 +94,7 @@ impl SnapshotCmd { let mut stdout = std::io::stdout(); if groups.len() == 1 && groups[0].group_key.is_empty() { // we don't use grouping, only output snapshots list - serde_json::to_writer_pretty(&mut stdout, &groups[0].items)?; + serde_json::to_writer(&mut stdout, &groups[0].items)?; } else { #[derive(Serialize, From)] struct SnapshotsGroup { @@ -105,8 +105,9 @@ impl SnapshotCmd { .into_iter() .map(|g| (g.group_key, g.items).into()) .collect(); - serde_json::to_writer_pretty(&mut stdout, &groups)?; + serde_json::to_writer(&mut stdout, &groups)?; } + println!(); return Ok(()); } diff --git a/tests/json_lines.rs b/tests/json_lines.rs new file mode 100644 index 000000000..d0ddb1f4f --- /dev/null +++ b/tests/json_lines.rs @@ -0,0 +1,58 @@ +use assert_cmd::Command; +use rustic_testing::TestResult; +use tempfile::{TempDir, tempdir}; + +fn rustic_runner(temp_dir: &TempDir) -> TestResult { + let mut runner = Command::new(env!("CARGO_BIN_EXE_rustic")); + runner.arg("-r").arg(temp_dir.path().join("repo")).args([ + "--password", + "test", + "--no-progress", + ]); + Ok(runner) +} + +#[test] +fn backup_json_writes_one_snapshot_per_line() -> TestResult<()> { + let temp_dir = tempdir()?; + let first_source = temp_dir.path().join("first-source"); + let second_source = temp_dir.path().join("second-source"); + std::fs::create_dir(&first_source)?; + std::fs::create_dir(&second_source)?; + std::fs::write(first_source.join("first.txt"), "first")?; + std::fs::write(second_source.join("second.txt"), "second")?; + + rustic_runner(&temp_dir)?.arg("init").assert().success(); + + let profile = temp_dir.path().join("two-sources.toml"); + let first_source = serde_json::to_string(&first_source.to_string_lossy())?; + let second_source = serde_json::to_string(&second_source.to_string_lossy())?; + std::fs::write( + &profile, + format!( + "[[backup.snapshots]]\nsources = [{first_source}]\n\n[[backup.snapshots]]\nsources = [{second_source}]\n" + ), + )?; + + let output = rustic_runner(&temp_dir)? + .arg("-P") + .arg(profile) + .args(["backup", "--json"]) + .output()?; + assert!(output.status.success()); + + let stdout = String::from_utf8(output.stdout)?; + assert!( + stdout.ends_with('\n'), + "JSON output is not line-terminated: {stdout}" + ); + let lines: Vec<_> = stdout.lines().collect(); + assert_eq!(lines.len(), 2, "unexpected JSON output: {stdout}"); + let snapshots: Vec = lines + .iter() + .map(|line| serde_json::from_str(line)) + .collect::>()?; + assert_eq!(snapshots.len(), 2); + + Ok(()) +}