use std::process::{Child, ExitStatus};
use std::time::{Duration, Instant};
use amiss_wire::digest::hj;
use amiss_wire::json::{Value, canonical, parse};
use amiss_wire::report::PAYLOAD_SCHEMA;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AcceptanceDefect {
Shape,
Noncanonical,
PayloadDigest,
Engine,
BaseIdentity,
CandidateIdentity,
Completeness,
FindingCount,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Expectations {
pub engine_digest: String,
pub base_commit: String,
pub candidate_commit: Option<String>,
}
fn member<'value>(value: &'value Value, key: &str) -> Option<&'value Value> {
match value {
Value::Object(members) => members
.iter()
.find(|(name, _)| name == key)
.map(|(_, member)| member),
Value::Null | Value::Bool(_) | Value::Integer(_) | Value::String(_) | Value::Array(_) => {
None
}
}
}
fn text<'value>(value: &'value Value, key: &str) -> Option<&'value str> {
match member(value, key) {
Some(Value::String(text)) => Some(text),
_ => None,
}
}
pub fn accept(wire: &[u8], expectations: &Expectations) -> Result<i64, AcceptanceDefect> {
let trimmed = wire
.strip_suffix(b"\n")
.ok_or(AcceptanceDefect::Noncanonical)?;
let envelope = parse(trimmed).map_err(|_defect| AcceptanceDefect::Shape)?;
if canonical(&envelope) != trimmed {
return Err(AcceptanceDefect::Noncanonical);
}
let payload = member(&envelope, "payload").ok_or(AcceptanceDefect::Shape)?;
let recorded = text(&envelope, "payload_digest").ok_or(AcceptanceDefect::Shape)?;
if hj(PAYLOAD_SCHEMA, payload).to_string() != recorded {
return Err(AcceptanceDefect::PayloadDigest);
}
let engine_row = member(payload, "engine").ok_or(AcceptanceDefect::Shape)?;
if text(engine_row, "engine_digest") != Some(expectations.engine_digest.as_str()) {
return Err(AcceptanceDefect::Engine);
}
let evaluation = member(payload, "evaluation").ok_or(AcceptanceDefect::Shape)?;
let resolved = text(evaluation, "status") != Some("unavailable");
if resolved {
let base = member(evaluation, "base").ok_or(AcceptanceDefect::Shape)?;
if text(base, "commit_oid") != Some(expectations.base_commit.as_str()) {
return Err(AcceptanceDefect::BaseIdentity);
}
let candidate = member(evaluation, "candidate").ok_or(AcceptanceDefect::Shape)?;
if let (Some(expected), Some("git-commit")) = (
expectations.candidate_commit.as_deref(),
text(candidate, "kind"),
) && text(candidate, "commit_oid") != Some(expected)
{
return Err(AcceptanceDefect::CandidateIdentity);
}
}
let result = member(payload, "result").ok_or(AcceptanceDefect::Shape)?;
let exit_code = match member(result, "exit_code") {
Some(Value::Integer(code)) => *code,
_ => return Err(AcceptanceDefect::Shape),
};
let complete = member(result, "complete") == Some(&Value::Bool(true));
if complete != (exit_code == 0 || exit_code == 1) {
return Err(AcceptanceDefect::Completeness);
}
let count = match member(result, "finding_count") {
Some(Value::Integer(count)) => *count,
_ => return Err(AcceptanceDefect::Shape),
};
let findings = match member(payload, "findings") {
Some(Value::Array(rows)) => rows.len(),
_ => return Err(AcceptanceDefect::Shape),
};
if i64::try_from(findings).map_err(|_defect| AcceptanceDefect::Shape)? != count {
return Err(AcceptanceDefect::FindingCount);
}
Ok(exit_code)
}
#[derive(Debug)]
pub enum Supervised {
Completed(ExitStatus),
Killed,
}
pub fn supervise(child: &mut Child, ceiling: Duration) -> std::io::Result<Supervised> {
let start = Instant::now();
loop {
if let Some(status) = child.try_wait()? {
return Ok(Supervised::Completed(status));
}
if start.elapsed() >= ceiling {
let _signalled = child.kill();
let _reaped = child.wait();
return Ok(Supervised::Killed);
}
std::thread::sleep(Duration::from_millis(25));
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Defect {
Killed,
Signalled,
Oversize,
ExitMismatch,
Acceptance(AcceptanceDefect),
}
pub fn settle(
outcome: &Supervised,
stdout: &[u8],
expectations: &Expectations,
) -> Result<i64, Defect> {
let status = match *outcome {
Supervised::Killed => return Err(Defect::Killed),
Supervised::Completed(status) => status,
};
if u64::try_from(stdout.len()).unwrap_or(u64::MAX) > amiss_wire::report::MACHINE_JSON_BYTES {
return Err(Defect::Oversize);
}
let code = status.code().ok_or(Defect::Signalled)?;
let class = accept(stdout, expectations).map_err(Defect::Acceptance)?;
if i64::from(code) != class {
return Err(Defect::ExitMismatch);
}
Ok(class)
}