homeboy 0.136.0

CLI for multi-component deployment and development workflow automation
Documentation
use homeboy::commands::lint::{run as run_lint, LintArgs};
use homeboy::commands::test::{run as run_test, TestArgs};
use homeboy::commands::utils::args::{
    BaselineArgs, ExtensionOverrideArgs, HiddenJsonArgs, PositionalComponentArgs, SettingArgs,
};
use homeboy::commands::GlobalArgs;
use std::fs;
use std::path::Path;

fn write_component(root: &Path, self_checks: &str) {
    fs::write(
        root.join("homeboy.json"),
        format!(
            r#"{{
  "id": "fixture",
  "self_checks": {}
}}"#,
            self_checks
        ),
    )
    .expect("homeboy.json should be written");
}

fn write_script(root: &Path, name: &str, body: &str) {
    let script_dir = root.join("scripts");
    fs::create_dir_all(&script_dir).expect("script dir should be created");
    fs::write(script_dir.join(name), body).expect("script should be written");
}

fn component_args(root: &Path) -> PositionalComponentArgs {
    PositionalComponentArgs {
        component: Some("fixture".to_string()),
        path: Some(root.to_string_lossy().to_string()),
    }
}

fn lint_args(root: &Path) -> LintArgs {
    LintArgs {
        comp: component_args(root),
        extension_override: ExtensionOverrideArgs::default(),
        summary: false,
        file: None,
        glob: None,
        changed_only: false,
        changed_since: None,
        errors_only: false,
        sniffs: None,
        exclude_sniffs: None,
        category: None,
        fix: false,
        setting_args: SettingArgs::default(),
        baseline_args: BaselineArgs::default(),
        _json: HiddenJsonArgs::default(),
        json_summary: false,
    }
}

fn test_args(root: &Path) -> TestArgs {
    TestArgs {
        comp: component_args(root),
        extension_override: ExtensionOverrideArgs::default(),
        skip_lint: false,
        coverage: false,
        coverage_min: None,
        baseline_args: BaselineArgs::default(),
        analyze: false,
        drift: false,
        write: false,
        since: "HEAD~10".to_string(),
        changed_since: None,
        setting_args: SettingArgs::default(),
        args: Vec::new(),
        _json: HiddenJsonArgs::default(),
        json_summary: false,
    }
}

#[test]
fn lint_runs_declared_self_check_without_extensions() {
    let dir = tempfile::tempdir().expect("temp dir");
    write_component(dir.path(), r#"{ "lint": ["sh scripts/lint.sh"] }"#);
    write_script(dir.path(), "lint.sh", "printf 'lint self-check ran\\n'\n");

    let (output, exit_code) =
        run_lint(lint_args(dir.path()), &GlobalArgs {}).expect("lint self-check should run");

    assert_eq!(exit_code, 0);
    assert!(output.passed);
    assert_eq!(output.component, "fixture");
}

#[test]
fn test_runs_declared_self_check_without_extensions() {
    let dir = tempfile::tempdir().expect("temp dir");
    write_component(dir.path(), r#"{ "test": ["sh scripts/test.sh"] }"#);
    write_script(dir.path(), "test.sh", "printf 'test self-check ran\\n'\n");

    let (output, exit_code) =
        run_test(test_args(dir.path()), &GlobalArgs {}).expect("test self-check should run");

    assert_eq!(exit_code, 0);
    assert!(output.passed);
    assert_eq!(output.component, "fixture");
}

#[test]
fn non_zero_self_check_fails_command_and_surfaces_output() {
    let dir = tempfile::tempdir().expect("temp dir");
    write_component(dir.path(), r#"{ "test": ["sh scripts/fail.sh"] }"#);
    write_script(
        dir.path(),
        "fail.sh",
        "printf 'visible failure stdout\\n'\nprintf 'visible failure stderr\\n' >&2\nexit 7\n",
    );

    let (output, exit_code) = run_test(test_args(dir.path()), &GlobalArgs {})
        .expect("test self-check failure should return structured output");

    assert_eq!(exit_code, 7);
    assert!(!output.passed);
    assert_eq!(output.status, "failed");
    let raw = output
        .raw_output
        .expect("failure should include raw output");
    assert!(raw.stdout_tail.contains("visible failure stdout"));
    assert!(raw.stderr_tail.contains("visible failure stderr"));
}

#[test]
fn missing_extension_and_self_check_keeps_existing_error() {
    let dir = tempfile::tempdir().expect("temp dir");
    write_component(dir.path(), r#"{}"#);

    let err = match run_lint(lint_args(dir.path()), &GlobalArgs {}) {
        Ok(_) => panic!("lint without extension or self-check should fail"),
        Err(err) => err,
    };

    assert!(
        err.to_string()
            .contains("Component 'fixture' has no extensions configured"),
        "unexpected error: {err}"
    );
}