use std::path::Path;
use std::process::Command;
use crate::engine::stable_id;
use super::{PromotionProposal, PromotionRatchet};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GateCommandOutput {
pub succeeded: bool,
pub stdout: String,
pub stderr: String,
}
impl GateCommandOutput {
#[must_use]
pub fn success(stdout: impl Into<String>) -> Self {
Self {
succeeded: true,
stdout: stdout.into(),
stderr: String::new(),
}
}
#[must_use]
pub fn failure(stdout: impl Into<String>, stderr: impl Into<String>) -> Self {
Self {
succeeded: false,
stdout: stdout.into(),
stderr: stderr.into(),
}
}
}
pub fn replay_promotion_gates_with<F>(
mut proposals: Vec<PromotionProposal>,
mut execute: F,
) -> Result<Vec<PromotionProposal>, String>
where
F: FnMut(&str) -> Result<GateCommandOutput, String>,
{
let mut evidence = Vec::new();
for mut gate in required_gates() {
let output = execute(&gate.runner).map_err(|error| {
format!(
"could not execute canonical gate `{}`: {error}",
gate.suite_id
)
})?;
let joined = format!(
"suite={}\nrunner={}\nsucceeded={}\nstdout:\n{}\nstderr:\n{}",
gate.suite_id, gate.runner, output.succeeded, output.stdout, output.stderr
);
gate.evidence_digest = Some(stable_id("promotion_gate_output", &joined));
gate.command_succeeded = output.succeeded;
if output.succeeded {
let (passed, failed) =
parse_pass_fail(&output.stdout, &output.stderr).ok_or_else(|| {
format!(
"canonical gate `{}` succeeded without a parseable pass/fail report",
gate.suite_id
)
})?;
gate.passed = passed;
gate.failed = failed;
} else {
gate.passed = 0;
gate.failed = 1;
}
evidence.push(gate);
}
for proposal in &mut proposals {
proposal.gates.clone_from(&evidence);
}
Ok(proposals)
}
pub fn replay_promotion_gates(
proposals: Vec<PromotionProposal>,
workspace_root: &Path,
) -> Result<Vec<PromotionProposal>, String> {
replay_promotion_gates_with(proposals, |runner| {
let output = Command::new("sh")
.arg("-c")
.arg(runner)
.current_dir(workspace_root)
.output()
.map_err(|error| format!("failed to start `{runner}`: {error}"))?;
Ok(GateCommandOutput {
succeeded: output.status.success(),
stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
})
})
}
pub(super) fn required_gates() -> Vec<PromotionRatchet> {
let mut gates = vec![
PromotionRatchet::coding_modification(0, 1),
PromotionRatchet::industry(0, 1),
PromotionRatchet::unit_specs(0, 1),
];
for gate in &mut gates {
gate.command_succeeded = false;
}
gates
}
fn parse_pass_fail(stdout: &str, stderr: &str) -> Option<(usize, usize)> {
let combined = format!("{stdout}\n{stderr}");
for line in combined.lines().rev() {
if let (Some(passed), Some(failed)) =
(count_after(line, "passed="), count_after(line, "failed="))
{
return Some((passed, failed));
}
}
for line in combined.lines().rev() {
if line.contains("test result:") {
let passed = count_before(line, " passed;");
let failed = count_before(line, " failed;");
if let (Some(passed), Some(failed)) = (passed, failed) {
return Some((passed, failed));
}
}
}
None
}
fn count_after(line: &str, marker: &str) -> Option<usize> {
let tail = line.split_once(marker)?.1;
let digits: String = tail.chars().take_while(char::is_ascii_digit).collect();
digits.parse().ok()
}
fn count_before(line: &str, marker: &str) -> Option<usize> {
let head = line.split_once(marker)?.0;
head.split_whitespace().next_back()?.parse().ok()
}