trellis_testing/conformance/
runner.rs1use std::collections::{BTreeMap, BTreeSet};
2use std::fmt;
3
4use super::{ConformanceLevel, ConformanceReport, ConformanceSuite};
5
6#[derive(Clone, Debug, Eq, PartialEq)]
8pub enum ConformanceCheckResult {
9 Passed,
11 Unsupported(String),
13 Failed(String),
15}
16
17impl ConformanceCheckResult {
18 pub const fn passed() -> Self {
20 Self::Passed
21 }
22
23 pub fn unsupported(reason: impl Into<String>) -> Self {
25 Self::Unsupported(reason.into())
26 }
27
28 pub fn failed(detail: impl Into<String>) -> Self {
30 Self::Failed(detail.into())
31 }
32}
33
34#[derive(Clone, Debug, Eq, PartialEq)]
36pub struct ConformanceCheckReport {
37 pub level: ConformanceLevel,
39 pub invariant: String,
41 pub result: ConformanceCheckResult,
43}
44
45#[derive(Clone, Debug, Eq, PartialEq)]
47pub struct ConformanceFailure {
48 pub level: ConformanceLevel,
50 pub invariant: String,
52 pub detail: String,
54}
55
56impl fmt::Display for ConformanceFailure {
57 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58 write!(
59 f,
60 "{:?} conformance failed for {}: {}",
61 self.level, self.invariant, self.detail
62 )
63 }
64}
65
66impl std::error::Error for ConformanceFailure {}
67
68pub fn conformance() -> ConformanceRunner {
70 ConformanceSuite::new().runner()
71}
72
73pub struct ConformanceRunner {
75 suite: ConformanceSuite,
76 checks: Vec<ConformanceCheck>,
77 unsupported: BTreeMap<ConformanceLevel, Vec<String>>,
78}
79
80impl ConformanceRunner {
81 pub fn new(suite: ConformanceSuite) -> Self {
83 Self {
84 suite,
85 checks: Vec::new(),
86 unsupported: BTreeMap::new(),
87 }
88 }
89
90 pub fn require(mut self, level: ConformanceLevel) -> Self {
92 self.suite = self.suite.require(level);
93 self
94 }
95
96 pub fn check(
98 mut self,
99 level: ConformanceLevel,
100 invariant: impl Into<String>,
101 run: impl FnMut() -> ConformanceCheckResult + 'static,
102 ) -> Self {
103 self.suite = self.suite.require(level);
104 self.checks.push(ConformanceCheck {
105 level,
106 invariant: invariant.into(),
107 run: Box::new(run),
108 });
109 self
110 }
111
112 pub fn unsupported(mut self, level: ConformanceLevel, reason: impl Into<String>) -> Self {
114 self.suite = self.suite.require(level);
115 self.unsupported
116 .entry(level)
117 .or_default()
118 .push(reason.into());
119 self
120 }
121
122 pub fn run(mut self) -> Result<ConformanceReport, ConformanceFailure> {
124 let mut report = ConformanceReport::new();
125 let mut seen = BTreeSet::new();
126 let mut unsupported = self.unsupported;
127
128 for check in &mut self.checks {
129 seen.insert(check.level);
130 let result = (check.run)();
131 match &result {
132 ConformanceCheckResult::Passed => {}
133 ConformanceCheckResult::Unsupported(reason) => {
134 unsupported
135 .entry(check.level)
136 .or_default()
137 .push(format!("{}: {reason}", check.invariant));
138 }
139 ConformanceCheckResult::Failed(detail) => {
140 return Err(ConformanceFailure {
141 level: check.level,
142 invariant: check.invariant.clone(),
143 detail: detail.clone(),
144 });
145 }
146 }
147 report = report.record_check(ConformanceCheckReport {
148 level: check.level,
149 invariant: check.invariant.clone(),
150 result,
151 });
152 }
153
154 for level in self.suite.required_levels() {
155 if let Some(reasons) = unsupported.remove(level) {
156 for reason in reasons {
157 report = report.unsupported_with_reason(*level, reason);
158 }
159 } else if seen.contains(level) {
160 report = report.support(*level);
161 } else {
162 report = report.unsupported_with_reason(
163 *level,
164 "no conformance check registered for required level",
165 );
166 }
167 }
168 Ok(report)
169 }
170}
171
172struct ConformanceCheck {
173 level: ConformanceLevel,
174 invariant: String,
175 run: Box<dyn FnMut() -> ConformanceCheckResult>,
176}