Skip to main content

persistant_blob/
contract.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Stage 1 of backend benchmarking for the blob kind: **eligibility**, pass or fail, nothing else.
3//!
4//! A consumer names the leaves it needs; a backend that does not pass every one of them is
5//! `Eligibility::Excluded`, with the reason per leaf. It is never "ranked slower". A required leaf
6//! nobody tested is an exclusion too (`Reason::Untested`), so a gap in the suite cannot read as a pass.
7//! The result type is [`persistant_core::choose::Eligibility`], shared with every kind.
8
9use crate::BlobBackend;
10use crate::conformance::{Certified, Leaf, certify_leaves};
11use persistant_core::choose::{Eligibility, LeafFailure};
12use persistant_core::{Config, Reason};
13
14/// Decide eligibility from certified rows. A required leaf with no row is `Untested`.
15pub fn evaluate(rows: &[Certified], required: &[Leaf]) -> Eligibility {
16    let mut out: Vec<LeafFailure> = Vec::new();
17    for leaf in required.iter().copied() {
18        if out.iter().any(|e| e.leaf == leaf.id()) {
19            continue;
20        }
21        match rows.iter().find(|c| c.leaf == leaf) {
22            Some(c) if c.row.enabled => {}
23            Some(c) => out.push(LeafFailure {
24                leaf: leaf.id().to_string(),
25                reason: c.row.reason.unwrap_or(Reason::Untested),
26                note: c.row.note.clone(),
27            }),
28            None => out.push(LeafFailure {
29                leaf: leaf.id().to_string(),
30                reason: Reason::Untested,
31                note: "no test ran for this leaf".to_string(),
32            }),
33        }
34    }
35    if out.is_empty() {
36        Eligibility::Eligible
37    } else {
38        Eligibility::Excluded(out)
39    }
40}
41
42/// Run the required leaves (each against its positive and known-bad controls) on `target`.
43pub async fn check<B: BlobBackend>(target: &B, required: &[Leaf]) -> Eligibility {
44    let rows = certify_leaves(target, &Config::new(), &Config::new(), required).await;
45    evaluate(&rows, required)
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51    use crate::{Defect, MemoryBlob};
52
53    #[tokio::test]
54    async fn a_correct_backend_is_eligible() {
55        let leaves = [Leaf::Read, Leaf::Write, Leaf::CreateExclusive];
56        assert_eq!(
57            check(&MemoryBlob::new(), &leaves).await,
58            Eligibility::Eligible
59        );
60    }
61
62    #[tokio::test]
63    async fn a_defective_backend_is_excluded_as_wrong_and_the_control_passes() {
64        let r = check(
65            &MemoryBlob::with_defect(Defect::DeleteIsIgnored),
66            &[Leaf::Delete],
67        )
68        .await;
69        assert!(
70            matches!(&r, Eligibility::Excluded(ex) if ex[0].reason == Reason::Wrong && ex[0].leaf == "blob.delete"),
71            "{r:?}"
72        );
73        assert_eq!(
74            check(&MemoryBlob::new(), &[Leaf::Delete]).await,
75            Eligibility::Eligible
76        );
77    }
78
79    #[test]
80    fn a_required_leaf_with_no_row_is_untested_not_a_pass() {
81        let r = evaluate(&[], &[Leaf::Read]);
82        assert!(matches!(&r, Eligibility::Excluded(ex) if ex[0].reason == Reason::Untested));
83        assert_eq!(evaluate(&[], &[]), Eligibility::Eligible);
84    }
85}