1use crate::{Dialect, RelError, RelationalBackend, RelationalTx, Row, Value};
31use persistant_core::{CapabilityId, Config, Controls, Reason, Row as MatrixRow, Verdict};
32use std::collections::BTreeMap;
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36#[non_exhaustive]
37pub enum Leaf {
38 Values,
40 Transaction,
42 UniqueClassified,
44 ForeignKey,
46 CheckConstraint,
48 IsolationNoDirtyRead,
50 Savepoint,
52 RowLock,
54}
55
56impl Leaf {
57 pub const ALL: [Leaf; 8] = [
59 Leaf::Values,
60 Leaf::Transaction,
61 Leaf::UniqueClassified,
62 Leaf::ForeignKey,
63 Leaf::CheckConstraint,
64 Leaf::IsolationNoDirtyRead,
65 Leaf::Savepoint,
66 Leaf::RowLock,
67 ];
68
69 pub fn id(self) -> &'static str {
71 match self {
72 Leaf::Values => "rel.values",
73 Leaf::Transaction => "rel.transaction",
74 Leaf::UniqueClassified => "rel.error.unique",
75 Leaf::ForeignKey => "rel.foreign_key",
76 Leaf::CheckConstraint => "rel.check_constraint",
77 Leaf::IsolationNoDirtyRead => "rel.isolation.no_dirty_read",
78 Leaf::Savepoint => "rel.savepoint",
79 Leaf::RowLock => "rel.row_lock",
80 }
81 }
82
83 fn defects(self) -> &'static [Defect] {
84 match self {
85 Leaf::Values => &[Defect::LossyBytes],
86 Leaf::Transaction | Leaf::IsolationNoDirtyRead | Leaf::Savepoint | Leaf::RowLock => {
87 &[Defect::NoTransactions]
88 }
89 Leaf::UniqueClassified => &[Defect::UnclassifiedErrors],
90 Leaf::ForeignKey | Leaf::CheckConstraint => &[],
91 }
92 }
93
94 fn has_unconstrained_control(self) -> bool {
96 matches!(self, Leaf::ForeignKey | Leaf::CheckConstraint)
97 }
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102#[non_exhaustive]
103pub enum Defect {
104 NoTransactions,
107 LossyBytes,
109 UnclassifiedErrors,
111}
112
113#[derive(Debug)]
115pub struct Defective<'a, R> {
116 inner: &'a R,
117 defect: Defect,
118}
119
120impl<'a, R: RelationalBackend> Defective<'a, R> {
121 pub fn new(inner: &'a R, defect: Defect) -> Self {
123 Defective { inner, defect }
124 }
125}
126
127pub enum DefectiveTx<'a, R: RelationalBackend + 'a> {
129 Real(R::Tx<'a>, Defect),
131 Direct(&'a R),
133}
134
135impl<R: RelationalBackend> RelationalBackend for Defective<'_, R> {
136 type Tx<'a>
137 = DefectiveTx<'a, R>
138 where
139 Self: 'a;
140
141 fn backend(&self) -> &str {
142 self.inner.backend()
143 }
144
145 fn version(&self) -> &str {
146 self.inner.version()
147 }
148
149 fn dialect(&self) -> Dialect {
150 self.inner.dialect()
151 }
152
153 async fn execute(&self, sql: &str, params: &[Value]) -> Result<u64, RelError> {
154 self.inner
155 .execute(sql, &lossy(self.defect, params))
156 .await
157 .map_err(|e| unclassify(self.defect, e))
158 }
159
160 async fn query(&self, sql: &str, params: &[Value]) -> Result<Vec<Row>, RelError> {
161 self.inner
162 .query(sql, &lossy(self.defect, params))
163 .await
164 .map_err(|e| unclassify(self.defect, e))
165 }
166
167 async fn begin(&self) -> Result<DefectiveTx<'_, R>, RelError> {
168 if self.defect == Defect::NoTransactions {
169 return Ok(DefectiveTx::Direct(self.inner));
170 }
171 Ok(DefectiveTx::Real(
172 self.inner
173 .begin()
174 .await
175 .map_err(|e| unclassify(self.defect, e))?,
176 self.defect,
177 ))
178 }
179}
180
181fn lossy(defect: Defect, params: &[Value]) -> Vec<Value> {
182 params
183 .iter()
184 .map(|p| match (defect, p) {
185 (Defect::LossyBytes, Value::Bytes(b)) => {
186 Value::Bytes(b.iter().take(1).copied().collect())
187 }
188 _ => p.clone(),
189 })
190 .collect()
191}
192
193fn unclassify(defect: Defect, e: RelError) -> RelError {
194 if defect == Defect::UnclassifiedErrors {
195 RelError::Backend(e.to_string())
196 } else {
197 e
198 }
199}
200
201impl<R: RelationalBackend> RelationalTx for DefectiveTx<'_, R> {
202 async fn execute(&mut self, sql: &str, params: &[Value]) -> Result<u64, RelError> {
203 match self {
204 DefectiveTx::Real(tx, d) => tx
205 .execute(sql, &lossy(*d, params))
206 .await
207 .map_err(|e| unclassify(*d, e)),
208 DefectiveTx::Direct(db) => db.execute(sql, params).await,
209 }
210 }
211
212 async fn query(&mut self, sql: &str, params: &[Value]) -> Result<Vec<Row>, RelError> {
213 match self {
214 DefectiveTx::Real(tx, d) => tx
215 .query(sql, &lossy(*d, params))
216 .await
217 .map_err(|e| unclassify(*d, e)),
218 DefectiveTx::Direct(db) => db.query(sql, params).await,
219 }
220 }
221
222 async fn commit(self) -> Result<(), RelError> {
223 match self {
224 DefectiveTx::Real(tx, d) => tx.commit().await.map_err(|e| unclassify(d, e)),
225 DefectiveTx::Direct(_) => Ok(()),
226 }
227 }
228
229 async fn rollback(self) -> Result<(), RelError> {
230 match self {
231 DefectiveTx::Real(tx, d) => tx.rollback().await.map_err(|e| unclassify(d, e)),
232 DefectiveTx::Direct(_) => Ok(()),
233 }
234 }
235}
236
237#[derive(Debug, Clone, PartialEq, Eq)]
239pub enum Failure {
240 Wrong(String),
242 Backend(RelError),
244}
245
246impl From<RelError> for Failure {
247 fn from(e: RelError) -> Self {
248 Failure::Backend(e)
249 }
250}
251
252fn wrong<T>(msg: impl Into<String>) -> Result<T, Failure> {
253 Err(Failure::Wrong(msg.into()))
254}
255
256#[derive(Clone, Copy)]
259enum Ty {
260 Int,
261 Text,
262 Bytes,
263 Float,
264}
265
266struct Sql(Dialect);
267
268impl Sql {
269 fn p(&self, n: usize) -> String {
271 match self.0 {
272 Dialect::Postgres => format!("${n}"),
273 Dialect::Sqlite => format!("?{n}"),
274 _ => "?".to_string(),
275 }
276 }
277
278 fn ty(&self, t: Ty) -> &'static str {
279 match (&self.0, t) {
280 (_, Ty::Int) => "BIGINT",
281 (Dialect::Postgres, Ty::Text) => "TEXT",
282 (Dialect::Sqlite, Ty::Text) => "TEXT",
283 (_, Ty::Text) => "VARCHAR(255)",
284 (Dialect::Postgres, Ty::Bytes) => "BYTEA",
285 (Dialect::Sqlite, Ty::Bytes) => "BLOB",
286 (_, Ty::Bytes) => "LONGBLOB",
287 (Dialect::Postgres, Ty::Float) => "DOUBLE PRECISION",
288 (_, Ty::Float) => "DOUBLE",
289 }
290 }
291
292 fn tail(&self) -> &'static str {
294 match self.0 {
295 Dialect::MySql | Dialect::MariaDb => " ENGINE=InnoDB",
296 _ => "",
297 }
298 }
299
300 fn pk(&self, name: &str) -> String {
302 format!("{name} BIGINT NOT NULL PRIMARY KEY")
303 }
304}
305
306fn supported(d: &Dialect) -> Result<(), Failure> {
307 match d {
308 Dialect::Postgres | Dialect::Sqlite | Dialect::MySql | Dialect::MariaDb => Ok(()),
309 other => Err(Failure::Backend(RelError::Unsupported(format!(
310 "the relational contract tests have no statements for {other}"
311 )))),
312 }
313}
314
315async fn drop_tables<R: RelationalBackend>(b: &R, names: &[&str]) -> Result<(), RelError> {
316 for n in names {
317 b.execute(&format!("DROP TABLE IF EXISTS {n}"), &[]).await?;
318 }
319 Ok(())
320}
321
322pub async fn run_leaf<R: RelationalBackend>(
326 leaf: Leaf,
327 b: &R,
328 constrained: bool,
329) -> Result<(), Failure> {
330 let d = b.dialect();
331 supported(&d)?;
332 let s = Sql(d);
333 match leaf {
334 Leaf::Values => values(b, &s).await,
335 Leaf::Transaction => transaction(b, &s).await,
336 Leaf::UniqueClassified => unique(b, &s).await,
337 Leaf::ForeignKey => foreign_key(b, &s, constrained).await,
338 Leaf::CheckConstraint => check(b, &s, constrained).await,
339 Leaf::IsolationNoDirtyRead => no_dirty_read(b, &s).await,
340 Leaf::Savepoint => savepoint(b, &s).await,
341 Leaf::RowLock => row_lock(b, &s).await,
342 }
343}
344
345async fn values<R: RelationalBackend>(b: &R, s: &Sql) -> Result<(), Failure> {
346 const T: &str = "persistant_rel_values";
347 drop_tables(b, &[T]).await?;
348 b.execute(
349 &format!(
350 "CREATE TABLE {T} ({}, i {}, t {}, b {}, f {}){}",
351 s.pk("id"),
352 s.ty(Ty::Int),
353 s.ty(Ty::Text),
354 s.ty(Ty::Bytes),
355 s.ty(Ty::Float),
356 s.tail()
357 ),
358 &[],
359 )
360 .await?;
361 let cases: Vec<(i64, Value, Value, Value, Value)> = vec![
362 (
363 1,
364 Value::Int(i64::MIN),
365 Value::Text("ünï ✓ \u{1F980}".to_string()),
366 Value::Bytes(vec![0x00, 0xFF, 0x80, 0x00, 0x7F]),
367 Value::Float(1.5),
368 ),
369 (
370 2,
371 Value::Int(i64::MAX),
372 Value::Text(String::new()),
373 Value::Bytes(vec![]),
374 Value::Float(-0.25),
375 ),
376 (3, Value::Null, Value::Null, Value::Null, Value::Null),
377 ];
378 for (id, i, t, bytes, f) in &cases {
379 b.execute(
380 &format!(
381 "INSERT INTO {T} (id, i, t, b, f) VALUES ({}, {}, {}, {}, {})",
382 s.p(1),
383 s.p(2),
384 s.p(3),
385 s.p(4),
386 s.p(5)
387 ),
388 &[
389 Value::Int(*id),
390 i.clone(),
391 t.clone(),
392 bytes.clone(),
393 f.clone(),
394 ],
395 )
396 .await?;
397 }
398 let rows = b
399 .query(&format!("SELECT id, i, t, b, f FROM {T} ORDER BY id"), &[])
400 .await?;
401 drop_tables(b, &[T]).await?;
402 if rows.len() != cases.len() {
403 return wrong(format!("wrote {} rows, read {}", cases.len(), rows.len()));
404 }
405 for (row, (id, i, t, bytes, f)) in rows.iter().zip(&cases) {
406 let want = [
407 Value::Int(*id),
408 i.clone(),
409 t.clone(),
410 bytes.clone(),
411 f.clone(),
412 ];
413 if row.values != want {
415 return wrong(format!("row {id}: wrote {want:?}, read {:?}", row.values));
416 }
417 }
418 Ok(())
419}
420
421async fn count<R: RelationalBackend>(b: &R, table: &str) -> Result<i64, Failure> {
422 let r = b
423 .query(&format!("SELECT COUNT(*) FROM {table}"), &[])
424 .await?;
425 match r.first().and_then(|r| r.int(0)) {
426 Some(n) => Ok(n),
427 None => wrong("COUNT(*) did not come back as an integer"),
428 }
429}
430
431async fn transaction<R: RelationalBackend>(b: &R, s: &Sql) -> Result<(), Failure> {
432 const T: &str = "persistant_rel_tx";
433 drop_tables(b, &[T]).await?;
434 b.execute(
435 &format!(
436 "CREATE TABLE {T} ({}, n BIGINT NOT NULL){}",
437 s.pk("id"),
438 s.tail()
439 ),
440 &[],
441 )
442 .await?;
443 let ins = format!("INSERT INTO {T} (id, n) VALUES ({}, {})", s.p(1), s.p(2));
444 let result = async {
445 let mut tx = b.begin().await?;
447 tx.execute(&ins, &[Value::Int(1), Value::Int(10)]).await?;
448 let own = tx.query(&format!("SELECT COUNT(*) FROM {T}"), &[]).await?;
449 if own.first().and_then(|r| r.int(0)) != Some(1) {
450 return wrong("a transaction cannot see its own write");
451 }
452 tx.commit().await?;
453 if count(b, T).await? != 1 {
454 return wrong("a committed write is not visible");
455 }
456 let mut tx = b.begin().await?;
458 tx.execute(&ins, &[Value::Int(2), Value::Int(20)]).await?;
459 tx.rollback().await?;
460 if count(b, T).await? != 1 {
461 return wrong("a rolled-back write is visible");
462 }
463 {
465 let mut tx = b.begin().await?;
466 tx.execute(&ins, &[Value::Int(3), Value::Int(30)]).await?;
467 }
468 if count(b, T).await? != 1 {
469 return wrong("a transaction dropped without commit was not rolled back");
470 }
471 Ok(())
472 }
473 .await;
474 drop_tables(b, &[T]).await?;
475 result
476}
477
478async fn no_dirty_read<R: RelationalBackend>(b: &R, s: &Sql) -> Result<(), Failure> {
479 const T: &str = "persistant_rel_isolation";
480 drop_tables(b, &[T]).await?;
481 b.execute(
482 &format!(
483 "CREATE TABLE {T} ({}, n BIGINT NOT NULL){}",
484 s.pk("id"),
485 s.tail()
486 ),
487 &[],
488 )
489 .await?;
490 let ins = format!("INSERT INTO {T} (id, n) VALUES ({}, {})", s.p(1), s.p(2));
491 let result = async {
492 let mut tx = b.begin().await?;
493 tx.execute(&ins, &[Value::Int(1), Value::Int(1)]).await?;
494 let outside = count(b, T).await?;
496 if outside != 0 {
497 let _ = tx.rollback().await;
498 return wrong(format!(
499 "another connection saw {outside} uncommitted row(s) (dirty read)"
500 ));
501 }
502 tx.commit().await?;
503 if count(b, T).await? != 1 {
504 return wrong("a committed write is not visible to another connection");
505 }
506 Ok(())
507 }
508 .await;
509 drop_tables(b, &[T]).await?;
510 result
511}
512
513async fn savepoint<R: RelationalBackend>(b: &R, s: &Sql) -> Result<(), Failure> {
514 const T: &str = "persistant_rel_savepoint";
515 drop_tables(b, &[T]).await?;
516 b.execute(
517 &format!(
518 "CREATE TABLE {T} ({}, n BIGINT NOT NULL){}",
519 s.pk("id"),
520 s.tail()
521 ),
522 &[],
523 )
524 .await?;
525 let ins = format!("INSERT INTO {T} (id, n) VALUES ({}, {})", s.p(1), s.p(2));
526 let result = async {
527 let mut tx = b.begin().await?;
528 tx.execute(&ins, &[Value::Int(1), Value::Int(1)]).await?;
529 tx.execute("SAVEPOINT persistant_sp", &[]).await?;
530 tx.execute(&ins, &[Value::Int(2), Value::Int(2)]).await?;
531 tx.execute("ROLLBACK TO SAVEPOINT persistant_sp", &[])
532 .await?;
533 tx.commit().await?;
534 let rows = b
535 .query(&format!("SELECT id FROM {T} ORDER BY id"), &[])
536 .await?;
537 let ids: Vec<Option<i64>> = rows.iter().map(|r| r.int(0)).collect();
538 if ids != [Some(1)] {
539 return wrong(format!(
540 "after ROLLBACK TO a savepoint and commit, expected only row 1, found {ids:?}"
541 ));
542 }
543 Ok(())
544 }
545 .await;
546 drop_tables(b, &[T]).await?;
547 result
548}
549
550async fn row_lock<R: RelationalBackend>(b: &R, s: &Sql) -> Result<(), Failure> {
551 const T: &str = "persistant_rel_row_lock";
552 if matches!(s.0, Dialect::Sqlite) {
553 return Err(Failure::Backend(RelError::Unsupported(
554 "SQLite has no row locks: a write locks the whole database".to_string(),
555 )));
556 }
557 drop_tables(b, &[T]).await?;
558 b.execute(
559 &format!(
560 "CREATE TABLE {T} ({}, n BIGINT NOT NULL){}",
561 s.pk("id"),
562 s.tail()
563 ),
564 &[],
565 )
566 .await?;
567 b.execute(
568 &format!("INSERT INTO {T} (id, n) VALUES ({}, {})", s.p(1), s.p(2)),
569 &[Value::Int(1), Value::Int(1)],
570 )
571 .await?;
572 let lock = format!("SELECT id FROM {T} WHERE id = 1 FOR UPDATE");
573 let probe = format!("SELECT id FROM {T} WHERE id = 1 FOR UPDATE SKIP LOCKED");
574 let result = async {
575 let mut tx = b.begin().await?;
576 if tx.query(&lock, &[]).await?.len() != 1 {
577 return wrong("FOR UPDATE did not return the row it locks");
578 }
579 let held = b.query(&probe, &[]).await;
581 let held = match held {
582 Ok(rows) => rows.len(),
583 Err(e) => {
584 let _ = tx.rollback().await;
585 return Err(e.into());
586 }
587 };
588 if held != 0 {
589 let _ = tx.rollback().await;
590 return wrong("another connection locked a row an open transaction holds FOR UPDATE");
591 }
592 tx.commit().await?;
593 if b.query(&probe, &[]).await?.len() != 1 {
594 return wrong("the row is still locked after the transaction committed");
595 }
596 Ok(())
597 }
598 .await;
599 drop_tables(b, &[T]).await?;
600 result
601}
602
603async fn unique<R: RelationalBackend>(b: &R, s: &Sql) -> Result<(), Failure> {
604 const T: &str = "persistant_rel_unique";
605 drop_tables(b, &[T]).await?;
606 b.execute(
607 &format!(
608 "CREATE TABLE {T} ({}, n BIGINT NOT NULL){}",
609 s.pk("id"),
610 s.tail()
611 ),
612 &[],
613 )
614 .await?;
615 let ins = format!("INSERT INTO {T} (id, n) VALUES ({}, {})", s.p(1), s.p(2));
616 let result = async {
617 b.execute(&ins, &[Value::Int(1), Value::Int(1)]).await?;
618 match b.execute(&ins, &[Value::Int(1), Value::Int(2)]).await {
619 Err(RelError::UniqueViolation { .. }) => {}
620 other => return wrong(format!("a duplicate key came back as {other:?}")),
621 }
622 let mut tx = b.begin().await?;
624 let dup = tx.execute(&ins, &[Value::Int(1), Value::Int(3)]).await;
625 let _ = tx.rollback().await;
626 match dup {
627 Err(RelError::UniqueViolation { .. }) => Ok(()),
628 other => wrong(format!(
629 "a duplicate key in a transaction came back as {other:?}"
630 )),
631 }
632 }
633 .await;
634 drop_tables(b, &[T]).await?;
635 result
636}
637
638async fn foreign_key<R: RelationalBackend>(
639 b: &R,
640 s: &Sql,
641 constrained: bool,
642) -> Result<(), Failure> {
643 const P: &str = "persistant_rel_fk_parent";
644 const C: &str = "persistant_rel_fk_child";
645 drop_tables(b, &[C, P]).await?;
646 b.execute(
647 &format!("CREATE TABLE {P} ({}){}", s.pk("id"), s.tail()),
648 &[],
649 )
650 .await?;
651 let refs = if constrained {
652 format!(", FOREIGN KEY (pid) REFERENCES {P}(id)")
653 } else {
654 String::new()
655 };
656 b.execute(
657 &format!(
658 "CREATE TABLE {C} ({}, pid BIGINT NOT NULL{refs}){}",
659 s.pk("id"),
660 s.tail()
661 ),
662 &[],
663 )
664 .await?;
665 let result = async {
666 b.execute(
667 &format!("INSERT INTO {P} (id) VALUES ({})", s.p(1)),
668 &[Value::Int(1)],
669 )
670 .await?;
671 let ins = format!("INSERT INTO {C} (id, pid) VALUES ({}, {})", s.p(1), s.p(2));
672 b.execute(&ins, &[Value::Int(1), Value::Int(1)]).await?;
673 match b.execute(&ins, &[Value::Int(2), Value::Int(99)]).await {
674 Err(RelError::ForeignKeyViolation { .. }) => {}
675 Ok(_) => return wrong("a reference to a missing row was accepted"),
676 other => {
677 return wrong(format!(
678 "a reference to a missing row came back as {other:?}"
679 ));
680 }
681 }
682 match b
683 .execute(
684 &format!("DELETE FROM {P} WHERE id = {}", s.p(1)),
685 &[Value::Int(1)],
686 )
687 .await
688 {
689 Err(RelError::ForeignKeyViolation { .. }) => Ok(()),
690 Ok(_) => wrong("a referenced row was deleted"),
691 other => wrong(format!("deleting a referenced row came back as {other:?}")),
692 }
693 }
694 .await;
695 drop_tables(b, &[C, P]).await?;
696 result
697}
698
699async fn check<R: RelationalBackend>(b: &R, s: &Sql, constrained: bool) -> Result<(), Failure> {
700 const T: &str = "persistant_rel_check";
701 drop_tables(b, &[T]).await?;
702 let ck = if constrained { " CHECK (n >= 0)" } else { "" };
703 b.execute(
704 &format!(
705 "CREATE TABLE {T} ({}, n BIGINT NOT NULL{ck}){}",
706 s.pk("id"),
707 s.tail()
708 ),
709 &[],
710 )
711 .await?;
712 let ins = format!("INSERT INTO {T} (id, n) VALUES ({}, {})", s.p(1), s.p(2));
713 let result = async {
714 b.execute(&ins, &[Value::Int(1), Value::Int(0)]).await?;
715 match b.execute(&ins, &[Value::Int(2), Value::Int(-1)]).await {
716 Err(RelError::CheckViolation { .. }) => Ok(()),
717 Ok(_) => wrong("a row violating a CHECK was accepted"),
718 other => wrong(format!("a row violating a CHECK came back as {other:?}")),
719 }
720 }
721 .await;
722 drop_tables(b, &[T]).await?;
723 result
724}
725
726#[derive(Debug, Clone, PartialEq, Eq)]
730pub struct Certified {
731 pub leaf: Leaf,
733 pub row: MatrixRow,
735}
736
737pub async fn certify<R: RelationalBackend>(
743 target: &R,
744 config: &Config,
745 metadata: &Config,
746) -> Vec<Certified> {
747 let mut out = Vec::new();
748 for leaf in Leaf::ALL {
749 let result = run_leaf(leaf, target, true).await;
750 let positive_passed = !matches!(
751 &result,
752 Err(Failure::Backend(
753 RelError::Unsupported(_) | RelError::Unavailable(_)
754 ))
755 );
756 let mut known_bad_failed = true;
757 for d in leaf.defects() {
758 let bad = Defective::new(target, *d);
759 if run_leaf(leaf, &bad, true).await.is_ok() {
760 known_bad_failed = false;
761 }
762 }
763 if leaf.has_unconstrained_control() && run_leaf(leaf, target, false).await.is_ok() {
764 known_bad_failed = false;
765 }
766 let controls = Controls {
767 positive_passed,
768 known_bad_failed,
769 };
770 let id = CapabilityId::new(leaf.id()).expect("leaf id is valid");
771 let md = metadata.clone().with("test", format!("rel::{}", leaf.id()));
772 let off = |reason: Reason, note: String| MatrixRow {
773 subject_kind: "capability".to_string(),
774 subject: id.clone(),
775 backend: target.backend().to_string(),
776 version: target.version().to_string(),
777 config: config.clone(),
778 enabled: false,
779 reason: Some(reason),
780 metadata: md.clone(),
781 note: note.replace(['\t', '\n'], " "),
782 extra: BTreeMap::new(),
783 };
784 let row = match (
785 &result,
786 controls.positive_passed && controls.known_bad_failed,
787 ) {
788 (Ok(()), true) => Verdict::Exact
789 .into_row(
790 id.clone(),
791 target.backend(),
792 target.version(),
793 config.clone(),
794 md.clone(),
795 controls,
796 )
797 .expect("both controls hold"),
798 (Ok(()), false) => off(
799 Reason::Untested,
800 format!("controls did not hold: {controls:?}"),
801 ),
802 (Err(Failure::Wrong(m)), _) => off(Reason::Wrong, m.clone()),
803 (Err(Failure::Backend(e)), _) => off(Reason::Missing, e.to_string()),
804 };
805 out.push(Certified { leaf, row });
806 }
807 out
808}