use std::collections::HashSet;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "lowercase")]
pub enum VerificationPhase {
Syntax,
Lint,
Typecheck,
Audit,
Test,
}
impl VerificationPhase {
pub fn canonical_order() -> [Self; 5] {
[
Self::Syntax,
Self::Lint,
Self::Typecheck,
Self::Audit,
Self::Test,
]
}
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum PhaseStatus {
Passed,
Failed,
Error,
Skipped,
NotRun,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum PhaseFailureCategory {
Findings,
Infrastructure,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct PhaseReport {
pub phase: VerificationPhase,
pub status: PhaseStatus,
pub exit_code: Option<i32>,
pub summary: String,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct PhaseFailure {
pub phase: VerificationPhase,
pub category: PhaseFailureCategory,
pub summary: String,
}
pub fn phase_status_from_exit_code(exit_code: i32) -> PhaseStatus {
if exit_code == 0 {
PhaseStatus::Passed
} else if exit_code >= 2 {
PhaseStatus::Error
} else {
PhaseStatus::Failed
}
}
pub fn phase_failure_category_from_exit_code(exit_code: i32) -> PhaseFailureCategory {
if exit_code >= 2 {
PhaseFailureCategory::Infrastructure
} else {
PhaseFailureCategory::Findings
}
}
#[derive(Debug, Clone, Default)]
pub struct RunnerStepFilter {
pub step: Option<String>,
pub skip: Option<String>,
}
impl RunnerStepFilter {
pub fn should_run(&self, step_name: &str) -> bool {
let step_name = step_name.trim();
if step_name.is_empty() {
return true;
}
let selected = csv_set(self.step.as_deref());
if !selected.is_empty() && !selected.contains(step_name) {
return false;
}
let skipped = csv_set(self.skip.as_deref());
if skipped.contains(step_name) {
return false;
}
true
}
pub fn to_env_pairs(&self) -> Vec<(String, String)> {
let mut env = Vec::new();
if let Some(step) = &self.step {
env.push((super::exec_context::STEP.to_string(), step.clone()));
}
if let Some(skip) = &self.skip {
env.push((super::exec_context::SKIP.to_string(), skip.clone()));
}
env
}
}
fn csv_set(value: Option<&str>) -> HashSet<String> {
value
.unwrap_or_default()
.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.map(ToString::to_string)
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_should_run_with_no_filters() {
let filter = RunnerStepFilter::default();
assert!(filter.should_run("lint"));
}
#[test]
fn test_should_run_honors_step_include() {
let filter = RunnerStepFilter {
step: Some("lint,test".to_string()),
skip: None,
};
assert!(filter.should_run("lint"));
assert!(!filter.should_run("deploy"));
}
#[test]
fn test_should_run_honors_skip() {
let filter = RunnerStepFilter {
step: None,
skip: Some("lint".to_string()),
};
assert!(!filter.should_run("lint"));
assert!(filter.should_run("test"));
}
#[test]
fn test_to_env_pairs_exports_step_and_skip() {
let filter = RunnerStepFilter {
step: Some("a".to_string()),
skip: Some("b".to_string()),
};
let env = filter.to_env_pairs();
assert_eq!(env.len(), 2);
assert!(env.iter().any(|(k, v)| k == "HOMEBOY_STEP" && v == "a"));
assert!(env.iter().any(|(k, v)| k == "HOMEBOY_SKIP" && v == "b"));
}
#[test]
fn test_csv_set() {
let set = csv_set(Some("lint, test,,"));
assert!(set.contains("lint"));
assert!(set.contains("test"));
assert_eq!(set.len(), 2);
}
#[test]
fn test_should_run() {
let filter = RunnerStepFilter {
step: Some("lint,test".to_string()),
skip: Some("test".to_string()),
};
assert!(filter.should_run("lint"));
assert!(!filter.should_run("test"));
assert!(!filter.should_run("deploy"));
}
#[test]
fn test_to_env_pairs() {
let filter = RunnerStepFilter {
step: Some("a".to_string()),
skip: Some("b".to_string()),
};
let env = filter.to_env_pairs();
assert!(env.iter().any(|(k, v)| k == "HOMEBOY_STEP" && v == "a"));
assert!(env.iter().any(|(k, v)| k == "HOMEBOY_SKIP" && v == "b"));
}
#[test]
fn verification_phase_order_is_canonical() {
assert_eq!(
VerificationPhase::canonical_order(),
[
VerificationPhase::Syntax,
VerificationPhase::Lint,
VerificationPhase::Typecheck,
VerificationPhase::Audit,
VerificationPhase::Test,
]
);
}
#[test]
fn phase_exit_codes_are_classified() {
assert_eq!(phase_status_from_exit_code(0), PhaseStatus::Passed);
assert_eq!(phase_status_from_exit_code(1), PhaseStatus::Failed);
assert_eq!(phase_status_from_exit_code(2), PhaseStatus::Error);
assert_eq!(
phase_failure_category_from_exit_code(1),
PhaseFailureCategory::Findings
);
assert_eq!(
phase_failure_category_from_exit_code(2),
PhaseFailureCategory::Infrastructure
);
}
}
#[cfg(test)]
#[path = "../../../tests/core/extension/runner_contract_test.rs"]
mod runner_contract_test;