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