use std::fs;
use std::path::Path;
use tempfile::tempdir;
mod common;
use common::perfgate_cmd;
fn write_basic_config(root: &Path) -> std::path::PathBuf {
let config_path = root.join("perfgate.toml");
fs::write(
&config_path,
r#"
[defaults]
out_dir = "artifacts/perfgate"
[[bench]]
name = "parser"
command = ["echo", "parser"]
"#,
)
.expect("write config");
config_path
}
fn write_decision_ready_config(root: &Path) -> std::path::PathBuf {
let config_path = root.join("perfgate.toml");
fs::write(
&config_path,
r#"
[defaults]
out_dir = "artifacts/perfgate"
[[bench]]
name = "parser"
command = ["echo", "parser"]
[[scenario]]
name = "release"
bench = "parser"
weight = 1.0
compare = "artifacts/perfgate/parser/compare.json"
probe_baseline = "artifacts/perfgate/parser/probes-baseline.json"
probe_current = "artifacts/perfgate/parser/probes-current.json"
probe_compare = "artifacts/perfgate/parser/probe-compare.json"
[[tradeoff]]
name = "parser tradeoff"
if_failed = "max_rss_kb"
downgrade_to = "warn"
[[tradeoff.require]]
metric = "wall_ms"
min_improvement_ratio = 1.05
[[tradeoff.allow]]
probe = "parser.tokenize"
metric = "wall_ms"
max_regression = 0.03
"#,
)
.expect("write config");
config_path
}
fn write_compare_receipt(path: &Path, status: &str, baseline: f64, current: f64) {
let regression = if current > baseline {
(current - baseline) / baseline
} else {
0.0
};
write_compare_receipt_for_metric(path, "wall_ms", status, baseline, current, regression);
}
fn write_compare_receipt_for_metric(
path: &Path,
metric: &str,
status: &str,
baseline: f64,
current: f64,
regression: f64,
) {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).expect("create compare parent");
}
let fail = u32::from(status == "fail");
let warn = u32::from(status == "warn");
let pass = u32::from(status == "pass");
let receipt = serde_json::json!({
"schema": "perfgate.compare.v1",
"tool": {"name": "perfgate", "version": "0.18.0"},
"bench": {
"name": "parser",
"command": ["echo", "parser"],
"repeat": 1,
"warmup": 0
},
"baseline_ref": {"path": "baselines/parser.json", "run_id": "baseline"},
"current_ref": {"path": "artifacts/perfgate/parser/run.json", "run_id": "current"},
"budgets": {},
"deltas": {
metric: {
"baseline": baseline,
"current": current,
"ratio": current / baseline,
"pct": (current - baseline) / baseline,
"regression": regression,
"status": status
}
},
"verdict": {
"status": status,
"counts": {"pass": pass, "warn": warn, "fail": fail, "skip": 0},
"reasons": []
}
});
fs::write(path, serde_json::to_string_pretty(&receipt).unwrap()).expect("write compare");
}
#[test]
fn decision_suggest_tells_user_to_run_local_gate_first_without_compares() {
let temp_dir = tempdir().expect("temp dir");
let config_path = write_basic_config(temp_dir.path());
let mut cmd = perfgate_cmd();
cmd.current_dir(temp_dir.path())
.arg("decision")
.arg("suggest")
.arg("--config")
.arg(&config_path);
let output = cmd.output().expect("run decision suggest");
assert!(
output.status.success(),
"decision suggest should succeed: stderr {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("Status: run_local_gate_first"),
"stdout: {stdout}"
);
assert!(
stdout.contains("no compare receipts found"),
"stdout: {stdout}"
);
assert!(
stdout.contains("perfgate check --config"),
"stdout: {stdout}"
);
}
#[test]
fn decision_suggest_detects_throughput_improvement_as_structured_candidate() {
let temp_dir = tempdir().expect("temp dir");
let config_path = write_basic_config(temp_dir.path());
let out_dir = temp_dir.path().join("artifacts").join("perfgate");
write_compare_receipt_for_metric(
&out_dir.join("parser").join("compare.json"),
"throughput_per_s",
"pass",
100.0,
120.0,
0.0,
);
let mut cmd = perfgate_cmd();
cmd.current_dir(temp_dir.path())
.arg("decision")
.arg("suggest")
.arg("--config")
.arg(&config_path);
let output = cmd.output().expect("run decision suggest");
assert!(
output.status.success(),
"decision suggest should succeed: stderr {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("Status: structured_decision_candidate"),
"expected throughput improvement to flag structured_decision_candidate, stdout: {stdout}"
);
}
#[test]
fn decision_suggest_detects_throughput_regression_as_structured_candidate() {
let temp_dir = tempdir().expect("temp dir");
let config_path = write_basic_config(temp_dir.path());
let out_dir = temp_dir.path().join("artifacts").join("perfgate");
write_compare_receipt_for_metric(
&out_dir.join("parser").join("compare.json"),
"throughput_per_s",
"fail",
100.0,
80.0,
0.20,
);
let mut cmd = perfgate_cmd();
cmd.current_dir(temp_dir.path())
.arg("decision")
.arg("suggest")
.arg("--config")
.arg(&config_path);
let output = cmd.output().expect("run decision suggest");
assert!(
output.status.success(),
"decision suggest should succeed: stderr {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("Status: structured_decision_candidate"),
"expected throughput regression to flag structured_decision_candidate, stdout: {stdout}"
);
}
#[test]
fn decision_suggest_reports_structured_decision_ready_when_policy_and_evidence_exist() {
let temp_dir = tempdir().expect("temp dir");
let config_path = write_decision_ready_config(temp_dir.path());
let out_dir = temp_dir.path().join("artifacts").join("perfgate");
write_compare_receipt(
&out_dir.join("parser").join("compare.json"),
"fail",
100.0,
115.0,
);
fs::write(out_dir.join("parser").join("probes-baseline.json"), "{}").expect("write probe");
fs::write(out_dir.join("parser").join("probes-current.json"), "{}").expect("write probe");
fs::write(out_dir.join("parser").join("probe-compare.json"), "{}").expect("write probe");
let mut cmd = perfgate_cmd();
cmd.current_dir(temp_dir.path())
.arg("decision")
.arg("suggest")
.arg("--config")
.arg(&config_path);
let output = cmd.output().expect("run decision suggest");
assert!(
output.status.success(),
"decision suggest should succeed: stderr {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("Status: structured_decision_ready"),
"stdout: {stdout}"
);
assert!(
stdout.contains("perfgate decision evaluate --config"),
"stdout: {stdout}"
);
assert!(
stdout.contains("probe evidence: configured, 3 receipts"),
"stdout: {stdout}"
);
}