contextgraph_trace/
report.rs1use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub enum CheckStatus {
13 Pass,
14 Fail,
15 Skipped,
18}
19
20#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
23pub struct CheckResult {
24 pub name: String,
25 pub status: CheckStatus,
26 pub evidence: String,
27}
28
29impl CheckResult {
30 pub fn pass(name: impl Into<String>, evidence: impl Into<String>) -> Self {
31 Self {
32 name: name.into(),
33 status: CheckStatus::Pass,
34 evidence: evidence.into(),
35 }
36 }
37
38 pub fn fail(name: impl Into<String>, evidence: impl Into<String>) -> Self {
39 Self {
40 name: name.into(),
41 status: CheckStatus::Fail,
42 evidence: evidence.into(),
43 }
44 }
45
46 pub fn skip(name: impl Into<String>, evidence: impl Into<String>) -> Self {
47 Self {
48 name: name.into(),
49 status: CheckStatus::Skipped,
50 evidence: evidence.into(),
51 }
52 }
53
54 pub fn from_violations(
57 name: impl Into<String>,
58 violations: Vec<String>,
59 pass_evidence: impl Into<String>,
60 ) -> Self {
61 if violations.is_empty() {
62 Self::pass(name, pass_evidence)
63 } else {
64 Self::fail(name, violations.join("; "))
65 }
66 }
67}
68
69#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
71pub struct TraceReport {
72 pub target: String,
75 pub checks: Vec<CheckResult>,
76}
77
78impl TraceReport {
79 pub fn passed(&self) -> bool {
82 !self
83 .checks
84 .iter()
85 .any(|check| check.status == CheckStatus::Fail)
86 }
87
88 pub fn failures(&self) -> impl Iterator<Item = &CheckResult> {
90 self.checks
91 .iter()
92 .filter(|check| check.status == CheckStatus::Fail)
93 }
94
95 pub fn tally(&self) -> (usize, usize, usize) {
97 let mut passed = 0;
98 let mut failed = 0;
99 let mut skipped = 0;
100 for check in &self.checks {
101 match check.status {
102 CheckStatus::Pass => passed += 1,
103 CheckStatus::Fail => failed += 1,
104 CheckStatus::Skipped => skipped += 1,
105 }
106 }
107 (passed, failed, skipped)
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
116 fn a_report_passes_only_when_nothing_failed() {
117 let clean = TraceReport {
118 target: "session sess_1".into(),
119 checks: vec![
120 CheckResult::pass("sequence-integrity", "16 events, dense"),
121 CheckResult::skip("resume-integrity", "no resume recorded"),
122 ],
123 };
124 assert!(clean.passed());
125 assert_eq!(clean.tally(), (1, 0, 1));
126
127 let broken = TraceReport {
128 target: "session sess_1".into(),
129 checks: vec![CheckResult::from_violations(
130 "effect-exactly-once",
131 vec!["effect `write:x#1` replayed at seq 12".into()],
132 "",
133 )],
134 };
135 assert!(!broken.passed());
136 assert_eq!(broken.failures().count(), 1);
137 }
138
139 #[test]
140 fn from_violations_passes_on_empty_and_joins_on_failure() {
141 let pass = CheckResult::from_violations("citation-at-use", vec![], "3 frame(s) labelled");
142 assert_eq!(pass.status, CheckStatus::Pass);
143 assert_eq!(pass.evidence, "3 frame(s) labelled");
144
145 let fail =
146 CheckResult::from_violations("citation-at-use", vec!["a".into(), "b".into()], "unused");
147 assert_eq!(fail.status, CheckStatus::Fail);
148 assert_eq!(fail.evidence, "a; b");
149 }
150
151 #[test]
152 fn report_is_serde_roundtrippable_for_json_output() {
153 let report = TraceReport {
154 target: "session sess_1".into(),
155 checks: vec![CheckResult::pass("turn-loop-pairing", "2 call(s) paired")],
156 };
157 let json = serde_json::to_string(&report).unwrap();
158 let back: TraceReport = serde_json::from_str(&json).unwrap();
159 assert_eq!(back, report);
160 }
161}