diff --git a/Cargo.lock b/Cargo.lock index a195f07..333150c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -140,7 +140,7 @@ dependencies = [ [[package]] name = "cargo-bless" -version = "0.1.3" +version = "0.1.4" dependencies = [ "anyhow", "assert_cmd", diff --git a/Cargo.toml b/Cargo.toml index 02e09dc..709db1c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,9 @@ [package] name = "cargo-bless" -version = "0.1.3" +version = "0.1.4" edition = "2021" rust-version = "1.80" +default-run = "cargo-bless" description = "Modernize your Rust dependencies with blessed.rs + live intel" license = "MIT" repository = "https://github.com/Ruffian-L/cargo-bless" diff --git a/README.md b/README.md index b6dad86..76ddabc 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ A Cargo subcommand that checks your dependencies against [blessed.rs](https://bl - Scans your `Cargo.toml` dependency tree (direct + transitive, with features) - Matches against a built-in rule database sourced from blessed.rs - Detects single-crate replacements _and_ combo optimizations (e.g. dropping `serde_json` when `reqwest` has the `json` feature) -- Runs a built-in bullshit detector code audit for suspicious Rust complexity patterns +- Optionally runs a built-in bullshit detector code audit for suspicious Rust complexity patterns - Fetches live metadata from crates.io (latest version, downloads) and GitHub (last push, archived status) - Optionally applies safe fixes to your `Cargo.toml` with `--fix` (preview first with `--dry-run`) @@ -47,7 +47,7 @@ cargo bless --fix # apply changes (creates .bak backup) cargo bless --update-rules # fetch latest rules from blessed.rs cargo bless --json # output suggestions as JSON cargo bless --offline # skip network calls, use local cache only -cargo bless --no-audit-code # skip the default code audit +cargo bless --audit-code # include code audit in the main report ``` ### CLI Flags @@ -56,8 +56,7 @@ cargo bless --no-audit-code # skip the default code audit |------|-------------| | `--fix` | Apply auto-fixable suggestions to Cargo.toml | | `--dry-run` | Preview changes without writing (use with `--fix`) | -| `--audit-code` | Explicitly run the bullshit detector code audit | -| `--no-audit-code` | Skip the default bullshit detector code audit | +| `--audit-code` | Include the bullshit detector code audit in the main report | | `--diff` | With `cargo bless bs`, audit only changed lines from `git diff HEAD` | | `--verbose` | Show every code-audit finding instead of the top findings summary | | `--json` | Output suggestions as JSON array (for CI/pipelines) | @@ -94,9 +93,9 @@ Or pass a custom path: `cargo bless --policy=custom-bless.toml` ## Example ``` -$ cargo bless +$ cargo bless --audit-code -🔥 cargo-bless v0.1.3 +🔥 cargo-bless v0.1.4 📋 Scanning dependencies... @@ -184,7 +183,7 @@ Before any write, `--fix` creates a `Cargo.toml.bak` backup and runs `cargo upda 2. Rules from `data/suggestions.json` are matched against direct deps (single-crate and combo patterns) 3. `crates_io_api::SyncClient` fetches live metadata (cached to `~/.cache/cargo-bless/` with 1-hour TTL) 4. `reqwest` checks GitHub for `pushed_at`, `archived`, and star count -5. The bullshit detector scans Rust files under `src`, `tests`, `examples`, and `benches` for static complexity patterns +5. With `--audit-code` or `cargo bless bs`, the bullshit detector scans Rust files under `src`, `tests`, `examples`, and `benches` for static complexity patterns 6. `toml_edit` applies fixes while preserving comments and formatting Network calls are non-fatal — if you're offline, the rule-based report still works. diff --git a/src/cli.rs b/src/cli.rs index 7b69080..45414d9 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,4 +1,4 @@ -use clap::{Parser, Subcommand}; +use clap::{Args, Parser, Subcommand}; /// Cargo subcommand wrapper — invoked as `cargo bless`. #[derive(Parser, Debug)] @@ -11,12 +11,27 @@ pub struct Cli { #[derive(Subcommand, Debug)] pub enum Commands { /// Bless your dependencies — modernize, optimize, and stay current. - Bless(BlessOpts), + Bless(BlessCommand), /// Run only the bullshit detector code audit. Bs(CodeAuditOpts), } -#[derive(clap::Args, Debug)] +#[derive(Args, Debug)] +pub struct BlessCommand { + #[command(flatten)] + pub opts: BlessOpts, + + #[command(subcommand)] + pub command: Option, +} + +#[derive(Subcommand, Debug)] +pub enum BlessSubcommand { + /// Run only the bullshit detector code audit. + Bs(CodeAuditOpts), +} + +#[derive(Args, Debug)] pub struct BlessOpts { /// Apply suggested changes to Cargo.toml (creates .bak backup first). #[arg(long)] @@ -43,7 +58,7 @@ pub struct BlessOpts { pub audit_code: bool, /// Skip the default bullshit detector code audit. - #[arg(long)] + #[arg(long, hide = true)] pub no_audit_code: bool, /// Output suggestions in JSON format. @@ -80,7 +95,7 @@ pub struct BlessOpts { pub verbose: bool, } -#[derive(clap::Args, Debug)] +#[derive(Args, Debug)] pub struct CodeAuditOpts { /// Path to the Cargo.toml whose source tree should be audited. #[arg(long, value_name = "PATH")] diff --git a/src/fix.rs b/src/fix.rs index b2f0fec..8735e71 100644 --- a/src/fix.rs +++ b/src/fix.rs @@ -9,7 +9,7 @@ use std::fs; use std::path::Path; -use std::process::Command; +use std::process::{Command, Output}; use anyhow::{Context, Result}; use colored::*; @@ -111,64 +111,21 @@ pub fn apply(suggestions: &[Suggestion], manifest_path: &Path, dry_run: bool) -> fs::write(manifest_path, &edited) .with_context(|| format!("failed to write {}", manifest_path.display()))?; - // Run cargo update - println!("{}", "📦 Running cargo update...".dimmed()); - let status = Command::new("cargo") - .arg("update") - .arg("--manifest-path") - .arg(manifest_path) - .status(); - - match status { - Ok(s) if s.success() => { - println!("{}", "✅ cargo update completed successfully.".green()); - } - Ok(s) => { - println!( - "{}", - format!("⚠️ cargo update exited with: {}", s).yellow() - ); - } - Err(e) => { - println!( - "{}", - format!("⚠️ Failed to run cargo update: {}", e).yellow() - ); - } - } + run_cargo_validation( + "cargo update", + "📦 Running cargo update...", + "✅ cargo update completed successfully.", + &["update", "--manifest-path"], + manifest_path, + ); - // Run cargo check to validate the fix didn't break compilation - println!("{}", "🔍 Running cargo check...".dimmed()); - let check_status = Command::new("cargo") - .arg("check") - .arg("--manifest-path") - .arg(manifest_path) - .status(); - - match check_status { - Ok(s) if s.success() => { - println!( - "{}", - "✅ cargo check passed — project still compiles.".green() - ); - } - Ok(s) => { - println!( - "{}", - format!( - "⚠️ cargo check failed (exit: {}). You may need to update source code.", - s - ) - .yellow() - ); - } - Err(e) => { - println!( - "{}", - format!("⚠️ Failed to run cargo check: {}", e).yellow() - ); - } - } + run_cargo_validation( + "cargo check", + "🔍 Running cargo check...", + "✅ cargo check passed — project still compiles.", + &["check", "--manifest-path"], + manifest_path, + ); println!(); if !applied.is_empty() { @@ -190,6 +147,53 @@ pub fn apply(suggestions: &[Suggestion], manifest_path: &Path, dry_run: bool) -> Ok(FixResult { applied, skipped }) } +fn run_cargo_validation( + command_name: &str, + start_message: &str, + success_message: &str, + args: &[&str], + manifest_path: &Path, +) { + println!("{}", start_message.dimmed()); + let output = Command::new("cargo").args(args).arg(manifest_path).output(); + + match output { + Ok(output) if output.status.success() => { + println!("{}", success_message.green()); + } + Ok(output) => { + println!( + "{}", + format!( + "⚠️ {command_name} exited with {}. Run `{command_name} --manifest-path {}` for details.", + output.status, + manifest_path.display() + ) + .yellow() + ); + if let Some(summary) = validation_summary(&output) { + println!(" {}", summary.dimmed()); + } + } + Err(err) => { + println!( + "{}", + format!("⚠️ Failed to run {command_name}: {err}").yellow() + ); + } + } +} + +fn validation_summary(output: &Output) -> Option { + let stderr = String::from_utf8_lossy(&output.stderr); + stderr + .lines() + .chain(String::from_utf8_lossy(&output.stdout).lines()) + .map(str::trim) + .find(|line| !line.is_empty()) + .map(str::to_string) +} + /// Apply a single suggestion to the TOML document. /// Returns a description of what was done on success. fn apply_single(doc: &mut DocumentMut, suggestion: &Suggestion) -> Result { diff --git a/src/main.rs b/src/main.rs index 664eafb..85d1e95 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,183 +11,190 @@ fn main() -> Result<()> { let args = cli::Cli::parse(); match args.command { - cli::Commands::Bless(opts) => { - reject_unfinished_flags(&opts)?; - let manifest = opts.manifest_path.as_deref(); - let run_code_audit = !opts.no_audit_code || opts.audit_code; - let policy = load_policy(opts.policy.as_deref(), manifest)?; - let code_audit_config = cargo_bless::code_audit::config_from_policy(policy.as_ref()); - - if opts.json { - let deps = cargo_bless::parser::get_deps(manifest)?; - let rules = cargo_bless::suggestions::load_rules(); - let suggestions = apply_policy( - cargo_bless::suggestions::analyze(manifest, &deps, &rules), - policy.as_ref(), - ); - let code_audit = if run_code_audit { - Some(cargo_bless::code_audit::scan_project( - manifest, - &code_audit_config, - )?) - } else { - None - }; - cargo_bless::output::render_json_report(&suggestions, code_audit.as_ref()); - return Ok(()); - } + cli::Commands::Bless(command) => match command.command { + Some(cli::BlessSubcommand::Bs(opts)) => run_code_audit_command(opts), + None => run_bless_command(command.opts), + }, + cli::Commands::Bs(opts) => run_code_audit_command(opts), + } +} - println!("🔥 cargo-bless v{}", env!("CARGO_PKG_VERSION")); - println!(); +fn run_bless_command(opts: cli::BlessOpts) -> Result<()> { + reject_invalid_flag_combinations(&opts)?; + reject_unfinished_flags(&opts)?; + let manifest = opts.manifest_path.as_deref(); + let run_code_audit = opts.audit_code; + let policy = load_policy(opts.policy.as_deref(), manifest)?; + let code_audit_config = cargo_bless::code_audit::config_from_policy(policy.as_ref()); - // Handle --update-rules before the main pipeline - if opts.update_rules { - cargo_bless::updater::update_rules()?; - println!(); - println!("Rules updated. Run `cargo bless` to use them."); - return Ok(()); - } - if opts.fix { - if opts.dry_run { - println!("🔍 Dry-run mode — previewing changes (no files will be modified)"); - } else { - println!("🔧 Fix mode — applying safe changes"); + if opts.json { + let deps = cargo_bless::parser::get_deps(manifest)?; + let rules = cargo_bless::suggestions::load_rules(); + let suggestions = apply_policy( + cargo_bless::suggestions::analyze(manifest, &deps, &rules), + policy.as_ref(), + ); + let code_audit = if run_code_audit { + Some(cargo_bless::code_audit::scan_project( + manifest, + &code_audit_config, + )?) + } else { + None + }; + cargo_bless::output::render_json_report(&suggestions, code_audit.as_ref()); + return Ok(()); + } + + println!("🔥 cargo-bless v{}", env!("CARGO_PKG_VERSION")); + println!(); + + // Handle --update-rules before the main pipeline + if opts.update_rules { + cargo_bless::updater::update_rules()?; + println!(); + println!("Rules updated. Run `cargo bless` to use them."); + return Ok(()); + } + if opts.fix { + if opts.dry_run { + println!("🔍 Dry-run mode — previewing changes (no files will be modified)"); + } else { + println!("🔧 Fix mode — applying safe changes"); + } + println!(); + } + + println!("📋 Scanning dependencies..."); + println!(); + + // Parse the dep tree + let deps = cargo_bless::parser::get_deps(manifest)?; + let (project_name, project_version) = cargo_bless::parser::get_project_info(manifest)?; + + let direct: Vec<_> = deps.iter().filter(|d| d.is_direct).collect(); + let transitive: Vec<_> = deps.iter().filter(|d| !d.is_direct).collect(); + + // Print direct dependencies + println!( + "{}", + format!("📦 Direct dependencies ({})", direct.len()).bold() + ); + for dep in &direct { + let features_str = if dep.enabled_features.is_empty() { + String::new() + } else { + format!(" [{}]", dep.enabled_features.join(", ")) + }; + println!( + " {} {} {}{}", + "•".green(), + dep.name.bold(), + dep.version.dimmed(), + features_str.dimmed() + ); + } + + println!(); + println!( + "{}", + format!("📎 Transitive dependencies ({})", transitive.len()).dimmed() + ); + + println!(); + println!( + "{}", + format!("Found {} direct deps, {} total.", direct.len(), deps.len()).bold() + ); + + // Suggestion engine: load rules → analyze + println!(); + let rules = cargo_bless::suggestions::load_rules(); + let suggestions = apply_policy( + cargo_bless::suggestions::analyze(manifest, &deps, &rules), + policy.as_ref(), + ); + + // Live intelligence: fetch metadata for flagged deps (non-fatal) + let effective_offline = opts.offline + || policy + .as_ref() + .is_some_and(|policy| policy.settings.offline); + let intel = if !effective_offline && !suggestions.is_empty() { + // Collect unique crate names from suggestions + let crate_names: Vec<&str> = suggestions + .iter() + .flat_map(|s| s.current.split('+')) + .collect(); + + match cargo_bless::intel::IntelClient::new() { + Ok(client) => { + println!("{}", "🌐 Fetching live intelligence...".dimmed()); + let result = client.fetch_bulk_intel(&crate_names); + if result.is_empty() && !crate_names.is_empty() { + println!( + "{}", + "⚠️ Live data unavailable (offline or rate-limited)" + .yellow() + .dimmed() + ); } println!(); + result } - - println!("📋 Scanning dependencies..."); - println!(); - - // Parse the dep tree - let deps = cargo_bless::parser::get_deps(manifest)?; - let (project_name, project_version) = cargo_bless::parser::get_project_info(manifest)?; - - let direct: Vec<_> = deps.iter().filter(|d| d.is_direct).collect(); - let transitive: Vec<_> = deps.iter().filter(|d| !d.is_direct).collect(); - - // Print direct dependencies - println!( - "{}", - format!("📦 Direct dependencies ({})", direct.len()).bold() - ); - for dep in &direct { - let features_str = if dep.enabled_features.is_empty() { - String::new() - } else { - format!(" [{}]", dep.enabled_features.join(", ")) - }; + Err(_) => { println!( - " {} {} {}{}", - "•".green(), - dep.name.bold(), - dep.version.dimmed(), - features_str.dimmed() + "{}", + "⚠️ Could not initialize live intelligence (continuing without)" + .yellow() + .dimmed() ); - } - - println!(); - println!( - "{}", - format!("📎 Transitive dependencies ({})", transitive.len()).dimmed() - ); - - println!(); - println!( - "{}", - format!("Found {} direct deps, {} total.", direct.len(), deps.len()).bold() - ); - - // Suggestion engine: load rules → analyze - println!(); - let rules = cargo_bless::suggestions::load_rules(); - let suggestions = apply_policy( - cargo_bless::suggestions::analyze(manifest, &deps, &rules), - policy.as_ref(), - ); - - // Live intelligence: fetch metadata for flagged deps (non-fatal) - let intel = if !opts.offline && !suggestions.is_empty() { - // Collect unique crate names from suggestions - let crate_names: Vec<&str> = suggestions - .iter() - .flat_map(|s| s.current.split('+')) - .collect(); - - match cargo_bless::intel::IntelClient::new() { - Ok(client) => { - println!("{}", "🌐 Fetching live intelligence...".dimmed()); - let result = client.fetch_bulk_intel(&crate_names); - if result.is_empty() && !crate_names.is_empty() { - println!( - "{}", - "⚠️ Live data unavailable (offline or rate-limited)" - .yellow() - .dimmed() - ); - } - println!(); - result - } - Err(_) => { - println!( - "{}", - "⚠️ Could not initialize live intelligence (continuing without)" - .yellow() - .dimmed() - ); - println!(); - HashMap::new() - } - } - } else { + println!(); HashMap::new() - }; - - // Render the report - cargo_bless::output::render_report( - &project_name, - &project_version, - &suggestions, - &intel, - ); - - if run_code_audit { - let report = cargo_bless::code_audit::scan_project(manifest, &code_audit_config)?; - cargo_bless::output::render_code_audit_report(&report, opts.verbose); } + } + } else { + HashMap::new() + }; - // Apply fixes if --fix was passed - if opts.fix && !suggestions.is_empty() { - println!(); - let manifest = opts - .manifest_path - .unwrap_or_else(|| std::path::PathBuf::from("Cargo.toml")); - cargo_bless::fix::apply(&suggestions, &manifest, opts.dry_run)?; - } + // Render the report + cargo_bless::output::render_report(&project_name, &project_version, &suggestions, &intel); - Ok(()) - } - cli::Commands::Bs(opts) => { - let manifest = opts.manifest_path.as_deref(); - let policy = load_policy(opts.policy.as_deref(), manifest)?; - let code_audit_config = cargo_bless::code_audit::config_from_policy(policy.as_ref()); - let report = if opts.diff { - cargo_bless::code_audit::scan_git_diff(manifest, &code_audit_config)? - } else { - cargo_bless::code_audit::scan_project(manifest, &code_audit_config)? - }; + if run_code_audit { + let report = cargo_bless::code_audit::scan_project(manifest, &code_audit_config)?; + cargo_bless::output::render_code_audit_report(&report, opts.verbose); + } - if opts.json { - cargo_bless::output::render_json_report(&[], Some(&report)); - } else { - println!("🔥 cargo-bless v{}", env!("CARGO_PKG_VERSION")); - cargo_bless::output::render_code_audit_report(&report, opts.verbose); - } + // Apply fixes if --fix was passed + if opts.fix && !suggestions.is_empty() { + println!(); + let manifest = opts + .manifest_path + .unwrap_or_else(|| std::path::PathBuf::from("Cargo.toml")); + cargo_bless::fix::apply(&suggestions, &manifest, opts.dry_run)?; + } - Ok(()) - } + Ok(()) +} + +fn run_code_audit_command(opts: cli::CodeAuditOpts) -> Result<()> { + let manifest = opts.manifest_path.as_deref(); + let policy = load_policy(opts.policy.as_deref(), manifest)?; + let code_audit_config = cargo_bless::code_audit::config_from_policy(policy.as_ref()); + let report = if opts.diff { + cargo_bless::code_audit::scan_git_diff(manifest, &code_audit_config)? + } else { + cargo_bless::code_audit::scan_project(manifest, &code_audit_config)? + }; + + if opts.json { + cargo_bless::output::render_json_report(&[], Some(&report)); + } else { + println!("🔥 cargo-bless v{}", env!("CARGO_PKG_VERSION")); + cargo_bless::output::render_code_audit_report(&report, opts.verbose); } + + Ok(()) } fn load_policy( @@ -225,6 +232,20 @@ fn apply_policy( } } +fn reject_invalid_flag_combinations(opts: &cli::BlessOpts) -> Result<()> { + if opts.dry_run && !opts.fix { + bail!("--dry-run requires --fix"); + } + if opts.json && opts.fix { + bail!("--json cannot be combined with --fix"); + } + if opts.json && opts.update_rules { + bail!("--json cannot be combined with --update-rules"); + } + + Ok(()) +} + fn reject_unfinished_flags(opts: &cli::BlessOpts) -> Result<()> { if opts.llm { bail!("--llm is not implemented in cargo-bless yet"); diff --git a/tests/integration.rs b/tests/integration.rs index 77c55c6..5713bdb 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -69,6 +69,39 @@ fn test_unfinished_flag_exits_nonzero() { .stderr(predicate::str::contains("--fail-on is not implemented")); } +#[test] +fn test_dry_run_without_fix_exits_nonzero() { + cargo_bless_cmd() + .arg("--dry-run") + .assert() + .failure() + .stderr(predicate::str::contains("--dry-run requires --fix")); +} + +#[test] +fn test_json_fix_combination_exits_nonzero() { + cargo_bless_cmd() + .arg("--json") + .arg("--fix") + .assert() + .failure() + .stderr(predicate::str::contains( + "--json cannot be combined with --fix", + )); +} + +#[test] +fn test_json_update_rules_combination_exits_nonzero() { + cargo_bless_cmd() + .arg("--json") + .arg("--update-rules") + .assert() + .failure() + .stderr(predicate::str::contains( + "--json cannot be combined with --update-rules", + )); +} + #[test] fn test_explicit_missing_policy_exits_nonzero() { cargo_bless_cmd() @@ -237,7 +270,7 @@ serde_derive = { path = "crates/serde_derive" } } #[test] -fn test_bless_reports_code_audit_by_default() { +fn test_bless_skips_code_audit_by_default() { use std::fs; use tempfile::TempDir; @@ -277,6 +310,54 @@ edition = "2021" let output = cmd.output().expect("run cargo-bless"); let stdout = String::from_utf8_lossy(&output.stdout); + assert!(output.status.success(), "should exit 0: {}", stdout); + assert!(!stdout.contains("Bullshit detector code audit")); + assert!(!stdout.contains("unwrap abuse")); + assert!(!stdout.contains("sleep abuse")); +} + +#[test] +fn test_audit_code_flag_runs_code_audit() { + use std::fs; + use tempfile::TempDir; + + let tmp = TempDir::new().expect("temp dir"); + let manifest = tmp.path().join("Cargo.toml"); + + fs::write( + &manifest, + r#"[package] +name = "test-audit-code" +version = "0.1.0" +edition = "2021" + +[dependencies] +"#, + ) + .expect("write Cargo.toml"); + + fs::create_dir_all(tmp.path().join("src")).expect("create src"); + fs::write( + tmp.path().join("src/main.rs"), + r#"fn main() { + let value = std::env::var("NOPE").unwrap(); + std::thread::sleep(std::time::Duration::from_millis(10)); + println!("{}", value); +} +"#, + ) + .expect("write main.rs"); + + let mut cmd = Command::cargo_bin("cargo-bless").expect("binary should exist"); + cmd.arg("bless") + .arg("--manifest-path") + .arg(&manifest) + .arg("--offline") + .arg("--audit-code"); + + let output = cmd.output().expect("run cargo-bless"); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(output.status.success(), "should exit 0: {}", stdout); assert!(stdout.contains("Bullshit detector code audit")); assert!(stdout.contains("unwrap abuse")); @@ -284,7 +365,7 @@ edition = "2021" } #[test] -fn test_no_audit_code_skips_code_audit() { +fn test_no_audit_code_remains_accepted_as_noop() { let output = cargo_bless_cmd() .arg("--offline") .arg("--no-audit-code") @@ -297,7 +378,7 @@ fn test_no_audit_code_skips_code_audit() { } #[test] -fn test_bs_subcommand_runs_code_audit_only() { +fn test_direct_bs_subcommand_runs_code_audit_only() { use std::fs; use tempfile::TempDir; @@ -336,7 +417,49 @@ edition = "2021" } #[test] -fn test_json_contains_dependency_and_code_sections() { +fn test_cargo_style_bs_subcommand_runs_code_audit_only() { + use std::fs; + use tempfile::TempDir; + + let tmp = TempDir::new().expect("temp dir"); + let manifest = tmp.path().join("Cargo.toml"); + + fs::write( + &manifest, + r#"[package] +name = "test-cargo-style-bs" +version = "0.1.0" +edition = "2021" + +[dependencies] +"#, + ) + .expect("write Cargo.toml"); + + fs::create_dir_all(tmp.path().join("src")).expect("create src"); + fs::write( + tmp.path().join("src/lib.rs"), + "pub fn bad() { thing().unwrap(); }\n", + ) + .expect("write lib.rs"); + + let mut cmd = Command::cargo_bin("cargo-bless").expect("binary should exist"); + cmd.arg("bless") + .arg("bs") + .arg("--manifest-path") + .arg(&manifest); + + let output = cmd.output().expect("run cargo-bless bless bs"); + let stdout = String::from_utf8_lossy(&output.stdout); + + assert!(output.status.success(), "should exit 0: {}", stdout); + assert!(stdout.contains("Bullshit detector code audit")); + assert!(stdout.contains("unwrap abuse")); + assert!(!stdout.contains("Direct dependencies")); +} + +#[test] +fn test_json_contains_dependency_section_and_null_code_audit_by_default() { use std::fs; use tempfile::TempDir; @@ -373,6 +496,52 @@ lazy_static = "1" let output = cmd.output().expect("run cargo-bless json"); let stdout = String::from_utf8_lossy(&output.stdout); + assert!(output.status.success(), "should exit 0: {}", stdout); + assert!(stdout.contains("\"dependency_suggestions\"")); + assert!(stdout.contains("\"code_audit\": null")); + assert!(stdout.contains("lazy_static")); + assert!(!stdout.contains("UnwrapAbuse")); +} + +#[test] +fn test_json_with_audit_code_contains_code_audit_section() { + use std::fs; + use tempfile::TempDir; + + let tmp = TempDir::new().expect("temp dir"); + let manifest = tmp.path().join("Cargo.toml"); + + fs::write( + &manifest, + r#"[package] +name = "test-json-audit" +version = "0.1.0" +edition = "2021" + +[dependencies] +lazy_static = "1" +"#, + ) + .expect("write Cargo.toml"); + + fs::create_dir_all(tmp.path().join("src")).expect("create src"); + fs::write( + tmp.path().join("src/main.rs"), + "fn main() { thing().unwrap(); }\n", + ) + .expect("write main.rs"); + + let mut cmd = Command::cargo_bin("cargo-bless").expect("binary should exist"); + cmd.arg("bless") + .arg("--manifest-path") + .arg(&manifest) + .arg("--offline") + .arg("--json") + .arg("--audit-code"); + + let output = cmd.output().expect("run cargo-bless json"); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(output.status.success(), "should exit 0: {}", stdout); assert!(stdout.contains("\"dependency_suggestions\"")); assert!(stdout.contains("\"code_audit\""));