1use crate::conformance::{Failure, Leaf, run_leaf};
12use crate::{BlobBackend, BlobError, Entry, Page};
13use std::collections::BTreeMap;
14use std::sync::Mutex;
15
16#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
18pub struct OpCount {
19 pub calls: u64,
21 pub ok: u64,
23 pub backend: u64,
25 pub unsupported: u64,
27}
28
29impl OpCount {
30 pub fn errors(&self) -> u64 {
32 self.backend + self.unsupported
33 }
34}
35
36#[derive(Debug)]
38pub struct Instrumented<'a, B> {
39 inner: &'a B,
40 counts: Mutex<BTreeMap<&'static str, OpCount>>,
41}
42
43impl<'a, B: BlobBackend> Instrumented<'a, B> {
44 pub fn new(inner: &'a B) -> Self {
46 Instrumented {
47 inner,
48 counts: Mutex::new(BTreeMap::new()),
49 }
50 }
51
52 pub fn counts(&self) -> BTreeMap<&'static str, OpCount> {
54 self.counts.lock().map(|c| c.clone()).unwrap_or_default()
55 }
56
57 fn note<T>(&self, op: &'static str, r: Result<T, BlobError>) -> Result<T, BlobError> {
58 if let Ok(mut c) = self.counts.lock() {
59 let e = c.entry(op).or_default();
60 e.calls += 1;
61 match &r {
62 Ok(_) => e.ok += 1,
63 Err(BlobError::Backend(_)) => e.backend += 1,
64 Err(BlobError::Unsupported(_)) => e.unsupported += 1,
65 }
66 }
67 r
68 }
69}
70
71impl<B: BlobBackend> BlobBackend for Instrumented<'_, B> {
72 fn backend(&self) -> &str {
73 self.inner.backend()
74 }
75
76 fn version(&self) -> &str {
77 self.inner.version()
78 }
79
80 async fn read(&self, key: &str) -> Result<Option<Vec<u8>>, BlobError> {
81 self.note("read", self.inner.read(key).await)
82 }
83
84 async fn write(&self, key: &str, bytes: Vec<u8>) -> Result<(), BlobError> {
85 self.note("write", self.inner.write(key, bytes).await)
86 }
87
88 async fn delete(&self, key: &str) -> Result<(), BlobError> {
89 self.note("delete", self.inner.delete(key).await)
90 }
91
92 async fn stat(&self, key: &str) -> Result<Option<Entry>, BlobError> {
93 self.note("stat", self.inner.stat(key).await)
94 }
95
96 async fn list(
97 &self,
98 prefix: &str,
99 after: Option<&str>,
100 limit: usize,
101 ) -> Result<Page, BlobError> {
102 self.note("list", self.inner.list(prefix, after, limit).await)
103 }
104
105 async fn create_exclusive(&self, key: &str, bytes: Vec<u8>) -> Result<bool, BlobError> {
106 self.note(
107 "create_exclusive",
108 self.inner.create_exclusive(key, bytes).await,
109 )
110 }
111
112 async fn replace_atomic(&self, key: &str, bytes: Vec<u8>) -> Result<(), BlobError> {
113 self.note(
114 "replace_atomic",
115 self.inner.replace_atomic(key, bytes).await,
116 )
117 }
118
119 async fn append(&self, key: &str, bytes: Vec<u8>) -> Result<(), BlobError> {
120 self.note("append", self.inner.append(key, bytes).await)
121 }
122}
123
124#[derive(Debug, Clone, PartialEq, Eq)]
126pub struct LeafStats {
127 pub leaf: Leaf,
129 pub runs: u32,
131 pub passed: u32,
133 pub wrong: u32,
135 pub errored: u32,
137 pub first_failure: Option<String>,
139 pub ops: BTreeMap<&'static str, OpCount>,
141}
142
143impl LeafStats {
144 pub fn is_exact(&self) -> bool {
146 self.runs > 0 && self.passed == self.runs
147 }
148
149 pub fn precision(&self) -> f64 {
152 if self.runs == 0 {
153 0.0
154 } else {
155 f64::from(self.passed) / f64::from(self.runs)
156 }
157 }
158}
159
160pub async fn measure<B: BlobBackend>(target: &B, leaves: &[Leaf], runs: u32) -> Vec<LeafStats> {
162 let mut out = Vec::with_capacity(leaves.len());
163 for leaf in leaves.iter().copied() {
164 let counted = Instrumented::new(target);
165 let (mut passed, mut wrong, mut errored) = (0, 0, 0);
166 let mut first_failure = None;
167 for _ in 0..runs {
168 match run_leaf(leaf, &counted).await {
169 Ok(()) => passed += 1,
170 Err(Failure::Wrong(m)) => {
171 wrong += 1;
172 first_failure.get_or_insert(m);
173 }
174 Err(Failure::Backend(e)) => {
175 errored += 1;
176 first_failure.get_or_insert(e.to_string());
177 }
178 }
179 }
180 out.push(LeafStats {
181 leaf,
182 runs,
183 passed,
184 wrong,
185 errored,
186 first_failure,
187 ops: counted.counts(),
188 });
189 }
190 out
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196 use crate::{Defect, MemoryBlob};
197
198 #[tokio::test]
199 async fn a_correct_backend_is_exact_on_every_run_and_the_counts_add_up() {
200 let s = measure(&MemoryBlob::new(), &[Leaf::Read, Leaf::CreateExclusive], 3).await;
201 for l in &s {
202 assert!(l.is_exact(), "{l:?}");
203 assert_eq!(l.precision(), 1.0);
204 assert_eq!((l.wrong, l.errored), (0, 0));
205 }
206 let w = &s[0].ops["write"];
207 assert_eq!(w.calls, w.ok + w.errors());
208 assert!(w.calls >= 3, "the test writes on every run");
209 }
210
211 #[tokio::test]
212 async fn a_defect_is_counted_as_wrong_not_rounded_to_a_pass() {
213 let b = MemoryBlob::with_defect(Defect::DeleteIsIgnored);
214 let s = measure(&b, &[Leaf::Delete], 4).await;
215 assert_eq!((s[0].passed, s[0].wrong, s[0].errored), (0, 4, 0));
216 assert!(!s[0].is_exact());
217 assert!(s[0].first_failure.is_some());
218 }
219
220 #[tokio::test]
221 async fn zero_runs_is_not_exact() {
222 let s = measure(&MemoryBlob::new(), &[Leaf::Read], 0).await;
223 assert!(!s[0].is_exact());
224 assert_eq!(s[0].precision(), 0.0);
225 }
226}