ci-bill 0.1.1

Rank GitHub Actions cost per workflow from the billing usage CSV and point to likely causes
Documentation
use std::process::Command;

fn report(args: &[&str]) -> std::process::Output {
    Command::new(env!("CARGO_BIN_EXE_ci-bill"))
        .current_dir(env!("CARGO_MANIFEST_DIR"))
        .args(args)
        .output()
        .expect("run ci-bill")
}

#[test]
fn sample_md_matches_snapshot() {
    let output = report(&[
        "report",
        "--usage",
        "samples/usage-sample.csv",
        "--workflows",
        "samples/.github/workflows",
        "--md",
    ]);
    let stdout = String::from_utf8(output.stdout).expect("utf-8 stdout");
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        output.status.success(),
        "stderr:\n{stderr}\nstdout:\n{stdout}"
    );
    assert!(stderr.is_empty(), "{stderr}");
    let expected = include_str!("snapshots/report.md");
    assert_eq!(stdout, expected);
    assert!(stdout.contains("lint.yml:8"), "{stdout}");
    assert!(stdout.contains("207.87"), "{stdout}");
    assert!(!stdout.contains("Sample"), "{stdout}");
}

#[test]
fn md_is_not_affected_by_top() {
    let with_top = report(&[
        "report",
        "--usage",
        "samples/usage-sample.csv",
        "--workflows",
        "samples/.github/workflows",
        "--md",
        "--top",
        "1",
    ]);
    let stdout = String::from_utf8(with_top.stdout).expect("utf-8 stdout");
    assert!(with_top.status.success());
    let expected = include_str!("snapshots/report.md");
    assert_eq!(
        stdout, expected,
        "the teardown lists every job and finding, not just the top workflow"
    );
}

#[test]
fn md_without_workflows_omits_job_and_finding_sections() {
    let output = report(&["report", "--usage", "samples/usage-sample.csv", "--md"]);
    let stdout = String::from_utf8(output.stdout).expect("utf-8 stdout");
    assert!(output.status.success());
    assert!(stdout.starts_with("# ci-bill teardown\n"), "{stdout}");
    assert!(stdout.contains("Total net: 284.21 USD\n"), "{stdout}");
    assert!(!stdout.contains("## Top jobs"), "{stdout}");
    assert!(!stdout.contains("## Findings"), "{stdout}");
    assert!(!stdout.contains("## Suggested fix"), "{stdout}");
}

#[test]
fn md_runs_the_usual_budget_check() {
    let under = report(&[
        "report",
        "--usage",
        "samples/usage-sample.csv",
        "--workflows",
        "samples/.github/workflows",
        "--md",
        "--budget",
        "1000",
    ]);
    assert!(under.status.success());
    assert!(String::from_utf8_lossy(&under.stdout).contains("# ci-bill teardown"));

    let over = report(&[
        "report",
        "--usage",
        "samples/usage-sample.csv",
        "--workflows",
        "samples/.github/workflows",
        "--md",
        "--budget",
        "10",
    ]);
    assert_eq!(over.status.code(), Some(1));
    let stdout = String::from_utf8_lossy(&over.stdout);
    assert!(stdout.contains("# ci-bill teardown"), "{stdout}");
    assert!(
        stdout.contains("Budget exceeded: 284.21 > 10.00\n"),
        "{stdout}"
    );
}

#[test]
fn md_help_mentions_the_flag() {
    let output = report(&["report", "--help"]);
    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("--md"), "{stdout}");
}