1use crate::concurrency::{Task, join_all, yield_now};
10use crate::{BlobBackend, BlobError, Defect, MemoryBlob};
11use persistant_core::{CapabilityId, Config, Controls, Reason, Row, Verdict};
12use std::collections::BTreeMap;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16#[non_exhaustive]
17pub enum Leaf {
18 Read,
20 Write,
22 Delete,
24 Stat,
26 List,
28 CreateExclusive,
30 ReplaceConcurrentRead,
32 Append,
34 AppendConcurrentWhole,
36}
37
38impl Leaf {
39 pub const ALL: [Leaf; 9] = [
41 Leaf::Read,
42 Leaf::Write,
43 Leaf::Delete,
44 Leaf::Stat,
45 Leaf::List,
46 Leaf::CreateExclusive,
47 Leaf::ReplaceConcurrentRead,
48 Leaf::Append,
49 Leaf::AppendConcurrentWhole,
50 ];
51
52 pub fn id(self) -> &'static str {
54 match self {
55 Leaf::Read => "blob.read",
56 Leaf::Write => "blob.write",
57 Leaf::Delete => "blob.delete",
58 Leaf::Stat => "blob.stat",
59 Leaf::List => "blob.list",
60 Leaf::CreateExclusive => "blob.create_exclusive",
61 Leaf::ReplaceConcurrentRead => "blob.replace_atomic.concurrent_read",
62 Leaf::Append => "blob.append",
63 Leaf::AppendConcurrentWhole => "blob.append.concurrent_whole",
64 }
65 }
66
67 fn trials(self) -> &'static str {
70 match self {
71 Leaf::CreateExclusive => "contenders=16;rounds=20;value_bytes=4096",
72 Leaf::ReplaceConcurrentRead => "readers=4;replaces=40;value_bytes=65536",
73 Leaf::AppendConcurrentWhole => "contenders=16;records=10;value_bytes=64",
74 _ => "",
75 }
76 }
77
78 fn defects(self) -> &'static [Defect] {
79 match self {
80 Leaf::Read => &[Defect::ReadTruncates, Defect::KeysFoldCase],
81 Leaf::Write => &[Defect::WriteIsLost],
82 Leaf::Delete => &[Defect::DeleteIsIgnored],
83 Leaf::Stat => &[Defect::StatLiesAboutLength],
84 Leaf::List => &[
85 Defect::ListIsDescending,
86 Defect::ListPrefixIsPattern,
87 Defect::ListIgnoresPaging,
88 ],
89 Leaf::CreateExclusive => &[Defect::CreateNotExclusive],
90 Leaf::ReplaceConcurrentRead => &[Defect::ReplaceTearsReads],
91 Leaf::Append => &[Defect::AppendOverwrites],
92 Leaf::AppendConcurrentWhole => &[Defect::AppendTearsRecords, Defect::AppendOverwrites],
93 }
94 }
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
99pub enum Failure {
100 Wrong(String),
102 Backend(BlobError),
104}
105
106impl From<BlobError> for Failure {
107 fn from(e: BlobError) -> Self {
108 Failure::Backend(e)
109 }
110}
111
112fn wrong<T>(msg: impl Into<String>) -> Result<T, Failure> {
113 Err(Failure::Wrong(msg.into()))
114}
115
116pub async fn run_leaf<B: BlobBackend>(leaf: Leaf, b: &B) -> Result<(), Failure> {
118 let base = format!("persistant-conformance/{}/", leaf.id());
119 let result = match leaf {
120 Leaf::Read => read(b, &base).await,
121 Leaf::Write => write(b, &base).await,
122 Leaf::Delete => delete(b, &base).await,
123 Leaf::Stat => stat(b, &base).await,
124 Leaf::List => list(b, &base).await,
125 Leaf::CreateExclusive => create_exclusive(b, &base).await,
126 Leaf::ReplaceConcurrentRead => replace_concurrent_read(b, &base).await,
127 Leaf::Append => append(b, &base).await,
128 Leaf::AppendConcurrentWhole => append_concurrent_whole(b, &base).await,
129 };
130 if let Ok(page) = b.list(&base, None, usize::MAX).await {
132 for e in page.entries {
133 let _ = b.delete(&e.key).await;
134 }
135 }
136 result
137}
138
139async fn read<B: BlobBackend>(b: &B, base: &str) -> Result<(), Failure> {
140 let k = format!("{base}k");
141 if b.read(&k).await?.is_some() {
142 return wrong("an absent key read as present");
143 }
144 for value in [vec![], vec![0u8], vec![1, 2, 3, 0xFF, 0x00, 0x80]] {
145 b.write(&k, value.clone()).await?;
146 match b.read(&k).await? {
147 Some(got) if got == value => {}
148 Some(got) => return wrong(format!("wrote {value:?}, read {got:?}")),
149 None => return wrong(format!("wrote {value:?}, read nothing")),
150 }
151 }
152 let upper = format!("{base}CaseKey");
155 let lower = format!("{base}casekey");
156 b.write(&upper, b"U".to_vec()).await?;
157 b.write(&lower, b"l".to_vec()).await?;
158 match b.read(&upper).await? {
159 Some(v) if v == b"U" => {}
160 other => {
161 return wrong(format!(
162 "keys differing only in case are the same key (read {other:?} after writing both)"
163 ));
164 }
165 }
166 Ok(())
167}
168
169async fn write<B: BlobBackend>(b: &B, base: &str) -> Result<(), Failure> {
170 let k = format!("{base}nested/dir/k");
171 b.write(&k, b"first".to_vec()).await?;
172 b.write(&k, b"second, longer".to_vec()).await?;
173 match b.read(&k).await? {
174 Some(v) if v == b"second, longer" => {}
175 other => return wrong(format!("after two writes read {other:?}")),
176 }
177 b.write(&k, b"x".to_vec()).await?;
178 match b.read(&k).await? {
179 Some(v) if v == b"x" => Ok(()),
180 other => wrong(format!("a shorter replacement left {other:?}")),
181 }
182}
183
184async fn delete<B: BlobBackend>(b: &B, base: &str) -> Result<(), Failure> {
185 let k = format!("{base}k");
186 b.write(&k, b"v".to_vec()).await?;
187 b.delete(&k).await?;
188 if b.read(&k).await?.is_some() {
189 return wrong("a deleted key still reads");
190 }
191 b.delete(&k).await?; Ok(())
193}
194
195async fn stat<B: BlobBackend>(b: &B, base: &str) -> Result<(), Failure> {
196 let k = format!("{base}k");
197 if b.stat(&k).await?.is_some() {
198 return wrong("an absent key has a stat");
199 }
200 for len in [0usize, 1, 5, 4096] {
201 b.write(&k, vec![7u8; len]).await?;
202 match b.stat(&k).await? {
203 Some(e) if e.key == k && e.len == len as u64 => {}
204 other => return wrong(format!("a {len}-byte value stat as {other:?}")),
205 }
206 }
207 Ok(())
208}
209
210async fn list<B: BlobBackend>(b: &B, base: &str) -> Result<(), Failure> {
211 let under = ["a/1", "a/10", "a/2", "a/b/c"];
213 for k in under
214 .iter()
215 .chain(["ab", "abz", "a0x", "a%z", "a[0]x", "b/1"].iter())
216 {
217 b.write(&format!("{base}{k}"), k.as_bytes().to_vec())
218 .await?;
219 }
220 let full = |s: &str| format!("{base}{s}");
221 let want: Vec<String> = ["a/1", "a/10", "a/2", "a/b/c"]
222 .iter()
223 .map(|s| full(s))
224 .collect();
225
226 let all = b.list(&full("a/"), None, 100).await?;
228 let got: Vec<String> = all.entries.iter().map(|e| e.key.clone()).collect();
229 if got != want || all.next.is_some() {
230 return wrong(format!(
231 "prefix a/ listed {got:?} next {:?}, want {want:?}",
232 all.next
233 ));
234 }
235 if all.entries.iter().any(|e| e.len == 0) {
236 return wrong("a listed entry reports length 0 for a non-empty value");
237 }
238
239 for (prefix, only) in [("a%", "a%z"), ("a[0]", "a[0]x")] {
242 let p = b.list(&full(prefix), None, 100).await?;
243 let got: Vec<String> = p.entries.iter().map(|e| e.key.clone()).collect();
244 if got != vec![full(only)] {
245 return wrong(format!(
246 "prefix {prefix} listed {got:?}, want only {:?}",
247 full(only)
248 ));
249 }
250 }
251
252 let mut seen = Vec::new();
254 let mut after: Option<String> = None;
255 for _ in 0..10 {
256 let p = b.list(&full("a/"), after.as_deref(), 2).await?;
257 if p.entries.len() > 2 {
258 return wrong(format!(
259 "a page of limit 2 held {} entries",
260 p.entries.len()
261 ));
262 }
263 seen.extend(p.entries.iter().map(|e| e.key.clone()));
264 match p.next {
265 Some(c) => after = Some(c),
266 None => break,
267 }
268 }
269 if seen != want {
270 return wrong(format!("paged listing returned {seen:?}, want {want:?}"));
271 }
272 Ok(())
273}
274
275async fn create_exclusive<B: BlobBackend>(b: &B, base: &str) -> Result<(), Failure> {
276 const CONTENDERS: usize = 16;
277 const ROUNDS: usize = 20;
278 let payload = |i: usize| vec![i as u8; 4096];
279 for round in 0..ROUNDS {
280 let key = format!("{base}r{round}");
281 let key = key.as_str();
282 let tasks: Vec<Task<'_, Result<bool, BlobError>>> = (0..CONTENDERS)
283 .map(|i| -> Task<'_, _> { Box::pin(b.create_exclusive(key, payload(i))) })
284 .collect();
285 let results = join_all(tasks).await;
286 let mut winners = Vec::new();
287 for (i, r) in results.into_iter().enumerate() {
288 if r? {
289 winners.push(i);
290 }
291 }
292 if winners.len() != 1 {
293 return wrong(format!(
294 "round {round}: {} of {CONTENDERS} concurrent creators succeeded, want exactly 1",
295 winners.len()
296 ));
297 }
298 match b.read(key).await? {
299 Some(v) if v == payload(winners[0]) => {}
300 other => {
301 return wrong(format!(
302 "round {round}: the stored value is not the winner's ({} bytes)",
303 other.map_or(0, |v| v.len())
304 ));
305 }
306 }
307 }
308 Ok(())
309}
310
311async fn replace_concurrent_read<B: BlobBackend>(b: &B, base: &str) -> Result<(), Failure> {
312 const LEN: usize = 65536;
313 const READERS: usize = 4;
314 const REPLACES: usize = 40;
315 let key = format!("{base}k");
316 let key = key.as_str();
317 let a = vec![b'A'; LEN];
318 let c = vec![b'B'; LEN];
319 b.replace_atomic(key, a.clone()).await?;
320 let done = std::cell::Cell::new(false);
321 let done = &done;
322 let mut tasks: Vec<Task<'_, Result<(), Failure>>> = Vec::new();
323 tasks.push(Box::pin(async move {
324 for i in 0..REPLACES {
325 let v = if i % 2 == 0 { c.clone() } else { a.clone() };
326 if let Err(e) = b.replace_atomic(key, v).await {
327 done.set(true);
328 return Err(Failure::from(e));
329 }
330 yield_now().await;
331 }
332 done.set(true);
333 Ok(())
334 }));
335 for _ in 0..READERS {
336 tasks.push(Box::pin(async move {
337 for _ in 0..(REPLACES * 20) {
338 match b.read(key).await {
339 Ok(Some(v)) => {
340 let uniform = v.len() == LEN
341 && v.iter().all(|x| *x == v[0])
342 && (v[0] == b'A' || v[0] == b'B');
343 if !uniform {
344 done.set(true);
345 return wrong(format!(
346 "a reader saw a mix of old and new ({} bytes, first {:?})",
347 v.len(),
348 v.first()
349 ));
350 }
351 }
352 Ok(None) => {
353 done.set(true);
354 return wrong("the key vanished during a replace");
355 }
356 Err(e) => {
357 done.set(true);
358 return Err(Failure::from(e));
359 }
360 }
361 if done.get() {
362 break;
363 }
364 yield_now().await;
365 }
366 Ok(())
367 }));
368 }
369 for r in join_all(tasks).await {
370 r?;
371 }
372 Ok(())
373}
374
375async fn append<B: BlobBackend>(b: &B, base: &str) -> Result<(), Failure> {
376 let k = format!("{base}k");
377 b.write(&k, b"head".to_vec()).await?;
378 for piece in [&b"a"[..], b"bc", b"", b"def"] {
379 b.append(&k, piece.to_vec()).await?;
380 }
381 match b.read(&k).await? {
382 Some(v) if v == b"headabcdef" => Ok(()),
383 other => wrong(format!(
384 "appends in order left {other:?}, want \"headabcdef\""
385 )),
386 }
387}
388
389async fn append_concurrent_whole<B: BlobBackend>(b: &B, base: &str) -> Result<(), Failure> {
390 const CONTENDERS: usize = 16;
391 const RECORDS: usize = 10;
392 const LEN: usize = 64;
393 let key = format!("{base}k");
394 let key = key.as_str();
395 b.write(key, Vec::new()).await?;
396 let tasks: Vec<Task<'_, Result<(), BlobError>>> = (0..CONTENDERS)
397 .map(|i| -> Task<'_, _> {
398 Box::pin(async move {
399 for _ in 0..RECORDS {
400 b.append(key, vec![i as u8; LEN]).await?;
401 yield_now().await;
402 }
403 Ok(())
404 })
405 })
406 .collect();
407 for r in join_all(tasks).await {
408 r?;
409 }
410 let all = b.read(key).await?.unwrap_or_default();
411 if all.len() != CONTENDERS * RECORDS * LEN {
412 return wrong(format!(
413 "{} bytes after {} appends of {LEN}, want {}",
414 all.len(),
415 CONTENDERS * RECORDS,
416 CONTENDERS * RECORDS * LEN
417 ));
418 }
419 let mut counts = [0usize; CONTENDERS];
420 for chunk in all.chunks(LEN) {
421 let first = chunk[0] as usize;
422 if first >= CONTENDERS || chunk.iter().any(|x| *x as usize != first) {
423 return wrong("a record is interleaved with another's bytes");
424 }
425 counts[first] += 1;
426 }
427 if counts.iter().any(|c| *c != RECORDS) {
428 return wrong(format!(
429 "records per appender {counts:?}, want {RECORDS} each"
430 ));
431 }
432 Ok(())
433}
434
435#[derive(Debug, Clone, PartialEq, Eq)]
437pub struct Certified {
438 pub leaf: Leaf,
440 pub row: Row,
442}
443
444pub async fn certify<B: BlobBackend>(
450 target: &B,
451 config: &Config,
452 metadata: &Config,
453) -> Vec<Certified> {
454 let mut out = Vec::new();
455 for leaf in Leaf::ALL {
456 let positive_passed = run_leaf(leaf, &MemoryBlob::new()).await.is_ok();
457 let mut known_bad_failed = true;
458 for d in leaf.defects() {
459 if run_leaf(leaf, &MemoryBlob::with_defect(*d)).await.is_ok() {
460 known_bad_failed = false;
461 }
462 }
463 let controls = Controls {
464 positive_passed,
465 known_bad_failed,
466 };
467 let result = run_leaf(leaf, target).await;
468 let mut md = metadata
469 .clone()
470 .with("test", format!("blob::{}", leaf.id()));
471 if let Ok(t) = persistant_core::Config::parse(leaf.trials()) {
472 for key in [
473 "contenders",
474 "rounds",
475 "value_bytes",
476 "readers",
477 "replaces",
478 "records",
479 ] {
480 if let Some(v) = t.get(key) {
481 md = md.with(key, v);
482 }
483 }
484 }
485 let id = CapabilityId::new(leaf.id()).expect("leaf id is valid");
486 let row = match (
487 &result,
488 controls.positive_passed && controls.known_bad_failed,
489 ) {
490 (Ok(()), true) => Verdict::Exact
491 .into_row(
492 id,
493 target.backend(),
494 target.version(),
495 config.clone(),
496 md,
497 controls,
498 )
499 .expect("both controls hold"),
500 (Ok(()), false) => off(
501 id,
502 target,
503 config,
504 md,
505 Reason::Untested,
506 format!("controls did not hold: {controls:?}"),
507 ),
508 (Err(Failure::Wrong(m)), _) => off(id, target, config, md, Reason::Wrong, m.clone()),
509 (Err(Failure::Backend(e)), _) => {
510 off(id, target, config, md, Reason::Missing, e.to_string())
511 }
512 };
513 out.push(Certified { leaf, row });
514 }
515 out
516}
517
518fn off<B: BlobBackend>(
519 subject: CapabilityId,
520 target: &B,
521 config: &Config,
522 metadata: Config,
523 reason: Reason,
524 note: String,
525) -> Row {
526 Row {
527 subject_kind: "capability".to_string(),
528 subject,
529 backend: target.backend().to_string(),
530 version: target.version().to_string(),
531 config: config.clone(),
532 enabled: false,
533 reason: Some(reason),
534 metadata,
535 note: note.replace(['\t', '\n'], " "),
536 extra: BTreeMap::new(),
537 }
538}
539
540#[cfg(test)]
541mod tests {
542 use super::*;
543
544 #[tokio::test]
545 async fn a_correct_backend_is_on_for_every_leaf() {
546 let rows = certify(&MemoryBlob::new(), &Config::new(), &Config::new()).await;
547 assert_eq!(rows.len(), Leaf::ALL.len());
548 for c in &rows {
549 assert!(c.row.enabled, "{}: {}", c.leaf.id(), c.row.note);
550 assert!(c.row.metadata.get("test").is_some());
551 }
552 }
553
554 #[tokio::test]
555 async fn every_defect_turns_its_own_leaf_off_as_wrong() {
556 for leaf in Leaf::ALL {
557 for d in leaf.defects() {
558 let rows =
559 certify(&MemoryBlob::with_defect(*d), &Config::new(), &Config::new()).await;
560 let mine = rows.iter().find(|c| c.leaf == leaf).unwrap();
561 assert!(!mine.row.enabled, "{d:?} must turn {} off", leaf.id());
562 assert_eq!(mine.row.reason, Some(Reason::Wrong), "{d:?}");
563 }
564 }
565 }
566
567 #[tokio::test]
568 async fn the_harness_can_tell_leaves_apart() {
569 let rows = certify(
571 &MemoryBlob::with_defect(Defect::ReadTruncates),
572 &Config::new(),
573 &Config::new(),
574 )
575 .await;
576 let by = |l: Leaf| rows.iter().find(|c| c.leaf == l).unwrap().row.enabled;
577 assert!(!by(Leaf::Read));
578 assert!(by(Leaf::Stat), "a read defect must not turn stat off");
579 }
580
581 #[tokio::test]
582 async fn rows_round_trip_through_the_matrix_and_the_gate_reads_them() {
583 let rows: Vec<Row> = certify(&MemoryBlob::new(), &Config::new(), &Config::new())
584 .await
585 .into_iter()
586 .map(|c| c.row)
587 .collect();
588 let m = persistant_core::Matrix::parse(&persistant_core::Matrix::from_rows(rows).to_tsv())
589 .unwrap();
590 let id = CapabilityId::new("blob.list").unwrap();
591 let v = env!("CARGO_PKG_VERSION");
592 assert!(m.enabled(&id, "memory", v, &Config::new()));
593 assert!(
594 !m.enabled(&id, "memory", "0.0.0-other", &Config::new()),
595 "unknown version is off"
596 );
597 }
598}