1use async_trait::async_trait;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4pub enum BackendKind {
5 InMemory,
6 Nats,
7}
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum PropertySet {
11 Parity,
12 NatsOnly,
13}
14
15impl PropertySet {
16 pub const fn name(self) -> &'static str {
17 match self {
18 Self::Parity => "parity",
19 Self::NatsOnly => "nats-only",
20 }
21 }
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub struct PropertySpec {
26 pub name: &'static str,
27 pub set: PropertySet,
28}
29
30pub const CONFORMANCE_PROPERTIES: &[PropertySpec] = &[
31 PropertySpec {
32 name: "publish_ack_monotonic_sequence",
33 set: PropertySet::Parity,
34 },
35 PropertySpec {
36 name: "consumer_replay_within_process_lifetime",
37 set: PropertySet::Parity,
38 },
39 PropertySpec {
40 name: "stream_delivery_count_rises_on_each_nak",
41 set: PropertySet::Parity,
42 },
43 PropertySpec {
44 name: "stream_term_is_never_redelivered",
45 set: PropertySet::Parity,
46 },
47 PropertySpec {
48 name: "stream_in_progress_extends_ack_wait",
49 set: PropertySet::Parity,
50 },
51 PropertySpec {
52 name: "register_revision",
53 set: PropertySet::Parity,
54 },
55 PropertySpec {
56 name: "register_absent_vs_empty",
57 set: PropertySet::Parity,
58 },
59 PropertySpec {
60 name: "watch_snapshot_then_updates_with_connection_flag",
61 set: PropertySet::Parity,
62 },
63 PropertySpec {
64 name: "work_redelivery_count",
65 set: PropertySet::Parity,
66 },
67 PropertySpec {
68 name: "work_cap_exhaustion_and_dead_letter",
69 set: PropertySet::Parity,
70 },
71 PropertySpec {
72 name: "full_work_queue_names_exhausted_limit",
73 set: PropertySet::Parity,
74 },
75 PropertySpec {
76 name: "error_taxonomy_and_boundary_mapping",
77 set: PropertySet::Parity,
78 },
79 PropertySpec {
80 name: "cursor_survives_process_restart",
81 set: PropertySet::NatsOnly,
82 },
83 PropertySpec {
84 name: "cursor_survives_credential_rotation",
85 set: PropertySet::NatsOnly,
86 },
87 PropertySpec {
88 name: "cursor_survives_device_rekey",
89 set: PropertySet::NatsOnly,
90 },
91 PropertySpec {
92 name: "server_enforced_grant_denial",
93 set: PropertySet::NatsOnly,
94 },
95 PropertySpec {
96 name: "reconnect_refusal_after_revocation",
97 set: PropertySet::NatsOnly,
98 },
99 PropertySpec {
100 name: "asynchronous_permission_violation_and_sentinel_attribution",
101 set: PropertySet::NatsOnly,
102 },
103 PropertySpec {
104 name: "leaf_behaviour",
105 set: PropertySet::NatsOnly,
106 },
107 PropertySpec {
108 name: "system_events",
109 set: PropertySet::NatsOnly,
110 },
111];
112
113#[derive(Debug, Clone, PartialEq, Eq)]
114pub enum PropertyApplicability {
115 Applicable,
116 NotApplicable { reason: &'static str },
117}
118
119#[derive(Debug, Clone, PartialEq, Eq)]
120pub struct PropertyClassification {
121 pub property: PropertySpec,
122 pub applicability: PropertyApplicability,
123}
124
125pub fn classify_properties(backend: BackendKind) -> Vec<PropertyClassification> {
126 CONFORMANCE_PROPERTIES
127 .iter()
128 .copied()
129 .map(|property| {
130 let applicability = match (backend, property.set) {
131 (BackendKind::InMemory, PropertySet::NatsOnly) => {
132 PropertyApplicability::NotApplicable {
133 reason:
134 "in-memory has no persisted process state, leaf, or server enforcement",
135 }
136 }
137 _ => PropertyApplicability::Applicable,
138 };
139 PropertyClassification {
140 property,
141 applicability,
142 }
143 })
144 .collect()
145}
146
147#[derive(Debug, Clone, PartialEq, Eq)]
148pub enum PropertyOutcome {
149 Passed,
150 Failed { reason: String },
151 NotApplicable { reason: String },
152}
153
154#[async_trait]
155pub trait ConformanceBackend: Send + Sync {
156 fn backend_kind(&self) -> BackendKind;
157 async fn probe(&self, property: PropertySpec) -> PropertyOutcome;
158}
159
160#[derive(Debug, Clone, PartialEq, Eq)]
161pub struct ConformanceReport {
162 pub backend: BackendKind,
163 pub property_set: PropertySet,
164 pub property_set_name: &'static str,
165 pub outcomes: Vec<(PropertySpec, PropertyOutcome)>,
166}
167
168impl ConformanceReport {
169 pub fn passed(&self) -> bool {
170 self.outcomes
171 .iter()
172 .all(|(_, outcome)| matches!(outcome, PropertyOutcome::Passed))
173 }
174}
175
176#[derive(Debug, Clone, PartialEq, Eq)]
177pub enum SuiteBug {
178 NatsOnlyAssertedAgainstInMemory,
179 ApplicablePropertyReportedNotApplicable { property: &'static str },
180}
181
182pub async fn run_conformance<B: ConformanceBackend>(
183 backend: &B,
184 property_set: PropertySet,
185) -> Result<ConformanceReport, SuiteBug> {
186 if backend.backend_kind() == BackendKind::InMemory && property_set == PropertySet::NatsOnly {
187 return Err(SuiteBug::NatsOnlyAssertedAgainstInMemory);
188 }
189
190 let mut outcomes = Vec::new();
191 for property in CONFORMANCE_PROPERTIES
192 .iter()
193 .copied()
194 .filter(|property| property.set == property_set)
195 {
196 let outcome = backend.probe(property).await;
197 if matches!(outcome, PropertyOutcome::NotApplicable { .. }) {
198 return Err(SuiteBug::ApplicablePropertyReportedNotApplicable {
199 property: property.name,
200 });
201 }
202 outcomes.push((property, outcome));
203 }
204
205 Ok(ConformanceReport {
206 backend: backend.backend_kind(),
207 property_set,
208 property_set_name: property_set.name(),
209 outcomes,
210 })
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 struct WeakenedNats;
218
219 #[async_trait]
220 impl ConformanceBackend for WeakenedNats {
221 fn backend_kind(&self) -> BackendKind {
222 BackendKind::Nats
223 }
224
225 async fn probe(&self, property: PropertySpec) -> PropertyOutcome {
226 match property.set {
227 PropertySet::Parity => PropertyOutcome::Passed,
228 PropertySet::NatsOnly => PropertyOutcome::Failed {
229 reason: "fixture intentionally has only parity semantics".into(),
230 },
231 }
232 }
233 }
234
235 #[tokio::test]
236 async fn weakened_nats_fixture_passes_parity_but_fails_nats_only() {
237 let parity = run_conformance(&WeakenedNats, PropertySet::Parity)
238 .await
239 .expect("parity run should be valid");
240 let nats_only = run_conformance(&WeakenedNats, PropertySet::NatsOnly)
241 .await
242 .expect("NATS-only run should be valid for a NATS fixture");
243
244 assert_eq!(parity.property_set_name, "parity");
245 assert!(parity.passed());
246 assert_eq!(nats_only.property_set_name, "nats-only");
247 assert!(!nats_only.passed());
248 }
249
250 #[test]
251 fn in_memory_classifies_every_nats_only_property_as_not_applicable() {
252 let classifications = classify_properties(BackendKind::InMemory);
253 for row in classifications
254 .iter()
255 .filter(|row| row.property.set == PropertySet::NatsOnly)
256 {
257 assert!(matches!(
258 row.applicability,
259 PropertyApplicability::NotApplicable { .. }
260 ));
261 }
262 }
263}