perfgate-cli 0.18.0

CLI for perfgate performance budgets and baseline diffs
//! End-to-end fixture for the paved first-run CLI path.

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

mod common;
use common::perfgate_cmd;

fn write_minimal_rust_repo(dir: &Path) {
    fs::write(
        dir.join("Cargo.toml"),
        r#"
[package]
name = "example"
version = "0.1.0"
edition = "2021"

[[bench]]
name = "parser"
harness = false
"#,
    )
    .expect("write Cargo.toml");
}

#[cfg(unix)]
fn success_command() -> Vec<&'static str> {
    vec!["true"]
}

#[cfg(windows)]
fn success_command() -> Vec<&'static str> {
    vec!["cmd", "/c", "exit", "0"]
}

fn command_toml_array(command: &[&str]) -> String {
    command
        .iter()
        .map(|part| format!("\"{part}\""))
        .collect::<Vec<_>>()
        .join(", ")
}

fn tune_generated_config_for_fast_fixture(config_path: &Path) {
    let generated = fs::read_to_string(config_path).expect("read generated config");
    assert!(generated.contains("repeat = 7"));
    assert!(generated.contains("warmup = 1"));
    assert!(generated.contains(r#"command = ["cargo", "bench", "--bench", "parser"]"#));

    // The e2e target is the paved perfgate command sequence, not Cargo's bench
    // implementation. Keep init discovery covered, then use a fast command so
    // this integration test stays deterministic on every runner.
    let tuned = generated
        .replace("repeat = 7", "repeat = 1")
        .replace("warmup = 1", "warmup = 0")
        .replace("threshold = 0.20", "threshold = 100.0")
        .replace(
            r#"command = ["cargo", "bench", "--bench", "parser"]"#,
            &format!(r#"command = [{}]"#, command_toml_array(&success_command())),
        );
    fs::write(config_path, tuned).expect("write tuned config");
}

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

    perfgate_cmd()
        .current_dir(temp_dir.path())
        .args(["init", "--ci", "github", "--profile", "standard"])
        .assert()
        .success()
        .stderr(predicate::str::contains("Discovered 1 benchmark(s):"))
        .stderr(predicate::str::contains(
            "perfgate check --config perfgate.toml --all",
        ))
        .stderr(predicate::str::contains(
            "perfgate baseline promote --config perfgate.toml --all",
        ));

    let root = temp_dir.path();
    tune_generated_config_for_fast_fixture(&root.join("perfgate.toml"));

    perfgate_cmd()
        .current_dir(temp_dir.path())
        .args(["doctor", "--config", "perfgate.toml"])
        .assert()
        .success()
        .stdout(predicate::str::contains("OK   config"))
        .stdout(predicate::str::contains("OK   benchmarks"))
        .stdout(predicate::str::contains("WARN baselines"))
        .stdout(predicate::str::contains("Summary: 0 failed"));

    perfgate_cmd()
        .current_dir(temp_dir.path())
        .args(["check", "--config", "perfgate.toml", "--all"])
        .assert()
        .success()
        .stderr(predicate::str::contains("parser"));

    let require_baseline = perfgate_cmd()
        .current_dir(temp_dir.path())
        .args([
            "check",
            "--config",
            "perfgate.toml",
            "--all",
            "--require-baseline",
        ])
        .output()
        .expect("run require-baseline check before promotion");
    assert_eq!(
        require_baseline.status.code(),
        Some(1),
        "require-baseline should fail before the first baseline is promoted"
    );
    let stderr = String::from_utf8_lossy(&require_baseline.stderr);
    assert!(
        stderr.contains("baseline") && stderr.contains("parser"),
        "missing-baseline failure should name the missing benchmark: {stderr}"
    );

    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 parser"))
        .stdout(predicate::str::contains(
            "perfgate baseline promote --config perfgate.toml --all",
        ));

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

    perfgate_cmd()
        .current_dir(temp_dir.path())
        .args([
            "check",
            "--config",
            "perfgate.toml",
            "--all",
            "--require-baseline",
        ])
        .assert()
        .success();

    assert!(root.join("perfgate.toml").exists());
    assert!(root.join(".github/workflows/perfgate.yml").exists());
    assert!(root.join(".perfgate/README.md").exists());
    assert!(root.join("baselines/.gitkeep").exists());
    assert!(root.join("baselines/parser.json").exists());
    assert!(root.join("artifacts/perfgate/parser/run.json").exists());
    assert!(root.join("artifacts/perfgate/parser/compare.json").exists());
    assert!(root.join("artifacts/perfgate/parser/report.json").exists());
    assert!(root.join("artifacts/perfgate/parser/comment.md").exists());

    let workflow = fs::read_to_string(root.join(".github/workflows/perfgate.yml"))
        .expect("read generated GitHub workflow");
    assert!(workflow.contains("EffortlessMetrics/perfgate@v0"));
    assert!(workflow.contains("config: perfgate.toml"));
    assert!(workflow.contains("require_baseline: \"true\""));
    assert!(workflow.contains("upload_artifact: \"true\""));

    let onboarding =
        fs::read_to_string(root.join(".perfgate/README.md")).expect("read onboarding README");
    assert!(onboarding.contains("Commit `perfgate.toml`"));
    assert!(onboarding.contains("perfgate.toml"));
    assert!(onboarding.contains(".github/workflows/perfgate.yml"));
    assert!(onboarding.contains("baselines/"));

    let baseline: serde_json::Value = serde_json::from_str(
        &fs::read_to_string(root.join("baselines/parser.json")).expect("read baseline"),
    )
    .expect("baseline should be json");
    assert_eq!(baseline["schema"].as_str(), Some("perfgate.run.v1"));
    assert_eq!(baseline["bench"]["name"].as_str(), Some("parser"));
}