use crate::exit_codes;
use assay_core::errors::diagnostic::{exit_class, Diagnostic, ExitClass, Severity};
use std::path::{Path, PathBuf};
pub fn normalize_severity(s: &str) -> &'static str {
Severity::parse(s)
.map(Severity::as_cli_str)
.unwrap_or("error")
}
pub fn infer_policy_path(assay_yaml: &Path) -> Option<PathBuf> {
let s = std::fs::read_to_string(assay_yaml).ok()?;
let doc: serde_yaml::Value = serde_yaml::from_str(&s).ok()?;
let m = doc.as_mapping()?;
let v = m.get(serde_yaml::Value::String("policy".into()))?;
let p = v.as_str()?;
Some(PathBuf::from(p))
}
pub fn decide_exit(diags: &[Diagnostic]) -> i32 {
let mut saw_error = false;
let mut saw_config = false;
for d in diags {
if normalize_severity(&d.severity) != "error" {
continue;
}
saw_error = true;
match exit_class(&d.code) {
ExitClass::Config => saw_config = true,
ExitClass::Test | ExitClass::Unregistered => {}
}
}
if !saw_error {
return exit_codes::OK;
}
if saw_config {
exit_codes::CONFIG_ERROR
} else {
exit_codes::TEST_FAILED
}
}
pub fn decide_repair_failure_exit() -> i32 {
exit_codes::CONFIG_ERROR
}
#[cfg(test)]
mod decide_exit_tests {
use super::*;
use assay_core::errors::diagnostic::{codes, ERROR_EXIT_CLASSES};
fn err(code: &str) -> Diagnostic {
Diagnostic::new(code, "test")
}
#[test]
fn missing_trace_exits_the_same_under_either_code() {
assert_eq!(
decide_exit(&[err(codes::E_TRACE_MISS)]),
decide_exit(&[err(codes::E_PATH_NOT_FOUND)]),
);
}
#[test]
fn codes_the_prefix_list_missed_now_exit_config() {
for code in [
codes::E_TRACE_MISS,
codes::E_TRACE_INVALID,
codes::E_REPLAY_STRICT_MISSING,
codes::E_EMB_DIMS,
] {
assert_eq!(
decide_exit(&[err(code)]),
exit_codes::CONFIG_ERROR,
"{code} is a registered error code and must not exit as a test failure",
);
}
}
#[test]
fn every_registered_error_code_exits_config() {
for (code, _) in ERROR_EXIT_CLASSES {
assert_eq!(
decide_exit(&[err(code)]),
exit_codes::CONFIG_ERROR,
"{code}",
);
}
}
#[test]
fn unregistered_code_exits_test_failure() {
assert_eq!(decide_exit(&[err("E_ARG_SCHEMA")]), exit_codes::TEST_FAILED);
assert_eq!(decide_exit(&[err("E_UNKNOWN")]), exit_codes::TEST_FAILED);
}
#[test]
fn one_config_code_among_test_codes_decides() {
let diags = vec![err("E_ARG_SCHEMA"), err(codes::E_CFG_PARSE)];
assert_eq!(decide_exit(&diags), exit_codes::CONFIG_ERROR);
}
#[test]
fn warnings_and_empty_sets_exit_ok() {
assert_eq!(decide_exit(&[]), exit_codes::OK);
let warn = err(codes::W_CFG_VACUOUS_EXPECTED).with_severity("warn");
assert_eq!(decide_exit(&[warn]), exit_codes::OK);
}
#[test]
fn spelling_no_longer_decides() {
assert_eq!(
decide_exit(&[err("E_TRACE_SCHEMA")]),
exit_codes::TEST_FAILED
);
assert_eq!(
decide_exit(&[err("E_CFG_INVENTED")]),
exit_codes::TEST_FAILED
);
}
}
#[cfg(test)]
mod severity_exit_tests {
use super::{decide_exit, normalize_severity};
use assay_core::errors::diagnostic::Diagnostic;
#[test]
fn an_unknown_severity_does_not_silently_pass() {
assert_eq!(normalize_severity("cirtical"), "error");
let diag = Diagnostic {
code: "E_CFG_PARSE".into(),
severity: "cirtical".into(),
source: "test".into(),
message: "unclassifiable".into(),
context: serde_json::Value::Null,
fix_steps: vec![],
};
assert_ne!(decide_exit(&[diag]), 0, "an unknown severity exited 0");
}
#[test]
fn known_severities_are_unchanged() {
assert_eq!(normalize_severity("Warning"), "warn");
assert_eq!(normalize_severity("INFO"), "note");
assert_eq!(normalize_severity("error"), "error");
}
}