contextgraph_conformance/
report.rs1use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11pub enum CheckStatus {
12 Pass,
13 Fail,
14 Skipped,
17}
18
19#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
21pub struct CheckResult {
22 pub name: String,
23 pub status: CheckStatus,
24 pub evidence: String,
25}
26
27impl CheckResult {
28 pub fn pass(name: impl Into<String>, evidence: impl Into<String>) -> Self {
29 Self {
30 name: name.into(),
31 status: CheckStatus::Pass,
32 evidence: evidence.into(),
33 }
34 }
35
36 pub fn fail(name: impl Into<String>, evidence: impl Into<String>) -> Self {
37 Self {
38 name: name.into(),
39 status: CheckStatus::Fail,
40 evidence: evidence.into(),
41 }
42 }
43
44 pub fn skip(name: impl Into<String>, evidence: impl Into<String>) -> Self {
45 Self {
46 name: name.into(),
47 status: CheckStatus::Skipped,
48 evidence: evidence.into(),
49 }
50 }
51
52 pub fn from_bool(name: impl Into<String>, passed: bool, evidence: impl Into<String>) -> Self {
55 if passed {
56 Self::pass(name, evidence)
57 } else {
58 Self::fail(name, evidence)
59 }
60 }
61}
62
63#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
65pub struct ConformanceReport {
66 pub target: String,
68 pub checks: Vec<CheckResult>,
69}
70
71impl ConformanceReport {
72 pub fn passed(&self) -> bool {
75 !self
76 .checks
77 .iter()
78 .any(|check| check.status == CheckStatus::Fail)
79 }
80
81 pub fn failures(&self) -> impl Iterator<Item = &CheckResult> {
83 self.checks
84 .iter()
85 .filter(|check| check.status == CheckStatus::Fail)
86 }
87
88 pub fn tally(&self) -> (usize, usize, usize) {
90 let mut passed = 0;
91 let mut failed = 0;
92 let mut skipped = 0;
93 for check in &self.checks {
94 match check.status {
95 CheckStatus::Pass => passed += 1,
96 CheckStatus::Fail => failed += 1,
97 CheckStatus::Skipped => skipped += 1,
98 }
99 }
100 (passed, failed, skipped)
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 #[test]
109 fn a_run_passes_only_when_nothing_failed() {
110 let report = ConformanceReport {
111 target: "in-process: t".into(),
112 checks: vec![
113 CheckResult::pass("handshake", "ok"),
114 CheckResult::skip("malformed-input-tolerance", "n/a"),
115 ],
116 };
117 assert!(report.passed());
118 assert_eq!(report.tally(), (1, 0, 1));
119
120 let broken = ConformanceReport {
121 target: "in-process: t".into(),
122 checks: vec![
123 CheckResult::pass("handshake", "ok"),
124 CheckResult::fail("budget-honesty", "over budget"),
125 ],
126 };
127 assert!(!broken.passed());
128 assert_eq!(broken.failures().count(), 1);
129 assert_eq!(broken.tally(), (1, 1, 0));
130 }
131
132 #[test]
133 fn report_is_serde_roundtrippable_for_json_output() {
134 let report = ConformanceReport {
135 target: "stdio: contextgraph-example-docs".into(),
136 checks: vec![CheckResult::from_bool(
137 "frame-validity",
138 true,
139 "3 frames ok",
140 )],
141 };
142 let json = serde_json::to_string(&report).unwrap();
143 let back: ConformanceReport = serde_json::from_str(&json).unwrap();
144 assert_eq!(back, report);
145 }
146}