Skip to main content

datumesh_core/testkit/
mod.rs

1//! A conformance kit for [`Resolver`] implementations (feature `testkit`).
2//!
3//! A resolver is read-only, so the kit never writes through it. Instead it hands out
4//! [`Fixtures`] (key documents, descriptors, receipts, and the versions of stored cells), the
5//! caller loads them into the resolver under test however that resolver is filled (maps, a
6//! database, files), and the kit then verifies receipts through it and looks things up in
7//! it. Each check returns a [`Report`] of what failed; [`Kit::check`] runs all of them.
8//!
9//! ```
10//! use datumesh_core::testkit::{Fixtures, Kit};
11//! use datumesh_core::MapResolver;
12//!
13//! fn load(fx: &Fixtures) -> MapResolver {
14//!     let mut m = MapResolver::new();
15//!     for (fn_hash, bytes) in &fx.descriptors {
16//!         m.insert_descriptor(fn_hash.as_str(), bytes.as_slice());
17//!     }
18//!     for (hash, bytes) in &fx.receipts {
19//!         m.insert_receipt(hash.as_str(), bytes.as_slice());
20//!     }
21//!     // MapResolver keeps one value per cell: the versions come in tx order, so the
22//!     // latest one stays. A time-aware resolver loads them all.
23//!     for v in &fx.versions {
24//!         m.insert_value(v.cell_id.as_str(), v.value.clone());
25//!     }
26//!     m
27//! }
28//!
29//! if let Err(report) = Kit::new().run(load) {
30//!     panic!("{report}");
31//! }
32//! ```
33//!
34//! The checks:
35//!
36//! * [`check_lazy_calls`](Kit::check_lazy_calls): through a [`Recording`] wrapper,
37//!   [`verify`] asks the resolver only for what step 7 reaches: the descriptor once; the
38//!   inputs in order up to the first that fails; `evaluate` once, after 7.1-7.5, with the
39//!   receipt's cell and `asof`; and nothing at all when the receipt fails before step 7.
40//! * [`check_unavailable`](Kit::check_unavailable): an unknown `fn_hash` is
41//!   `E_FN_UNAVAILABLE`, an input receipt it does not have is `E_INPUT_UNAVAILABLE` (first or
42//!   second input), a function it cannot evaluate is `E_EVAL_UNAVAILABLE` (`ok`, level
43//!   `signed`), and looking up unknown or malformed hashes (empty, uppercase, too long,
44//!   non-hex, NUL, SQL or path syntax, non-ASCII) gives `None` without panicking.
45//! * [`check_bytes`](Kit::check_bytes): `descriptor` and `receipt` return exactly the bytes
46//!   loaded (all fixture bytes are canonical JSON, so a resolver that canonicalises what it
47//!   is given still passes).
48//! * [`check_asof`](Kit::check_asof): `evaluate` receives the receipt's `asof`, and stored
49//!   cells recompute. With [`Kit::time_aware`], a cell with versions at several times gives
50//!   the value of its latest version with `tx <= asof` (inclusive at each `tx`, `None` before
51//!   the first), both through `verify` and through direct `evaluate` calls.
52//!
53//! The kit expects a resolver to evaluate only what the fixtures give it: the versions of
54//! stored cells. Computed cells (`convert`, language `expr`) are not evaluable, so receipts
55//! of them end at `E_EVAL_UNAVAILABLE`. A panic in the resolver propagates to the caller.
56//!
57//! The fixtures come from the specification's vectors (revision `spec-0.3.0`; the files
58//! used are unchanged since `spec-0.2.1`), in canonical form, plus receipts signed for the
59//! kit with the vectors' seeds (feature `sign`): a second
60//! `fx_rate` cell (EUR/CHF) with versions at two times, and a `convert` receipt whose first
61//! input is not loaded. They are embedded in the crate (`src/testkit/data.rs`), generated and
62//! checked against `spec/` by the crate's test `tests/testkit_data.rs`.
63
64use std::borrow::Cow;
65use std::cell::RefCell;
66use std::fmt;
67
68use crate::descriptor::Descriptor;
69use crate::error::ErrorCode;
70use crate::keys::Keys;
71use crate::model::{Cell, Scalar};
72use crate::receipt::Receipt;
73use crate::verify::{verify, Level, Resolver, VerifyResult};
74
75#[rustfmt::skip]
76mod data;
77
78/// What the resolver under test is loaded with. All bytes are canonical JSON (JCS).
79///
80/// Load every descriptor under its `fn_hash`, every receipt under its hash, and the
81/// versions: for a time-aware resolver (a store), version `v` says that cell `v.cell_id`
82/// has, from transaction time `v.tx` on, the value of receipt `v.receipt_hash` (which is
83/// `v.value`); a resolver holding one value per cell keeps the latest. Versions are listed
84/// in `tx` order per cell.
85#[non_exhaustive]
86#[derive(Clone, Debug, PartialEq, Eq)]
87pub struct Fixtures {
88    /// Key documents, by host. Verification uses them (see [`Fixtures::keys`]); a resolver
89    /// does not need them.
90    pub key_documents: Vec<(String, Vec<u8>)>,
91    /// Descriptors: function hash, descriptor JSON text.
92    pub descriptors: Vec<(String, Vec<u8>)>,
93    /// Receipts: receipt hash, receipt JSON text.
94    pub receipts: Vec<(String, Vec<u8>)>,
95    /// Versions of stored cells, one per loaded receipt of a `stored` function.
96    pub versions: Vec<Version>,
97}
98
99impl Fixtures {
100    /// The key documents as [`Keys`].
101    pub fn keys(&self) -> Keys {
102        let mut keys = Keys::new();
103        for (host, body) in &self.key_documents {
104            keys.insert_response(host, 200, Some(body));
105        }
106        keys
107    }
108}
109
110/// A version of a stored cell: from `tx` on, the cell's value is that of the receipt.
111#[non_exhaustive]
112#[derive(Clone, Debug, PartialEq, Eq)]
113pub struct Version {
114    /// The cell id ([`Cell::id`]).
115    pub cell_id: String,
116    /// The transaction time, the receipt's `tx`.
117    pub tx: String,
118    /// The hash of the receipt stating the value.
119    pub receipt_hash: String,
120    /// The receipt's value.
121    pub value: Scalar,
122}
123
124/// A call made to a [`Resolver`], as [`Recording`] records it.
125///
126/// The variants are not named `Descriptor` and `Receipt`: a second public item named like
127/// the crate's [`Receipt`] would make rustc print `datumesh_core::Receipt` in its
128/// diagnostics, for everyone who enables the feature.
129#[non_exhaustive]
130#[derive(Clone, Debug, PartialEq, Eq)]
131pub enum Call {
132    /// [`Resolver::descriptor`] with this function hash.
133    LookupDescriptor(String),
134    /// [`Resolver::receipt`] with this receipt hash.
135    LookupReceipt(String),
136    /// [`Resolver::evaluate`] with the cell of this id, as of this time.
137    Evaluate {
138        /// [`Cell::id`] of the cell.
139        cell_id: String,
140        /// The `asof` argument.
141        asof: String,
142    },
143}
144
145/// A [`Resolver`] that forwards to another and records each call, in order.
146pub struct Recording<'a> {
147    inner: &'a dyn Resolver,
148    calls: RefCell<Vec<Call>>,
149}
150
151impl<'a> Recording<'a> {
152    /// Records the calls made to `inner`.
153    pub fn new(inner: &'a dyn Resolver) -> Recording<'a> {
154        Recording { inner, calls: RefCell::new(Vec::new()) }
155    }
156
157    /// The calls recorded so far.
158    pub fn calls(&self) -> Vec<Call> {
159        self.calls.borrow().clone()
160    }
161
162    /// The calls recorded so far, clearing the record.
163    pub fn take(&self) -> Vec<Call> {
164        self.calls.take()
165    }
166}
167
168impl fmt::Debug for Recording<'_> {
169    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
170        f.debug_struct("Recording").field("calls", &self.calls.borrow()).finish_non_exhaustive()
171    }
172}
173
174impl Resolver for Recording<'_> {
175    fn descriptor(&self, fn_hash: &str) -> Option<Cow<'_, [u8]>> {
176        self.calls.borrow_mut().push(Call::LookupDescriptor(fn_hash.to_string()));
177        self.inner.descriptor(fn_hash)
178    }
179
180    fn receipt(&self, hash: &str) -> Option<Cow<'_, [u8]>> {
181        self.calls.borrow_mut().push(Call::LookupReceipt(hash.to_string()));
182        self.inner.receipt(hash)
183    }
184
185    fn evaluate(&self, cell: &Cell, asof: &str) -> Option<Scalar> {
186        self.calls.borrow_mut().push(Call::Evaluate { cell_id: cell.id(), asof: asof.to_string() });
187        self.inner.evaluate(cell, asof)
188    }
189}
190
191/// The failures of a check, one line each: `<check>: <case>: <what>`, where `<check>` is
192/// `lazy_calls`, `unavailable`, `bytes`, `asof` (or `setup`, if the embedded fixtures could
193/// not be read, which the crate's tests rule out).
194#[derive(Clone, Debug, PartialEq, Eq)]
195pub struct Report {
196    failures: Vec<String>,
197}
198
199impl Report {
200    /// The failures, one per line of the report; never empty.
201    pub fn failures(&self) -> &[String] {
202        &self.failures
203    }
204}
205
206impl fmt::Display for Report {
207    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
208        for (i, line) in self.failures.iter().enumerate() {
209            if i > 0 {
210                f.write_str("\n")?;
211            }
212            f.write_str(line)?;
213        }
214        Ok(())
215    }
216}
217
218impl std::error::Error for Report {}
219
220/// A receipt the kit verifies, loaded or not.
221#[derive(Clone, Debug)]
222struct Case {
223    name: &'static str,
224    bytes: Vec<u8>,
225}
226
227/// The resolver test kit: the fixtures and the checks. See the [module](self) documentation.
228#[derive(Clone, Debug)]
229pub struct Kit {
230    fixtures: Fixtures,
231    cases: Vec<Case>,
232    time_aware: bool,
233    setup: Vec<String>,
234}
235
236impl Default for Kit {
237    fn default() -> Kit {
238        Kit::new()
239    }
240}
241
242/// The receipt that is not JSON.
243const NOT_JSON: &str = "not-json";
244
245impl Kit {
246    /// The kit, for a resolver that is not time-aware.
247    pub fn new() -> Kit {
248        let mut setup = Vec::new();
249        let mut fx =
250            Fixtures { key_documents: Vec::new(), descriptors: Vec::new(), receipts: Vec::new(), versions: Vec::new() };
251        let mut cases = Vec::new();
252        for (host, text) in data::KEY_DOCUMENTS {
253            fx.key_documents.push((host.to_string(), text.as_bytes().to_vec()));
254        }
255        let mut stored = Vec::new();
256        for (source, text) in data::DESCRIPTORS {
257            match Descriptor::from_json(text.as_bytes()) {
258                Ok(d) => {
259                    if d.lang == "stored" {
260                        stored.push(d.hash());
261                    }
262                    fx.descriptors.push((d.hash(), text.as_bytes().to_vec()));
263                }
264                Err(e) => setup.push(format!("setup: {source}: {e}")),
265            }
266        }
267        for (name, source, text) in data::RECEIPTS {
268            match Receipt::from_json(text.as_bytes()) {
269                Ok(r) => {
270                    if stored.contains(&r.cell().fn_hash) {
271                        fx.versions.push(Version {
272                            cell_id: r.cell().id(),
273                            tx: r.tx().to_string(),
274                            receipt_hash: r.hash().to_string(),
275                            value: r.value().clone(),
276                        });
277                    }
278                    fx.receipts.push((r.hash().to_string(), text.as_bytes().to_vec()));
279                }
280                Err(e) => setup.push(format!("setup: {source}: {e}")),
281            }
282            cases.push(Case { name, bytes: text.as_bytes().to_vec() });
283        }
284        for (name, _, text) in data::PROBES {
285            cases.push(Case { name, bytes: text.as_bytes().to_vec() });
286        }
287        cases.push(Case { name: NOT_JSON, bytes: b"{".to_vec() });
288        // Versions in tx order per cell (a stable sort keeps the cells' order otherwise).
289        fx.versions.sort_by(|a, b| a.cell_id.cmp(&b.cell_id).then_with(|| a.tx.cmp(&b.tx)));
290        Kit { fixtures: fx, cases, time_aware: false, setup }
291    }
292
293    /// Declares whether the resolver under test is time-aware: whether
294    /// [`evaluate`](Resolver::evaluate) gives, for a cell with several versions, the value of
295    /// the latest version with `tx <= asof`. Time-aware resolvers (a store) are held to it;
296    /// others (`MapResolver`) are not.
297    pub fn time_aware(mut self, yes: bool) -> Kit {
298        self.time_aware = yes;
299        self
300    }
301
302    /// The fixtures to load into the resolver under test.
303    pub fn fixtures(&self) -> &Fixtures {
304        &self.fixtures
305    }
306
307    /// Calls `load` with the fixtures, then runs every check on the resolver it returns.
308    pub fn run<R: Resolver, F: FnOnce(&Fixtures) -> R>(&self, load: F) -> Result<(), Report> {
309        self.setup_ok()?;
310        let r = load(&self.fixtures);
311        self.check(&r)
312    }
313
314    /// Runs every check on a resolver loaded with [`fixtures`](Kit::fixtures).
315    pub fn check(&self, resolver: &dyn Resolver) -> Result<(), Report> {
316        self.setup_ok()?;
317        let mut failures = Vec::new();
318        for check in [Kit::check_lazy_calls, Kit::check_unavailable, Kit::check_bytes, Kit::check_asof] {
319            if let Err(mut r) = check(self, resolver) {
320                failures.append(&mut r.failures);
321            }
322        }
323        report(failures)
324    }
325
326    /// Step 7 asks the resolver only for what it reaches, and nothing when the receipt
327    /// fails before step 7.
328    pub fn check_lazy_calls(&self, resolver: &dyn Resolver) -> Result<(), Report> {
329        self.setup_ok()?;
330        let keys = self.fixtures.keys();
331        let mut f = Failures::new("lazy_calls");
332        for case in &self.cases {
333            let rec = Recording::new(resolver);
334            let without = verify(&case.bytes, &keys, None, None);
335            let out = verify(&case.bytes, &keys, Some(&rec), None);
336            let calls = rec.take();
337            if !without.ok {
338                if !calls.is_empty() {
339                    f.push(
340                        case.name,
341                        format!("steps 1-6 fail ({:?}), yet the resolver was asked {calls:?}", without.code),
342                    );
343                }
344                if out != without {
345                    f.push(case.name, format!("a resolver changed a failure before step 7: {out:?}"));
346                }
347                continue;
348            }
349            match Receipt::from_json(&case.bytes) {
350                Ok(r) => {
351                    let want = expected_calls(&r, &out);
352                    if calls != want {
353                        f.push(case.name, format!("asked {calls:?}, step 7 needs {want:?} (result {:?})", out.code));
354                    }
355                }
356                Err(e) => f.push(case.name, format!("steps 1-6 passed but the receipt does not parse: {e}")),
357            }
358        }
359        for name in ["tampered-value", "duplicate-member", NOT_JSON] {
360            if let Some(case) = self.case(name) {
361                if verify(&case.bytes, &keys, None, None).ok {
362                    f.push(name, "expected to fail before step 7".to_string());
363                }
364            }
365        }
366        f.finish()
367    }
368
369    /// What the resolver does not have is unavailable, never a panic.
370    pub fn check_unavailable(&self, resolver: &dyn Resolver) -> Result<(), Report> {
371        self.setup_ok()?;
372        let mut f = Failures::new("unavailable");
373        let keys = self.fixtures.keys();
374        // Through verify.
375        self.expect(&mut f, resolver, &keys, "instant-no-valid-time", Want::Fails(ErrorCode::FnUnavailable));
376        self.expect(&mut f, resolver, &keys, "computed-two-inputs", Want::Fails(ErrorCode::InputUnavailable));
377        self.expect(&mut f, resolver, &keys, "first-input-missing", Want::Fails(ErrorCode::InputUnavailable));
378        self.expect(&mut f, resolver, &keys, "computed-with-lineage", Want::EvalUnavailable);
379        // Lookups of hashes that are unknown, or are not hashes at all.
380        let unknown_fn = self.receipt_of("instant-no-valid-time").map(|r| r.cell().fn_hash.clone());
381        let known_receipt = self.fixtures.receipts.first().map(|(h, _)| h.clone()).unwrap_or_default();
382        let known_fn = self.fixtures.descriptors.first().map(|(h, _)| h.clone()).unwrap_or_default();
383        let mut probes: Vec<String> = vec![
384            "0".repeat(64),
385            "f".repeat(64),
386            String::new(),
387            "abc".into(),
388            "g".repeat(64),
389            "\0".into(),
390            format!("{known_receipt}\0"),
391            "../../etc/passwd".into(),
392            "' OR '1'='1".into(),
393            "%".into(),
394            "_".repeat(64),
395            "é".repeat(32),
396            "a".repeat(100_000),
397            known_receipt.to_uppercase(),
398            known_fn.to_uppercase(),
399            format!("{known_receipt}0"),
400            format!(" {known_receipt}"),
401            format!("{known_fn}\n"),
402            known_receipt.get(..63).unwrap_or_default().to_string(),
403        ];
404        probes.extend(unknown_fn);
405        // Receipts that exist but are not loaded: an input of computed-two-inputs and the
406        // first input of first-input-missing.
407        for name in ["computed-two-inputs", "first-input-missing"] {
408            if let Some(r) = self.receipt_of(name) {
409                probes.extend(r.inputs().iter().filter(|h| !self.has_receipt(h)).cloned());
410            }
411        }
412        for p in &probes {
413            if resolver.descriptor(p).is_some() {
414                f.push("lookup", format!("descriptor({}) is not None", shown(p)));
415            }
416            if resolver.receipt(p).is_some() {
417                f.push("lookup", format!("receipt({}) is not None", shown(p)));
418            }
419        }
420        // evaluate with an asof that is not an instant: anything but a panic.
421        if let Some(r) = self.receipt_of("timeline-1") {
422            for asof in ["", "not-an-instant", "\0", "2024-03-16", "' OR '1'='1", "2024-03-16T09:00:00Z"] {
423                let _ = resolver.evaluate(r.cell(), asof);
424            }
425        }
426        f.finish()
427    }
428
429    /// `descriptor` and `receipt` return exactly the bytes loaded.
430    pub fn check_bytes(&self, resolver: &dyn Resolver) -> Result<(), Report> {
431        self.setup_ok()?;
432        let mut f = Failures::new("bytes");
433        for (h, bytes) in &self.fixtures.descriptors {
434            for _ in 0..2 {
435                match resolver.descriptor(h) {
436                    Some(b) if *b == **bytes => {}
437                    Some(b) => {
438                        f.push("descriptor", format!("{h}: {} bytes differ from the {} loaded", b.len(), bytes.len()))
439                    }
440                    None => f.push("descriptor", format!("{h}: None, but it was loaded")),
441                }
442            }
443        }
444        for (h, bytes) in &self.fixtures.receipts {
445            for _ in 0..2 {
446                match resolver.receipt(h) {
447                    Some(b) if *b == **bytes => {}
448                    Some(b) => {
449                        f.push("receipt", format!("{h}: {} bytes differ from the {} loaded", b.len(), bytes.len()))
450                    }
451                    None => f.push("receipt", format!("{h}: None, but it was loaded")),
452                }
453            }
454        }
455        f.finish()
456    }
457
458    /// `evaluate` receives the receipt's `asof`; for a time-aware resolver, a cell's value is
459    /// that of its latest version with `tx <= asof`.
460    pub fn check_asof(&self, resolver: &dyn Resolver) -> Result<(), Report> {
461        self.setup_ok()?;
462        let mut f = Failures::new("asof");
463        let keys = self.fixtures.keys();
464        // Pass-through: every evaluate call carries the receipt's cell and asof.
465        for case in &self.cases {
466            let Ok(r) = Receipt::from_json(&case.bytes) else { continue };
467            let rec = Recording::new(resolver);
468            let _ = verify(&case.bytes, &keys, Some(&rec), None);
469            for call in rec.take() {
470                if let Call::Evaluate { cell_id, asof } = call {
471                    if cell_id != r.cell().id() || asof != r.asof() {
472                        f.push(case.name, format!("evaluate got ({cell_id}, {asof}), not the receipt's cell and asof"));
473                    }
474                }
475            }
476        }
477        self.expect(&mut f, resolver, &keys, "stored-number", Want::Recomputed);
478        if !self.time_aware {
479            return f.finish();
480        }
481        // Through verify: each receipt of the EUR/CHF cell recomputes as of its own asof, and
482        // one as of a time before the first version cannot be evaluated.
483        for name in ["timeline-1", "timeline-1-later", "timeline-2"] {
484            self.expect(&mut f, resolver, &keys, name, Want::Recomputed);
485        }
486        self.expect(&mut f, resolver, &keys, "timeline-0", Want::EvalUnavailable);
487        // Directly: inclusive at each tx, None before the first.
488        let (Some(v1), Some(v2)) = (self.receipt_of("timeline-1"), self.receipt_of("timeline-2")) else {
489            f.push("timeline", "fixtures missing".to_string());
490            return f.finish();
491        };
492        let cell = v1.cell();
493        let points: [(&str, Option<&Scalar>); 7] = [
494            ("0001-01-01T00:00:00.000000Z", None),
495            ("2024-03-15T15:59:59.999999Z", None),
496            (v1.tx(), Some(v1.value())),
497            ("2024-03-17T00:00:00.000000Z", Some(v1.value())),
498            ("2024-03-18T09:59:59.999999Z", Some(v1.value())),
499            (v2.tx(), Some(v2.value())),
500            ("9999-12-31T23:59:59.999999Z", Some(v2.value())),
501        ];
502        for (asof, want) in points {
503            let got = resolver.evaluate(cell, asof);
504            if got.as_ref() != want {
505                f.push("timeline", format!("evaluate as of {asof} gave {got:?}, want {want:?}"));
506            }
507        }
508        f.finish()
509    }
510
511    fn setup_ok(&self) -> Result<(), Report> {
512        if self.setup.is_empty() {
513            Ok(())
514        } else {
515            Err(Report { failures: self.setup.clone() })
516        }
517    }
518
519    fn case(&self, name: &str) -> Option<&Case> {
520        self.cases.iter().find(|c| c.name == name)
521    }
522
523    fn receipt_of(&self, name: &str) -> Option<Receipt> {
524        self.case(name).and_then(|c| Receipt::from_json(&c.bytes).ok())
525    }
526
527    fn has_receipt(&self, hash: &str) -> bool {
528        self.fixtures.receipts.iter().any(|(h, _)| h == hash)
529    }
530
531    /// Verifies case `name` through `resolver` and records a failure unless it ends as wanted.
532    fn expect(&self, f: &mut Failures, resolver: &dyn Resolver, keys: &Keys, name: &str, want: Want) {
533        let Some(case) = self.case(name) else {
534            f.push(name, "no such fixture".to_string());
535            return;
536        };
537        let out = verify(&case.bytes, keys, Some(resolver), None);
538        let good = match want {
539            Want::Recomputed => out.ok && out.level == Some(Level::Recomputed) && out.code.is_none(),
540            Want::EvalUnavailable => {
541                out.ok && out.level == Some(Level::Signed) && out.code == Some(ErrorCode::EvalUnavailable)
542            }
543            Want::Fails(code) => !out.ok && out.code == Some(code),
544        };
545        if !good {
546            f.push(
547                name,
548                format!(
549                    "want {want:?}, got ok={} level={:?} code={:?} {} {}",
550                    out.ok, out.level, out.code, out.path, out.detail
551                ),
552            );
553        }
554    }
555}
556
557/// How a case should end.
558#[derive(Clone, Copy, Debug)]
559enum Want {
560    Recomputed,
561    EvalUnavailable,
562    Fails(ErrorCode),
563}
564
565/// The calls step 7 needs for receipt `r` (which passed steps 1-6), given how it ended.
566fn expected_calls(r: &Receipt, out: &VerifyResult) -> Vec<Call> {
567    let mut calls = vec![Call::LookupDescriptor(r.cell().fn_hash.clone())];
568    let all_inputs = || r.inputs().iter().map(|h| Call::LookupReceipt(h.clone()));
569    let evaluate = Call::Evaluate { cell_id: r.cell().id(), asof: r.asof().to_string() };
570    match out.code {
571        // 7.6 compared, or could not evaluate.
572        None | Some(ErrorCode::ValueMismatch | ErrorCode::EvalUnavailable) => {
573            calls.extend(all_inputs());
574            calls.push(evaluate);
575        }
576        // 7.5: up to and including the failing input.
577        Some(_) if out.path.starts_with("$.inputs.") => {
578            let failing = &out.path["$.inputs.".len()..];
579            let n = r.inputs().iter().position(|h| h == failing).map_or(r.inputs().len(), |i| i + 1);
580            calls.extend(all_inputs().take(n));
581        }
582        // 7.5's last check: tx against the latest input tx.
583        Some(ErrorCode::TimeOrder) if out.path == "$.tx" => calls.extend(all_inputs()),
584        // 7.1-7.4.
585        Some(_) => {}
586    }
587    calls
588}
589
590/// A hash-like argument as shown in a report: long ones shortened.
591fn shown(s: &str) -> String {
592    if s.chars().count() > 80 {
593        format!("{:?}… ({} bytes)", s.chars().take(16).collect::<String>(), s.len())
594    } else {
595        format!("{s:?}")
596    }
597}
598
599struct Failures {
600    check: &'static str,
601    lines: Vec<String>,
602}
603
604impl Failures {
605    fn new(check: &'static str) -> Failures {
606        Failures { check, lines: Vec::new() }
607    }
608
609    fn push(&mut self, case: &str, what: String) {
610        self.lines.push(format!("{}: {case}: {what}", self.check));
611    }
612
613    fn finish(self) -> Result<(), Report> {
614        report(self.lines)
615    }
616}
617
618fn report(failures: Vec<String>) -> Result<(), Report> {
619    if failures.is_empty() {
620        Ok(())
621    } else {
622        Err(Report { failures })
623    }
624}