perfgate-cli 0.18.1

CLI for perfgate performance budgets and baseline diffs
//! Integration tests for local baseline bootstrap commands.

use predicates::prelude::*;
use std::fs;
use std::path::Path;

mod common;
use common::{fixtures_dir, perfgate_cmd};

fn write_config(dir: &Path) {
    fs::write(
        dir.join("perfgate.toml"),
        r#"[defaults]
out_dir = "artifacts/perfgate"
baseline_dir = "baselines"

[[bench]]
name = "test-benchmark"
command = ["echo", "hello"]
"#,
    )
    .expect("write config");
}

fn write_two_bench_config(dir: &Path) {
    fs::write(
        dir.join("perfgate.toml"),
        r#"[defaults]
out_dir = "artifacts/perfgate"
baseline_dir = "baselines"

[[bench]]
name = "test-benchmark"
command = ["echo", "hello"]

[[bench]]
name = "second-benchmark"
command = ["echo", "world"]
"#,
    )
    .expect("write config");
}

fn write_run_fixture(path: &Path, bench: &str) {
    let mut receipt: serde_json::Value =
        serde_json::from_str(&fs::read_to_string(fixtures_dir().join("baseline.json")).unwrap())
            .expect("parse run fixture");
    receipt["bench"]["name"] = serde_json::json!(bench);
    fs::create_dir_all(path.parent().expect("run fixture has parent")).expect("create run parent");
    fs::write(
        path,
        serde_json::to_string_pretty(&receipt).expect("serialize run fixture"),
    )
    .expect("write run fixture");
}

fn write_baseline_maturity_fixture(
    path: &Path,
    bench: &str,
    sample_count: usize,
    cv: f64,
    days_old: i64,
) {
    let mut receipt: serde_json::Value =
        serde_json::from_str(&fs::read_to_string(fixtures_dir().join("baseline.json")).unwrap())
            .expect("parse run fixture");
    receipt["bench"]["name"] = serde_json::json!(bench);
    receipt["run"]["host"]["os"] = serde_json::json!(std::env::consts::OS);
    receipt["run"]["host"]["arch"] = serde_json::json!(std::env::consts::ARCH);
    let started_at = chrono::Utc::now() - chrono::Duration::days(days_old);
    receipt["run"]["started_at"] = serde_json::json!(started_at.to_rfc3339());
    receipt["run"]["ended_at"] =
        serde_json::json!((started_at + chrono::Duration::seconds(1)).to_rfc3339());
    receipt["stats"]["wall_ms"]["mean"] = serde_json::json!(100.0);
    receipt["stats"]["wall_ms"]["stddev"] = serde_json::json!(100.0 * cv);
    receipt["samples"] = serde_json::json!(
        (0..sample_count)
            .map(|_| serde_json::json!({
                "wall_ms": 100,
                "exit_code": 0,
                "warmup": false,
                "timed_out": false
            }))
            .collect::<Vec<_>>()
    );
    fs::create_dir_all(path.parent().expect("baseline fixture has parent"))
        .expect("create baseline parent");
    fs::write(
        path,
        serde_json::to_string_pretty(&receipt).expect("serialize baseline fixture"),
    )
    .expect("write baseline fixture");
}

#[test]
fn baseline_status_reports_missing_then_found_local_baseline() {
    let temp_dir = tempfile::tempdir().expect("create temp dir");
    write_config(temp_dir.path());

    perfgate_cmd()
        .current_dir(temp_dir.path())
        .args(["baseline", "status", "--config", "perfgate.toml"])
        .assert()
        .success()
        .stdout(predicate::str::contains("Baseline status"))
        .stdout(predicate::str::contains("MISSING test-benchmark"))
        .stdout(predicate::str::contains(
            "perfgate baseline promote --config perfgate.toml --all",
        ));

    fs::create_dir_all(temp_dir.path().join("baselines")).expect("create baselines");
    fs::copy(
        fixtures_dir().join("baseline.json"),
        temp_dir.path().join("baselines/test-benchmark.json"),
    )
    .expect("copy baseline fixture");

    perfgate_cmd()
        .current_dir(temp_dir.path())
        .args(["baseline", "status", "--config", "perfgate.toml"])
        .assert()
        .success()
        .stdout(predicate::str::contains("FOUND   test-benchmark"))
        .stdout(predicate::str::contains(
            "Summary: 1/1 local baseline found",
        ));
}

