Skip to main content

cortexkit_bus_trait/
conformance.rs

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}