use crate::core::types::QualityGate;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GateResult {
Pass,
Fail(GateAction, String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GateAction {
Block,
Warn,
SkipDependents,
}
pub fn evaluate_gate(gate: &QualityGate, exit_code: i32, stdout: &str) -> GateResult {
let action = parse_action(gate.on_fail.as_deref());
if exit_code != 0 {
let msg = gate
.message
.clone()
.unwrap_or_else(|| format!("gate failed: command exited with code {exit_code}"));
return GateResult::Fail(action, msg);
}
if let Some(ref parse_mode) = gate.parse {
if parse_mode == "json" {
return evaluate_json_gate(gate, stdout, action);
}
}
if let Some(ref pattern) = gate.regex {
return evaluate_regex_gate(pattern, stdout, action, gate.message.as_deref());
}
GateResult::Pass
}
fn evaluate_json_gate(gate: &QualityGate, stdout: &str, action: GateAction) -> GateResult {
let field_name = match gate.field.as_deref() {
Some(f) => f,
None => return GateResult::Pass,
};
let parsed: serde_json::Value = match serde_json::from_str(stdout) {
Ok(v) => v,
Err(e) => {
return GateResult::Fail(
action,
gate.message
.clone()
.unwrap_or_else(|| format!("gate: invalid JSON output: {e}")),
);
}
};
let value = match parsed.get(field_name) {
Some(v) => v,
None => {
return GateResult::Fail(
action,
gate.message
.clone()
.unwrap_or_else(|| format!("gate: JSON field '{field_name}' not found")),
);
}
};
if !gate.threshold.is_empty() {
let val_str = match value.as_str() {
Some(s) => s.to_string(),
None => value.to_string(),
};
if !gate.threshold.contains(&val_str) {
return GateResult::Fail(
action,
gate.message.clone().unwrap_or_else(|| {
format!(
"gate: field '{field_name}' = '{val_str}', expected one of {:?}",
gate.threshold
)
}),
);
}
}
if let Some(min) = gate.min {
let num = value.as_f64().unwrap_or_else(|| {
value
.as_str()
.and_then(|s| s.parse::<f64>().ok())
.unwrap_or(f64::NAN)
});
if num < min {
return GateResult::Fail(
action,
gate.message.clone().unwrap_or_else(|| {
format!("gate: field '{field_name}' = {num}, minimum is {min}")
}),
);
}
}
GateResult::Pass
}
fn evaluate_regex_gate(
pattern: &str,
stdout: &str,
action: GateAction,
message: Option<&str>,
) -> GateResult {
match regex::Regex::new(pattern) {
Ok(re) => {
if re.is_match(stdout) {
GateResult::Pass
} else {
GateResult::Fail(
action,
message.map(String::from).unwrap_or_else(|| {
format!("gate: stdout did not match pattern '{pattern}'")
}),
)
}
}
Err(e) => GateResult::Fail(action, format!("gate: invalid regex '{pattern}': {e}")),
}
}
fn parse_action(on_fail: Option<&str>) -> GateAction {
match on_fail {
Some("warn") => GateAction::Warn,
Some("skip_dependents") => GateAction::SkipDependents,
_ => GateAction::Block,
}
}
pub fn gpu_env_vars(gpu_device: Option<u32>) -> Vec<(String, String)> {
match gpu_device {
Some(dev) => vec![
("CUDA_VISIBLE_DEVICES".into(), dev.to_string()),
("HIP_VISIBLE_DEVICES".into(), dev.to_string()),
],
None => vec![],
}
}