#[test]
fn baseline_doctor_reports_mature_and_missing_local_baselines() {
    let temp_dir = tempfile::tempdir().expect("create temp dir");
    write_two_bench_config(temp_dir.path());
    write_baseline_maturity_fixture(
        &temp_dir.path().join("baselines/test-benchmark.json"),
        "test-benchmark",
        7,
        0.03,
        0,
    );

    perfgate_cmd()
        .current_dir(temp_dir.path())
        .args(["baseline", "doctor", "--config", "perfgate.toml"])
        .assert()
        .success()
        .stdout(predicate::str::contains("Baseline doctor"))
        .stdout(predicate::str::contains("bench: test-benchmark"))
        .stdout(predicate::str::contains("status: mature"))
        .stdout(predicate::str::contains("bench: second-benchmark"))
        .stdout(predicate::str::contains("status: missing"))
        .stdout(predicate::str::contains("Summary: 1 mature"))
        .stdout(predicate::str::contains(
            "perfgate baseline promote --config perfgate.toml --all",
        ))
        .stdout(predicate::str::contains("Do not:"));
}

#[test]
fn baseline_doctor_classifies_high_noise_as_advisory() {
    let temp_dir = tempfile::tempdir().expect("create temp dir");
    write_config(temp_dir.path());
    write_baseline_maturity_fixture(
        &temp_dir.path().join("baselines/test-benchmark.json"),
        "test-benchmark",
        7,
        0.20,
        0,
    );

    perfgate_cmd()
        .current_dir(temp_dir.path())
        .args([
            "baseline",
            "doctor",
            "--config",
            "perfgate.toml",
            "--bench",
            "test-benchmark",
        ])
        .assert()
        .success()
        .stdout(predicate::str::contains("status: high_noise"))
        .stdout(predicate::str::contains("keep advisory"))
        .stdout(predicate::str::contains("perfgate paired"));
}

#[test]
fn baseline_init_creates_gitkeep_for_configured_baseline_dir() {
    let temp_dir = tempfile::tempdir().expect("create temp dir");
    write_config(temp_dir.path());

    perfgate_cmd()
        .current_dir(temp_dir.path())
        .args(["baseline", "init", "--config", "perfgate.toml"])
        .assert()
        .success()
        .stdout(predicate::str::contains("Wrote baselines"))
        .stdout(predicate::str::contains(
            "perfgate baseline promote --config perfgate.toml --all",
        ));

    assert!(
        temp_dir.path().join("baselines/.gitkeep").exists(),
        "baseline init should create baselines/.gitkeep"
    );
}

#[test]
fn baseline_promote_uses_check_all_artifact_convention() {
    let temp_dir = tempfile::tempdir().expect("create temp dir");
    write_config(temp_dir.path());
    let run_dir = temp_dir.path().join("artifacts/perfgate/test-benchmark");
    fs::create_dir_all(&run_dir).expect("create run dir");
    fs::copy(
        fixtures_dir().join("baseline.json"),
        run_dir.join("run.json"),
    )
    .expect("copy run fixture");

    perfgate_cmd()
        .current_dir(temp_dir.path())
        .args([
            "baseline",
            "promote",
            "--config",
            "perfgate.toml",
            "--bench",
            "test-benchmark",
        ])
        .assert()
        .success()
        .stderr(predicate::str::contains(
            "Promoted baseline for test-benchmark",
        ));

    let baseline_path = temp_dir.path().join("baselines/test-benchmark.json");
    assert!(
        baseline_path.exists(),
        "baseline promote should write the configured baseline path"
    );
    let promoted: serde_json::Value =
        serde_json::from_str(&fs::read_to_string(baseline_path).expect("read promoted baseline"))
            .expect("promoted baseline is json");
    assert_eq!(promoted["schema"].as_str(), Some("perfgate.run.v1"));
}

