diff --git a/src/main.rs b/src/main.rs index 05e4280..45cfdc9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -56,6 +56,10 @@ enum Commands { #[arg(long)] entry: Option, + /// Output machine-readable JSON + #[arg(long)] + json: bool, + /// Max packages to show in diff output (-1 for all) #[arg(long, default_value_t = report::DEFAULT_TOP, allow_hyphen_values = true)] limit: i32, @@ -233,9 +237,10 @@ fn run(command: Commands, no_color: bool, sc: report::StderrColor) -> Result run_diff(a, b, entry, limit, quiet, color, sc).map(|()| ExitCode::SUCCESS), + } => run_diff(a, b, entry, json, limit, quiet, color, sc).map(|()| ExitCode::SUCCESS), Commands::Packages(ref args) => run_packages(args, color, sc).map(|()| ExitCode::SUCCESS), @@ -309,11 +314,7 @@ fn run_trace(args: TraceArgs, color: bool, sc: report::StderrColor) -> Result Result Result Result Result t) { let kind = if args.include_dynamic { @@ -410,6 +400,7 @@ fn handle_trace_diff( opts: &query::TraceOptions, no_cache: bool, limit: i32, + json: bool, color: bool, sc: report::StderrColor, ) -> Result<(), Error> { @@ -438,7 +429,7 @@ fn handle_trace_diff( let diff_output = query::diff_snapshots(&result.to_snapshot(entry_rel), &diff_snapshot); let report = report::DiffReport::from_diff(&diff_output, entry_rel, &diff_snapshot.entry, limit); - print!("{}", report.to_terminal(color)); + report::emit(json, || report.to_json(), || report.to_terminal(color)); Ok(()) } @@ -470,11 +461,7 @@ fn run_packages(args: &PackagesArgs, color: bool, sc: report::StderrColor) -> Re } let report = session.packages_report(args.top); - if args.json { - println!("{}", report.to_json()); - } else { - print!("{}", report.to_terminal(color)); - } + report::emit(args.json, || report.to_json(), || report.to_terminal(color)); Ok(()) } @@ -526,6 +513,7 @@ fn run_diff( a: String, b: Option, entry: Option, + json: bool, limit: i32, quiet: bool, color: bool, @@ -568,13 +556,13 @@ fn run_diff( let wt_snap = build_snapshot_from_working_tree(entry_path, quiet, sc)?; let wt_label = wt_snap.entry.clone(); return finish_diff( - &snap_a, &label_a, &wt_snap, &wt_label, limit, color, start, quiet, sc, + &snap_a, &label_a, &wt_snap, &wt_label, json, limit, color, start, quiet, sc, ); } }; finish_diff( - &snap_a, &label_a, &snap_b, &label_b, limit, color, start, quiet, sc, + &snap_a, &label_a, &snap_b, &label_b, json, limit, color, start, quiet, sc, ) } @@ -584,6 +572,7 @@ fn finish_diff( label_a: &str, snap_b: &query::TraceSnapshot, label_b: &str, + json: bool, limit: i32, color: bool, start: Instant, @@ -592,7 +581,7 @@ fn finish_diff( ) -> Result<(), Error> { let diff_output = query::diff_snapshots(snap_a, snap_b); let report = report::DiffReport::from_diff(&diff_output, label_a, label_b, limit); - print!("{}", report.to_terminal(color)); + report::emit(json, || report.to_json(), || report.to_terminal(color)); if !quiet { eprintln!( "\n{} in {:.1}ms", diff --git a/src/report.rs b/src/report.rs index f46e991..9b3d84c 100644 --- a/src/report.rs +++ b/src/report.rs @@ -360,6 +360,24 @@ pub struct PackageListEntry { pub file_count: u32, } +// --------------------------------------------------------------------------- +// Emit helper — centralizes --json dispatch +// --------------------------------------------------------------------------- + +/// Emit a report to stdout, choosing JSON or terminal format. +/// +/// Centralizes the `--json` / terminal dispatch so every CLI output path +/// goes through a single function. New output paths that forget the json +/// flag will be caught in code review: the pattern is always +/// `report::emit(json, ...)` rather than ad-hoc if/else. +pub fn emit(json: bool, json_fn: impl FnOnce() -> String, terminal_fn: impl FnOnce() -> String) { + if json { + println!("{}", json_fn()); + } else { + print!("{}", terminal_fn()); + } +} + // --------------------------------------------------------------------------- // Report rendering // --------------------------------------------------------------------------- diff --git a/tests/cli.rs b/tests/cli.rs index aff8c93..6543c03 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -307,6 +307,57 @@ fn unsupported_file_type() { .stderr(predicate::str::contains("unsupported file type")); } +// --- diff --json --- + +#[test] +fn trace_diff_json() { + let p = common::TestProject::new(); + let output = chainsaw() + .args(["trace", "--diff"]) + .arg(p.root().join("b.ts")) + .args(["--json", "--no-cache"]) + .arg(&p.entry) + .output() + .unwrap(); + assert!(output.status.success()); + let v: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); + assert!(v["weight_delta"].is_number()); + assert!(v["entry_a"].is_string()); + assert!(v["entry_b"].is_string()); +} + +#[test] +fn diff_snapshot_json() { + let p = common::TestProject::new(); + // Save two snapshots from different entry points. + let snap_a = p.root().join("snap_a.json"); + let snap_b = p.root().join("snap_b.json"); + chainsaw() + .args(["trace", "--save", snap_a.to_str().unwrap(), "--no-cache"]) + .arg(&p.entry) + .assert() + .success(); + chainsaw() + .args(["trace", "--save", snap_b.to_str().unwrap(), "--no-cache"]) + .arg(p.root().join("b.ts")) + .assert() + .success(); + // diff subcommand with --json + let output = chainsaw() + .args([ + "diff", + snap_a.to_str().unwrap(), + snap_b.to_str().unwrap(), + "--json", + ]) + .output() + .unwrap(); + assert!(output.status.success()); + let v: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); + assert!(v["weight_delta"].is_number()); + assert!(v["entry_a"].is_string()); +} + // --- quiet flag --- #[test]