use crate::core::types::environment::{PromotionConfig, PromotionGate};
use std::path::Path;
#[derive(Debug, Clone)]
pub struct GateResult {
pub gate_type: String,
pub passed: bool,
pub message: String,
}
#[derive(Debug, Clone)]
pub struct PromotionResult {
pub from: String,
pub to: String,
pub gates: Vec<GateResult>,
pub all_passed: bool,
pub auto_approve: bool,
}
impl PromotionResult {
pub fn failed_count(&self) -> usize {
self.gates.iter().filter(|g| !g.passed).count()
}
pub fn passed_count(&self) -> usize {
self.gates.iter().filter(|g| g.passed).count()
}
}
pub fn evaluate_gates(
config_file: &Path,
target_env: &str,
promotion: &PromotionConfig,
) -> PromotionResult {
let mut gate_results = Vec::new();
for gate in &promotion.gates {
let result = evaluate_single_gate(config_file, gate);
gate_results.push(result);
}
let all_passed = gate_results.iter().all(|g| g.passed);
PromotionResult {
from: promotion.from.clone(),
to: target_env.to_string(),
gates: gate_results,
all_passed,
auto_approve: promotion.auto_approve,
}
}
fn evaluate_single_gate(config_file: &Path, gate: &PromotionGate) -> GateResult {
if let Some(ref opts) = gate.validate {
evaluate_validate_gate(config_file, opts.deep)
} else if gate.policy.is_some() {
evaluate_policy_gate(config_file)
} else if let Some(ref opts) = gate.coverage {
evaluate_coverage_gate(opts.min)
} else if let Some(ref script) = gate.script {
evaluate_script_gate(script)
} else {
GateResult {
gate_type: "unknown".into(),
passed: false,
message: "no gate type configured".into(),
}
}
}
fn evaluate_validate_gate(config_file: &Path, deep: bool) -> GateResult {
let config = match crate::core::parser::parse_and_validate(config_file) {
Ok(c) => c,
Err(e) => {
return GateResult {
gate_type: "validate".into(),
passed: false,
message: format!("validation failed: {e}"),
};
}
};
let errors = crate::core::parser::validate_config(&config);
let mode = if deep { "deep" } else { "standard" };
if errors.is_empty() {
GateResult {
gate_type: "validate".into(),
passed: true,
message: format!("{mode} validation passed"),
}
} else {
GateResult {
gate_type: "validate".into(),
passed: false,
message: format!("{mode} validation: {} error(s)", errors.len()),
}
}
}
fn evaluate_policy_gate(config_file: &Path) -> GateResult {
let config = match crate::core::parser::parse_and_validate(config_file) {
Ok(c) => c,
Err(e) => {
return GateResult {
gate_type: "policy".into(),
passed: false,
message: format!("parse error: {e}"),
};
}
};
let result = crate::core::parser::evaluate_policies_full(&config);
if result.has_blocking_violations() {
GateResult {
gate_type: "policy".into(),
passed: false,
message: format!(
"{} error(s), {} warning(s)",
result.error_count(),
result.warning_count()
),
}
} else {
GateResult {
gate_type: "policy".into(),
passed: true,
message: format!(
"policy check passed ({} warning(s))",
result.warning_count()
),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum CovProbe {
Unavailable,
Ran(String),
Failed { code: Option<i32> },
}
fn evaluate_coverage_gate(min_coverage: u32) -> GateResult {
evaluate_coverage_gate_inner(min_coverage, run_llvm_cov)
}
fn evaluate_coverage_gate_inner(min_coverage: u32, runner: fn() -> CovProbe) -> GateResult {
match runner() {
CovProbe::Ran(stdout) => match parse_coverage_from_output(&stdout) {
Some(actual) => coverage_threshold_result(actual, min_coverage),
None => coverage_advisory_result(min_coverage),
},
CovProbe::Failed { code } => GateResult {
gate_type: "coverage".into(),
passed: false,
message: match code {
Some(c) => format!("coverage gate failed: cargo llvm-cov exited with code {c}"),
None => "coverage gate failed: cargo llvm-cov terminated by signal".into(),
},
},
CovProbe::Unavailable => coverage_advisory_result(min_coverage),
}
}
fn coverage_threshold_result(actual: f64, min_coverage: u32) -> GateResult {
if actual >= min_coverage as f64 {
GateResult {
gate_type: "coverage".into(),
passed: true,
message: format!("coverage {actual:.1}% >= {min_coverage}%"),
}
} else {
GateResult {
gate_type: "coverage".into(),
passed: false,
message: format!("coverage {actual:.1}% < {min_coverage}% required"),
}
}
}
fn coverage_advisory_result(min_coverage: u32) -> GateResult {
GateResult {
gate_type: "coverage".into(),
passed: true,
message: format!(
"coverage gate: minimum {min_coverage}% (advisory — llvm-cov not available)"
),
}
}
fn run_llvm_cov() -> CovProbe {
classify_llvm_cov(
std::process::Command::new("cargo")
.args(["llvm-cov", "--summary-only"])
.output(),
)
}
fn classify_llvm_cov(spawn: std::io::Result<std::process::Output>) -> CovProbe {
match spawn {
Err(_) => CovProbe::Unavailable,
Ok(output) => classify_exit(
output.status.success(),
output.status.code(),
String::from_utf8_lossy(&output.stdout).to_string(),
),
}
}
fn classify_exit(success: bool, code: Option<i32>, stdout: String) -> CovProbe {
if success {
CovProbe::Ran(stdout)
} else {
CovProbe::Failed { code }
}
}
fn parse_coverage_from_output(stdout: &str) -> Option<f64> {
for line in stdout.lines() {
let trimmed = line.trim();
if trimmed.starts_with("TOTAL") || trimmed.contains("TOTAL") {
for word in trimmed.split_whitespace().rev() {
if let Some(pct_str) = word.strip_suffix('%') {
if let Ok(pct) = pct_str.parse::<f64>() {
return Some(pct);
}
}
}
}
}
None
}
fn evaluate_script_gate(script: &str) -> GateResult {
match std::process::Command::new("sh")
.args(["-c", script])
.output()
{
Ok(output) => {
if output.status.success() {
GateResult {
gate_type: "script".into(),
passed: true,
message: format!("script passed: {script}"),
}
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
GateResult {
gate_type: "script".into(),
passed: false,
message: format!("script failed (exit {}): {}", output.status, stderr.trim()),
}
}
}
Err(e) => GateResult {
gate_type: "script".into(),
passed: false,
message: format!("script error: {e}"),
},
}
}
#[cfg(test)]
#[path = "promotion_tests.rs"]
mod tests;