#[test]
fn baseline_promote_also_accepts_single_bench_artifact_convention() {
    let temp_dir = tempfile::tempdir().expect("create temp dir");
    write_config(temp_dir.path());
    let run_dir = temp_dir.path().join("artifacts/perfgate");
    fs::create_dir_all(&run_dir).expect("create run dir");
    fs::copy(
        fixtures_dir().join("baseline.json"),
        run_dir.join("run.json"),
    )
    .expect("copy run fixture");

    perfgate_cmd()
        .current_dir(temp_dir.path())
        .args([
            "baseline",
            "promote",
            "--config",
            "perfgate.toml",
            "--bench",
            "test-benchmark",
        ])
        .assert()
        .success()
        .stderr(predicate::str::contains("artifacts/perfgate"))
        .stderr(predicate::str::contains("run.json"));

    assert!(
        temp_dir
            .path()
            .join("baselines/test-benchmark.json")
            .exists(),
        "baseline promote should accept the single-bench artifact path"
    );
}

#[test]
fn baseline_promote_all_uses_check_all_artifact_convention() {
    let temp_dir = tempfile::tempdir().expect("create temp dir");
    write_two_bench_config(temp_dir.path());
    write_run_fixture(
        &temp_dir
            .path()
            .join("artifacts/perfgate/test-benchmark/run.json"),
        "test-benchmark",
    );
    write_run_fixture(
        &temp_dir
            .path()
            .join("artifacts/perfgate/second-benchmark/run.json"),
        "second-benchmark",
    );

    perfgate_cmd()
        .current_dir(temp_dir.path())
        .args(["baseline", "promote", "--config", "perfgate.toml", "--all"])
        .assert()
        .success()
        .stderr(predicate::str::contains(
            "Promoted baseline for test-benchmark",
        ))
        .stderr(predicate::str::contains(
            "Promoted baseline for second-benchmark",
        ))
        .stderr(predicate::str::contains("Promoted 2 baselines"));

    assert!(
        temp_dir
            .path()
            .join("baselines/test-benchmark.json")
            .exists(),
        "baseline promote --all should write the first configured baseline"
    );
    assert!(
        temp_dir
            .path()
            .join("baselines/second-benchmark.json")
            .exists(),
        "baseline promote --all should write the second configured baseline"
    );
}

#[test]
fn baseline_promote_refuses_overwrite_without_force() {
    let temp_dir = tempfile::tempdir().expect("create temp dir");
    write_config(temp_dir.path());
    write_run_fixture(
        &temp_dir
            .path()
            .join("artifacts/perfgate/test-benchmark/run.json"),
        "test-benchmark",
    );
    fs::create_dir_all(temp_dir.path().join("baselines")).expect("create baselines");
    fs::write(
        temp_dir.path().join("baselines/test-benchmark.json"),
        r#"{"schema":"perfgate.run.v1"}"#,
    )
    .expect("write existing baseline");

    perfgate_cmd()
        .current_dir(temp_dir.path())
        .args(["baseline", "promote", "--config", "perfgate.toml", "--all"])
        .assert()
        .failure()
        .stderr(predicate::str::contains("baseline already exists"))
        .stderr(predicate::str::contains("--force"));

    perfgate_cmd()
        .current_dir(temp_dir.path())
        .args([
            "baseline",
            "promote",
            "--config",
            "perfgate.toml",
            "--all",
            "--force",
        ])
        .assert()
        .success()
        .stderr(predicate::str::contains("Promoted 1 baseline"));
}

#[test]
fn baseline_promote_missing_default_artifact_teaches_next_command() {
    let temp_dir = tempfile::tempdir().expect("create temp dir");
    write_config(temp_dir.path());

    perfgate_cmd()
        .current_dir(temp_dir.path())
        .args([
            "baseline",
            "promote",
            "--config",
            "perfgate.toml",
            "--bench",
            "test-benchmark",
        ])
        .assert()
        .failure()
        .stderr(predicate::str::contains("run receipt not found"))
        .stderr(predicate::str::contains(
            "perfgate check --config perfgate.toml --all",
        ));
}