use crate::harness::types::{CheckOutput, MatchOutput};
pub fn parse_check_result(stdout: &str) -> Result<CheckOutput, String> {
let parsed: serde_json::Value = serde_json::from_str(stdout).map_err(|_| {
format!(
"Failed to parse check output as JSON: {}",
&stdout[..stdout.len().min(200)]
)
})?;
let obj = parsed
.as_object()
.ok_or("Check output must be a JSON object")?;
let result = obj.get("result").and_then(|v| v.as_bool()).ok_or_else(|| {
let actual_type = obj
.get("result")
.map(|v| format!("{}", v))
.unwrap_or_else(|| "missing".to_string());
format!(
"Check output \"result\" must be a boolean, got {}",
actual_type
)
})?;
let reason = if let Some(r) = obj.get("reason") {
Some(
r.as_str()
.ok_or_else(|| {
format!(
"Check output \"reason\" must be a string if present, got {}",
r
)
})?
.to_string(),
)
} else {
None
};
let allowed_keys: std::collections::HashSet<&str> = ["result", "reason"].into_iter().collect();
for key in obj.keys() {
if !allowed_keys.contains(key.as_str()) {
return Err(format!("Check output has unexpected property \"{}\"", key));
}
}
Ok(CheckOutput { result, reason })
}
pub fn parse_match_result(stdout: &str) -> Result<MatchOutput, String> {
let parsed: serde_json::Value = serde_json::from_str(stdout).map_err(|_| {
format!(
"Failed to parse match output as JSON: {}",
&stdout[..stdout.len().min(200)]
)
})?;
let obj = parsed
.as_object()
.ok_or("Match output must be a JSON object")?;
let variant = obj.get("variant").and_then(|v| v.as_str()).ok_or_else(|| {
let actual_type = obj
.get("variant")
.map(|v| format!("{}", v))
.unwrap_or_else(|| "missing".to_string());
format!(
"Match output \"variant\" must be a non-empty string, got {}",
actual_type
)
})?;
if variant.is_empty() {
return Err("Match output \"variant\" must be a non-empty string, got \"\"".to_string());
}
let reason = if let Some(r) = obj.get("reason") {
Some(
r.as_str()
.ok_or_else(|| {
format!(
"Match output \"reason\" must be a string if present, got {}",
r
)
})?
.to_string(),
)
} else {
None
};
let allowed_keys: std::collections::HashSet<&str> = ["variant", "reason"].into_iter().collect();
for key in obj.keys() {
if !allowed_keys.contains(key.as_str()) {
return Err(format!("Match output has unexpected property \"{}\"", key));
}
}
Ok(MatchOutput {
variant: variant.to_string(),
reason,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_valid_true() {
let result = parse_check_result(r#"{"result": true, "reason": "all good"}"#).unwrap();
assert!(result.result);
assert_eq!(result.reason.as_deref(), Some("all good"));
}
#[test]
fn test_parse_valid_false_no_reason() {
let result = parse_check_result(r#"{"result": false}"#).unwrap();
assert!(!result.result);
assert!(result.reason.is_none());
}
#[test]
fn test_parse_valid_true_no_reason() {
let result = parse_check_result(r#"{"result": true}"#).unwrap();
assert!(result.result);
assert!(result.reason.is_none());
}
#[test]
fn test_parse_invalid_json() {
let result = parse_check_result("not json");
assert!(result.is_err());
assert!(result.unwrap_err().contains("Failed to parse"));
}
#[test]
fn test_parse_missing_result() {
let result = parse_check_result(r#"{"reason": "oops"}"#);
assert!(result.is_err());
assert!(result.unwrap_err().contains("must be a boolean"));
}
#[test]
fn test_parse_extra_property_rejected() {
let result = parse_check_result(r#"{"result": true, "extra": "bad"}"#);
assert!(result.is_err());
assert!(result.unwrap_err().contains("unexpected property"));
}
#[test]
fn test_parse_non_boolean_result() {
let result = parse_check_result(r#"{"result": "yes"}"#);
assert!(result.is_err());
assert!(result.unwrap_err().contains("must be a boolean"));
}
#[test]
fn test_parse_non_string_reason() {
let result = parse_check_result(r#"{"result": true, "reason": 42}"#);
assert!(result.is_err());
assert!(result.unwrap_err().contains("must be a string"));
}
#[test]
fn test_parse_array_rejected() {
let result = parse_check_result(r#"[true]"#);
assert!(result.is_err());
assert!(result.unwrap_err().contains("must be a JSON object"));
}
#[test]
fn test_parse_null_rejected() {
let result = parse_check_result("null");
assert!(result.is_err());
assert!(result.unwrap_err().contains("must be a JSON object"));
}
#[test]
fn test_parse_number_rejected() {
let result = parse_check_result("42");
assert!(result.is_err());
assert!(result.unwrap_err().contains("must be a JSON object"));
}
#[test]
fn test_parse_check_result_from_exec_stdout() {
let stdout = r#"{"result": true, "reason": "pass"}"#;
let output = parse_check_result(stdout).unwrap();
assert!(output.result);
assert_eq!(output.reason.as_deref(), Some("pass"));
}
#[test]
fn test_parse_match_valid_with_reason() {
let result =
parse_match_result(r#"{"variant": "small", "reason": "under 50 lines"}"#).unwrap();
assert_eq!(result.variant, "small");
assert_eq!(result.reason.as_deref(), Some("under 50 lines"));
}
#[test]
fn test_parse_match_valid_no_reason() {
let result = parse_match_result(r#"{"variant": "large"}"#).unwrap();
assert_eq!(result.variant, "large");
assert!(result.reason.is_none());
}
#[test]
fn test_parse_match_empty_variant_rejected() {
let result = parse_match_result(r#"{"variant": ""}"#);
assert!(result.is_err());
assert!(result.unwrap_err().contains("non-empty string"));
}
#[test]
fn test_parse_match_missing_variant() {
let result = parse_match_result(r#"{"reason": "oops"}"#);
assert!(result.is_err());
assert!(result.unwrap_err().contains("must be a non-empty string"));
}
#[test]
fn test_parse_match_extra_property_rejected() {
let result = parse_match_result(r#"{"variant": "small", "extra": "bad"}"#);
assert!(result.is_err());
assert!(result.unwrap_err().contains("unexpected property"));
}
#[test]
fn test_parse_match_non_string_variant() {
let result = parse_match_result(r#"{"variant": 42}"#);
assert!(result.is_err());
assert!(result.unwrap_err().contains("must be a non-empty string"));
}
#[test]
fn test_parse_match_non_string_reason() {
let result = parse_match_result(r#"{"variant": "small", "reason": 42}"#);
assert!(result.is_err());
assert!(result.unwrap_err().contains("must be a string"));
}
#[test]
fn test_parse_match_invalid_json() {
let result = parse_match_result("not json");
assert!(result.is_err());
assert!(result.unwrap_err().contains("Failed to parse"));
}
#[test]
fn test_parse_match_array_rejected() {
let result = parse_match_result(r#"["small"]"#);
assert!(result.is_err());
assert!(result.unwrap_err().contains("must be a JSON object"));
}
#[test]
fn test_parse_match_null_rejected() {
let result = parse_match_result("null");
assert!(result.is_err());
assert!(result.unwrap_err().contains("must be a JSON object"));
}
}