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 that answers as of the requested time under the kit's
51//!   [`AnswerRule`] (`None` before the first), both through `verify` and through direct
52//!   `evaluate` calls.
53//!
54//! The answer rule ([`Kit::answer_rule`]) says when a version of a stored cell answers as of
55//! a time `A`:
56//!
57//! * [`AnswerRule::TxAndAsof`], the default: its `tx <= A` **and** its receipt's
58//!   `asof <= A`. This is what SPEC 5.5 step 5 (D31 revised) requires of an `ok` response
59//!   to a request with `asof` `A`: the receipt's `asof`, and so the version answering, at or
60//!   before `A`. A version is not an answer between its `tx` and the time it became known.
61//! * [`AnswerRule::TxOnly`]: its `tx <= A`, whenever its receipt was made (the kit's only
62//!   rule before 0.3.3). A time-aware resolver with this rule must say so.
63//!
64//! In the fixtures, the EUR/CHF cell's first version has `tx` 2024-03-15T16:00 and receipt
65//! `asof` 2024-03-16T09:00: under `TxAndAsof` it answers from 2024-03-16T09:00 on (and nothing
66//! does before); under `TxOnly`, from 2024-03-15T16:00 on. The rule matters only to a resolver
67//! declared time-aware. Receipts recompute through `verify` the same under both, since each
68//! receipt is verified as of its own `asof`.
69//!
70//! **Slices** (0.3.4, `spec-0.4.0`). [`Fixtures::slices`] holds receipts whose lineage cites
71//! slices: their descriptors, the citing receipts and every member, the manifests by entry,
72//! and the key documents of their signers. A resolver that supplies manifests
73//! ([`Resolver::manifest`]) loads them too and is held to
74//! [`check_slices`](Kit::check_slices), which [`Kit::check`] and [`Kit::run`] include once
75//! [`Kit::slices`] says so:
76//!
77//! * each citing receipt passes step 7.5 through the resolver: it recomputes, or ends at
78//!   `E_EVAL_UNAVAILABLE`; never another code;
79//! * `verify` asks, per slice entry, for the manifest once and then for each member in the
80//!   manifest's order; a probe whose manifest is not loaded stops at it, and one whose
81//!   manifest names a member that is not loaded stops at that member, both
82//!   `E_INPUT_UNAVAILABLE`;
83//! * `manifest` returns exactly the bytes loaded, and `None` for entries it was not given
84//!   (unknown, malformed, a receipt hash);
85//! * a lineage the resolver reports with an evaluation ([`Resolver::evaluation`]) is made of
86//!   `inputs` entries, and those the receipt does not list are ones the resolver can back with
87//!   a receipt or a manifest.
88//!
89//! The descriptors of the slice fixtures are formula descriptors with `signature.order`:
90//! read them with [`crate::FunctionDescriptor`] (a [`Descriptor`] refuses `order`). A resolver
91//! loaded as for 0.3.3, without the slice fixtures, passes the kit as before as long as
92//! `slices` is not set.
93//!
94//! The kit expects a resolver to evaluate only what the fixtures give it: the versions of
95//! stored cells. Computed cells (`convert`, language `expr`) are not evaluable, so receipts
96//! of them end at `E_EVAL_UNAVAILABLE`. A panic in the resolver propagates to the caller.
97//!
98//! The fixtures come from the specification's vectors (revision `spec-0.4.0`; the files of
99//! the 0.3.3 fixtures are unchanged since `spec-0.2.1`, the slice fixtures are the
100//! slice-lineage receipt vectors of `spec-0.4.0`), in canonical form, plus receipts signed
101//! for the kit with the vectors' seeds (feature `sign`): a second `fx_rate` cell (EUR/CHF)
102//! with versions at two times, a `convert` receipt whose first input is not loaded, and two
103//! receipts citing a slice whose manifest, or one of whose members, is not loaded. They are embedded in the crate (`src/testkit/data.rs`), generated and
104//! checked against `spec/` by the crate's test `tests/testkit_data.rs`.
105
106use std::borrow::Cow;
107use std::cell::RefCell;
108use std::fmt;
109
110use crate::descriptor::Descriptor;
111use crate::error::ErrorCode;
112use crate::keys::Keys;
113use crate::model::{Cell, Scalar};
114use crate::receipt::Receipt;
115use crate::slice::Manifest;
116use crate::verify::{verify, Evaluation, Level, Resolver, VerifyResult};
117
118#[rustfmt::skip]
119mod data;
120
121/// What the resolver under test is loaded with. All bytes are canonical JSON (JCS).
122///
123/// Load every descriptor under its `fn_hash`, every receipt under its hash, and the
124/// versions: for a time-aware resolver (a store), version `v` says that cell `v.cell_id`
125/// has, from transaction time `v.tx` on, the value of receipt `v.receipt_hash` (which is
126/// `v.value`), answering as of a time under the kit's [`AnswerRule`] (by default only once
127/// that receipt's `asof` is reached too); a resolver holding one value per cell keeps the
128/// latest. Versions are listed in `tx` order per cell.
129#[non_exhaustive]
130#[derive(Clone, Debug, PartialEq, Eq)]
131pub struct Fixtures {
132    /// Key documents, by host. Verification uses them (see [`Fixtures::keys`]); a resolver
133    /// does not need them.
134    pub key_documents: Vec<(String, Vec<u8>)>,
135    /// Descriptors: function hash, descriptor JSON text.
136    pub descriptors: Vec<(String, Vec<u8>)>,
137    /// Receipts: receipt hash, receipt JSON text.
138    pub receipts: Vec<(String, Vec<u8>)>,
139    /// Versions of stored cells, one per loaded receipt of a `stored` function.
140    pub versions: Vec<Version>,
141    /// The slice fixtures (0.3.4), for a resolver that supplies manifests ([`Kit::slices`]).
142    pub slices: SliceFixtures,
143}
144
145impl Fixtures {
146    /// The key documents as [`Keys`].
147    pub fn keys(&self) -> Keys {
148        keys_of(&self.key_documents)
149    }
150}
151
152fn keys_of(docs: &[(String, Vec<u8>)]) -> Keys {
153    let mut keys = Keys::new();
154    for (host, body) in docs {
155        keys.insert_response(host, 200, Some(body));
156    }
157    keys
158}
159
160/// The slice fixtures (0.3.4, `spec-0.4.0`): receipts whose lineage cites slices, from the
161/// specification's slice-lineage receipt vectors. All bytes are canonical JSON (JCS). A
162/// resolver that supplies manifests loads the descriptors under their `fn_hash`, the receipts
163/// (citing receipts and members) under their hashes and the manifests under their entries.
164#[non_exhaustive]
165#[derive(Clone, Debug, PartialEq, Eq)]
166pub struct SliceFixtures {
167    /// Key documents of the signers, by host (verification only; see [`SliceFixtures::keys`]).
168    pub key_documents: Vec<(String, Vec<u8>)>,
169    /// Descriptors: function hash, descriptor JSON text (formula descriptors with `order`).
170    pub descriptors: Vec<(String, Vec<u8>)>,
171    /// Receipts: receipt hash, receipt JSON text; the citing receipts and every member.
172    pub receipts: Vec<(String, Vec<u8>)>,
173    /// Manifests: slice entry (`slice:<slice id>:<members hash>`), manifest JSON text.
174    pub manifests: Vec<(String, Vec<u8>)>,
175}
176
177impl SliceFixtures {
178    /// The key documents as [`Keys`].
179    pub fn keys(&self) -> Keys {
180        keys_of(&self.key_documents)
181    }
182}
183
184/// A version of a stored cell: from `tx` on, the cell's value is that of the receipt. When
185/// it answers a request as of some time is the kit's [`AnswerRule`]; the receipt's `asof`
186/// is that of receipt `receipt_hash`.
187#[non_exhaustive]
188#[derive(Clone, Debug, PartialEq, Eq)]
189pub struct Version {
190    /// The cell id ([`Cell::id`]).
191    pub cell_id: String,
192    /// The transaction time, the receipt's `tx`.
193    pub tx: String,
194    /// The hash of the receipt stating the value.
195    pub receipt_hash: String,
196    /// The receipt's value.
197    pub value: Scalar,
198}
199
200/// When a version of a stored cell answers as of a time `A`, for a resolver declared
201/// time-aware ([`Kit::time_aware`]). Set with [`Kit::answer_rule`]; the default is
202/// [`TxAndAsof`](AnswerRule::TxAndAsof). Among the versions that answer, the one with the
203/// latest `tx` gives the value; when none does, `evaluate` gives `None`.
204#[non_exhaustive]
205#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
206pub enum AnswerRule {
207    /// A version answers as of `A` when its `tx <= A` and its receipt's `asof <= A`: an `ok`
208    /// response to a request as of `A` carries a receipt with an `asof` at or before `A`
209    /// (SPEC 5.5 step 5, D31 revised), so a version does not answer before it was known.
210    #[default]
211    TxAndAsof,
212    /// A version answers as of `A` when its `tx <= A`, whatever its receipt's `asof`: the
213    /// kit's rule before 0.3.3.
214    TxOnly,
215}
216
217/// A call made to a [`Resolver`], as [`Recording`] records it.
218///
219/// The variants are not named `Descriptor` and `Receipt`: a second public item named like
220/// the crate's [`Receipt`] would make rustc print `datumesh_core::Receipt` in its
221/// diagnostics, for everyone who enables the feature.
222#[non_exhaustive]
223#[derive(Clone, Debug, PartialEq, Eq)]
224pub enum Call {
225    /// [`Resolver::descriptor`] with this function hash.
226    LookupDescriptor(String),
227    /// [`Resolver::receipt`] with this receipt hash.
228    LookupReceipt(String),
229    /// [`Resolver::manifest`] with this slice entry (0.3.4).
230    LookupManifest(String),
231    /// [`Resolver::evaluate`] or [`Resolver::evaluation`] with the cell of this id, as of this
232    /// time.
233    Evaluate {
234        /// [`Cell::id`] of the cell.
235        cell_id: String,
236        /// The `asof` argument.
237        asof: String,
238    },
239}
240
241/// A [`Resolver`] that forwards to another and records each call, in order.
242pub struct Recording<'a> {
243    inner: &'a dyn Resolver,
244    calls: RefCell<Vec<Call>>,
245}
246
247impl<'a> Recording<'a> {
248    /// Records the calls made to `inner`.
249    pub fn new(inner: &'a dyn Resolver) -> Recording<'a> {
250        Recording { inner, calls: RefCell::new(Vec::new()) }
251    }
252
253    /// The calls recorded so far.
254    pub fn calls(&self) -> Vec<Call> {
255        self.calls.borrow().clone()
256    }
257
258    /// The calls recorded so far, clearing the record.
259    pub fn take(&self) -> Vec<Call> {
260        self.calls.take()
261    }
262}
263
264impl fmt::Debug for Recording<'_> {
265    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
266        f.debug_struct("Recording").field("calls", &self.calls.borrow()).finish_non_exhaustive()
267    }
268}
269
270impl Resolver for Recording<'_> {
271    fn descriptor(&self, fn_hash: &str) -> Option<Cow<'_, [u8]>> {
272        self.calls.borrow_mut().push(Call::LookupDescriptor(fn_hash.to_string()));
273        self.inner.descriptor(fn_hash)
274    }
275
276    fn receipt(&self, hash: &str) -> Option<Cow<'_, [u8]>> {
277        self.calls.borrow_mut().push(Call::LookupReceipt(hash.to_string()));
278        self.inner.receipt(hash)
279    }
280
281    fn evaluate(&self, cell: &Cell, asof: &str) -> Option<Scalar> {
282        self.calls.borrow_mut().push(Call::Evaluate { cell_id: cell.id(), asof: asof.to_string() });
283        self.inner.evaluate(cell, asof)
284    }
285
286    fn manifest(&self, entry: &str) -> Option<Cow<'_, [u8]>> {
287        self.calls.borrow_mut().push(Call::LookupManifest(entry.to_string()));
288        self.inner.manifest(entry)
289    }
290
291    fn evaluation(&self, cell: &Cell, asof: &str) -> Option<Evaluation> {
292        self.calls.borrow_mut().push(Call::Evaluate { cell_id: cell.id(), asof: asof.to_string() });
293        self.inner.evaluation(cell, asof)
294    }
295}
296
297/// The failures of a check, one line each: `<check>: <case>: <what>`, where `<check>` is
298/// `lazy_calls`, `unavailable`, `bytes`, `asof`, `slices` (or `setup`, if the embedded
299/// fixtures could not be read, which the crate's tests rule out).
300#[derive(Clone, Debug, PartialEq, Eq)]
301pub struct Report {
302    failures: Vec<String>,
303}
304
305impl Report {
306    /// The failures, one per line of the report; never empty.
307    pub fn failures(&self) -> &[String] {
308        &self.failures
309    }
310}
311
312impl fmt::Display for Report {
313    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
314        for (i, line) in self.failures.iter().enumerate() {
315            if i > 0 {
316                f.write_str("\n")?;
317            }
318            f.write_str(line)?;
319        }
320        Ok(())
321    }
322}
323
324impl std::error::Error for Report {}
325
326/// A receipt the kit verifies, loaded or not.
327#[derive(Clone, Debug)]
328struct Case {
329    name: &'static str,
330    bytes: Vec<u8>,
331}
332
333/// The resolver test kit: the fixtures and the checks. See the [module](self) documentation.
334#[derive(Clone, Debug)]
335pub struct Kit {
336    fixtures: Fixtures,
337    cases: Vec<Case>,
338    slice_cases: Vec<Case>,
339    time_aware: bool,
340    answer_rule: AnswerRule,
341    slices: bool,
342    setup: Vec<String>,
343}
344
345impl Default for Kit {
346    fn default() -> Kit {
347        Kit::new()
348    }
349}
350
351/// The receipt that is not JSON.
352const NOT_JSON: &str = "not-json";
353
354impl Kit {
355    /// The kit, for a resolver that is not time-aware, with the default [`AnswerRule`]
356    /// (`TxAndAsof`) for when it is declared time-aware.
357    pub fn new() -> Kit {
358        let mut setup = Vec::new();
359        let mut fx = Fixtures {
360            key_documents: Vec::new(),
361            descriptors: Vec::new(),
362            receipts: Vec::new(),
363            versions: Vec::new(),
364            slices: SliceFixtures {
365                key_documents: Vec::new(),
366                descriptors: Vec::new(),
367                receipts: Vec::new(),
368                manifests: Vec::new(),
369            },
370        };
371        let mut cases = Vec::new();
372        for (host, text) in data::KEY_DOCUMENTS {
373            fx.key_documents.push((host.to_string(), text.as_bytes().to_vec()));
374        }
375        let mut stored = Vec::new();
376        for (source, text) in data::DESCRIPTORS {
377            match Descriptor::from_json(text.as_bytes()) {
378                Ok(d) => {
379                    if d.lang == "stored" {
380                        stored.push(d.hash());
381                    }
382                    fx.descriptors.push((d.hash(), text.as_bytes().to_vec()));
383                }
384                Err(e) => setup.push(format!("setup: {source}: {e}")),
385            }
386        }
387        for (name, source, text) in data::RECEIPTS {
388            match Receipt::from_json(text.as_bytes()) {
389                Ok(r) => {
390                    if stored.contains(&r.cell().fn_hash) {
391                        fx.versions.push(Version {
392                            cell_id: r.cell().id(),
393                            tx: r.tx().to_string(),
394                            receipt_hash: r.hash().to_string(),
395                            value: r.value().clone(),
396                        });
397                    }
398                    fx.receipts.push((r.hash().to_string(), text.as_bytes().to_vec()));
399                }
400                Err(e) => setup.push(format!("setup: {source}: {e}")),
401            }
402            cases.push(Case { name, bytes: text.as_bytes().to_vec() });
403        }
404        for (name, _, text) in data::PROBES {
405            cases.push(Case { name, bytes: text.as_bytes().to_vec() });
406        }
407        cases.push(Case { name: NOT_JSON, bytes: b"{".to_vec() });
408        // Versions in tx order per cell (a stable sort keeps the cells' order otherwise).
409        fx.versions.sort_by(|a, b| a.cell_id.cmp(&b.cell_id).then_with(|| a.tx.cmp(&b.tx)));
410        let slice_cases = load_slices(&mut fx.slices, &mut setup);
411        Kit {
412            fixtures: fx,
413            cases,
414            slice_cases,
415            time_aware: false,
416            answer_rule: AnswerRule::default(),
417            slices: false,
418            setup,
419        }
420    }
421
422    /// Declares whether the resolver under test supplies manifests ([`Resolver::manifest`])
423    /// and is loaded with [`Fixtures::slices`]: then [`check`](Kit::check) and
424    /// [`run`](Kit::run) include [`check_slices`](Kit::check_slices). Off by default, so a
425    /// resolver loaded as for 0.3.3 passes as before.
426    pub fn slices(mut self, yes: bool) -> Kit {
427        self.slices = yes;
428        self
429    }
430
431    /// Declares whether the resolver under test is time-aware: whether
432    /// [`evaluate`](Resolver::evaluate) gives, for a cell with several versions, the value of
433    /// the latest version that answers as of `asof` under the kit's
434    /// [`answer_rule`](Kit::answer_rule) (by default `TxAndAsof`: `tx <= asof` and the
435    /// receipt's `asof` at or before it). Time-aware resolvers (a store) are held to it;
436    /// others (`MapResolver`) are not.
437    pub fn time_aware(mut self, yes: bool) -> Kit {
438        self.time_aware = yes;
439        self
440    }
441
442    /// Sets the [`AnswerRule`] a time-aware resolver is held to (default `TxAndAsof`). It
443    /// can be set before or after [`time_aware`](Kit::time_aware), and changes nothing for
444    /// a resolver that is not time-aware.
445    pub fn answer_rule(mut self, rule: AnswerRule) -> Kit {
446        self.answer_rule = rule;
447        self
448    }
449
450    /// The fixtures to load into the resolver under test.
451    pub fn fixtures(&self) -> &Fixtures {
452        &self.fixtures
453    }
454
455    /// Calls `load` with the fixtures, then runs every check on the resolver it returns.
456    pub fn run<R: Resolver, F: FnOnce(&Fixtures) -> R>(&self, load: F) -> Result<(), Report> {
457        self.setup_ok()?;
458        let r = load(&self.fixtures);
459        self.check(&r)
460    }
461
462    /// Runs every check on a resolver loaded with [`fixtures`](Kit::fixtures).
463    pub fn check(&self, resolver: &dyn Resolver) -> Result<(), Report> {
464        self.setup_ok()?;
465        let mut failures = Vec::new();
466        let mut checks: Vec<Check> =
467            vec![Kit::check_lazy_calls, Kit::check_unavailable, Kit::check_bytes, Kit::check_asof];
468        if self.slices {
469            checks.push(Kit::check_slices);
470        }
471        for check in checks {
472            if let Err(mut r) = check(self, resolver) {
473                failures.append(&mut r.failures);
474            }
475        }
476        report(failures)
477    }
478
479    /// Receipts citing slices verify through the resolver's manifests and members, which it
480    /// returns exactly as loaded; see the [module](self) documentation. A resolver that
481    /// supplies no manifests fails it.
482    pub fn check_slices(&self, resolver: &dyn Resolver) -> Result<(), Report> {
483        self.setup_ok()?;
484        let mut f = Failures::new("slices");
485        let keys = self.fixtures.slices.keys();
486        let members = self.manifest_members();
487        for case in &self.slice_cases {
488            let Ok(r) = Receipt::from_json(&case.bytes) else {
489                f.push(case.name, "the fixture does not parse".to_string());
490                continue;
491            };
492            let rec = Recording::new(resolver);
493            let out = verify(&case.bytes, &keys, Some(&rec), None);
494            let calls = rec.take();
495            let want_code = match case.name {
496                "slice-manifest-missing" | "slice-member-missing" => Some(ErrorCode::InputUnavailable),
497                _ => None,
498            };
499            match want_code {
500                Some(code) => {
501                    if out.ok || out.code != Some(code) {
502                        f.push(case.name, format!("want {code}, got ok={} code={:?} {}", out.ok, out.code, out.path));
503                    }
504                }
505                None => {
506                    let passed = out.ok
507                        && ((out.level == Some(Level::Recomputed) && out.code.is_none())
508                            || (out.level == Some(Level::Signed) && out.code == Some(ErrorCode::EvalUnavailable)));
509                    if !passed {
510                        f.push(
511                            case.name,
512                            format!(
513                                "want step 7.5 to pass (recomputed, or E_EVAL_UNAVAILABLE), got ok={} level={:?} code={:?} {} {}",
514                                out.ok, out.level, out.code, out.path, out.detail
515                            ),
516                        );
517                    }
518                }
519            }
520            let want = expected_calls_with(&r, &out, &members);
521            let same = match out.code {
522                // After a different value, the lineage's receipts may follow.
523                Some(ErrorCode::ValueMismatch | ErrorCode::InputSuperseded) => {
524                    calls.starts_with(&want) && calls[want.len()..].iter().all(|c| matches!(c, Call::LookupReceipt(_)))
525                }
526                _ => calls == want,
527            };
528            if !same {
529                f.push(case.name, format!("asked {calls:?}, step 7 needs {want:?} (result {:?})", out.code));
530            }
531            // A lineage, if reported, is made of entries the resolver can back.
532            if want_code.is_none() {
533                if let Some(Evaluation { lineage: Some(lineage), .. }) = resolver.evaluation(r.cell(), r.asof()) {
534                    for e in &lineage {
535                        let form = crate::lexical::check_input_entry(e, "").is_ok();
536                        let backed = r.inputs().contains(e)
537                            || if e.starts_with("slice:") {
538                                resolver.manifest(e).is_some()
539                            } else {
540                                resolver.receipt(e).is_some()
541                            };
542                        if !form || !backed {
543                            f.push(case.name, format!("lineage entry {} is not an entry the resolver backs", shown(e)));
544                        }
545                    }
546                }
547            }
548        }
549        // Bytes: manifests (and the slice receipts and descriptors) exactly as loaded.
550        let sf = &self.fixtures.slices;
551        for (e, bytes) in &sf.manifests {
552            for _ in 0..2 {
553                match resolver.manifest(e) {
554                    Some(b) if *b == **bytes => {}
555                    Some(b) => f.push(
556                        "bytes",
557                        format!("manifest {e}: {} bytes differ from the {} loaded", b.len(), bytes.len()),
558                    ),
559                    None => f.push("bytes", format!("manifest {e}: None, but it was loaded")),
560                }
561            }
562        }
563        for (h, bytes) in &sf.receipts {
564            match resolver.receipt(h) {
565                Some(b) if *b == **bytes => {}
566                Some(_) => f.push("bytes", format!("receipt {h}: the bytes differ from those loaded")),
567                None => f.push("bytes", format!("receipt {h}: None, but it was loaded")),
568            }
569        }
570        for (h, bytes) in &sf.descriptors {
571            match resolver.descriptor(h) {
572                Some(b) if *b == **bytes => {}
573                Some(_) => f.push("bytes", format!("descriptor {h}: the bytes differ from those loaded")),
574                None => f.push("bytes", format!("descriptor {h}: None, but it was loaded")),
575            }
576        }
577        // Entries it was not given.
578        let known = sf.manifests.first().map(|(e, _)| e.clone()).unwrap_or_default();
579        let missing: Vec<String> = self
580            .slice_cases
581            .iter()
582            .filter_map(|c| Receipt::from_json(&c.bytes).ok())
583            .flat_map(|r| r.inputs().to_vec())
584            .filter(|e| e.starts_with("slice:") && !sf.manifests.iter().any(|(x, _)| x == e))
585            .collect();
586        let mut probes = vec![
587            format!("slice:{}:{}", "0".repeat(64), "0".repeat(64)),
588            String::new(),
589            "slice:".into(),
590            known.to_uppercase(),
591            format!("{known}0"),
592            format!(" {known}"),
593            known.get(..134).unwrap_or_default().to_string(),
594            known.replacen("slice:", "SLICE:", 1),
595            "' OR '1'='1".into(),
596            "../../etc/passwd".into(),
597            "a".repeat(100_000),
598        ];
599        probes.extend(sf.receipts.first().map(|(h, _)| h.clone()));
600        probes.extend(missing);
601        for p in &probes {
602            if resolver.manifest(p).is_some() {
603                f.push("lookup", format!("manifest({}) is not None", shown(p)));
604            }
605        }
606        f.finish()
607    }
608
609    /// The members of every loaded manifest, by entry.
610    fn manifest_members(&self) -> Vec<(String, Vec<String>)> {
611        self.fixtures
612            .slices
613            .manifests
614            .iter()
615            .filter_map(|(e, b)| Manifest::from_json(b).ok().map(|m| (e.clone(), m.members().to_vec())))
616            .collect()
617    }
618
619    /// Step 7 asks the resolver only for what it reaches, and nothing when the receipt
620    /// fails before step 7.
621    pub fn check_lazy_calls(&self, resolver: &dyn Resolver) -> Result<(), Report> {
622        self.setup_ok()?;
623        let keys = self.fixtures.keys();
624        let mut f = Failures::new("lazy_calls");
625        for case in &self.cases {
626            let rec = Recording::new(resolver);
627            let without = verify(&case.bytes, &keys, None, None);
628            let out = verify(&case.bytes, &keys, Some(&rec), None);
629            let calls = rec.take();
630            if !without.ok {
631                if !calls.is_empty() {
632                    f.push(
633                        case.name,
634                        format!("steps 1-6 fail ({:?}), yet the resolver was asked {calls:?}", without.code),
635                    );
636                }
637                if out != without {
638                    f.push(case.name, format!("a resolver changed a failure before step 7: {out:?}"));
639                }
640                continue;
641            }
642            match Receipt::from_json(&case.bytes) {
643                Ok(r) => {
644                    let want = expected_calls(&r, &out);
645                    let same = match out.code {
646                        // After a different value, the receipts of a reported lineage follow.
647                        Some(ErrorCode::ValueMismatch | ErrorCode::InputSuperseded) => {
648                            calls.starts_with(&want)
649                                && calls[want.len()..].iter().all(|c| matches!(c, Call::LookupReceipt(_)))
650                        }
651                        _ => calls == want,
652                    };
653                    if !same {
654                        f.push(case.name, format!("asked {calls:?}, step 7 needs {want:?} (result {:?})", out.code));
655                    }
656                }
657                Err(e) => f.push(case.name, format!("steps 1-6 passed but the receipt does not parse: {e}")),
658            }
659        }
660        for name in ["tampered-value", "duplicate-member", NOT_JSON] {
661            if let Some(case) = self.case(name) {
662                if verify(&case.bytes, &keys, None, None).ok {
663                    f.push(name, "expected to fail before step 7".to_string());
664                }
665            }
666        }
667        f.finish()
668    }
669
670    /// What the resolver does not have is unavailable, never a panic.
671    pub fn check_unavailable(&self, resolver: &dyn Resolver) -> Result<(), Report> {
672        self.setup_ok()?;
673        let mut f = Failures::new("unavailable");
674        let keys = self.fixtures.keys();
675        // Through verify.
676        self.expect(&mut f, resolver, &keys, "instant-no-valid-time", Want::Fails(ErrorCode::FnUnavailable));
677        self.expect(&mut f, resolver, &keys, "computed-two-inputs", Want::Fails(ErrorCode::InputUnavailable));
678        self.expect(&mut f, resolver, &keys, "first-input-missing", Want::Fails(ErrorCode::InputUnavailable));
679        self.expect(&mut f, resolver, &keys, "computed-with-lineage", Want::EvalUnavailable);
680        // Lookups of hashes that are unknown, or are not hashes at all.
681        let unknown_fn = self.receipt_of("instant-no-valid-time").map(|r| r.cell().fn_hash.clone());
682        let known_receipt = self.fixtures.receipts.first().map(|(h, _)| h.clone()).unwrap_or_default();
683        let known_fn = self.fixtures.descriptors.first().map(|(h, _)| h.clone()).unwrap_or_default();
684        let mut probes: Vec<String> = vec![
685            "0".repeat(64),
686            "f".repeat(64),
687            String::new(),
688            "abc".into(),
689            "g".repeat(64),
690            "\0".into(),
691            format!("{known_receipt}\0"),
692            "../../etc/passwd".into(),
693            "' OR '1'='1".into(),
694            "%".into(),
695            "_".repeat(64),
696            "é".repeat(32),
697            "a".repeat(100_000),
698            known_receipt.to_uppercase(),
699            known_fn.to_uppercase(),
700            format!("{known_receipt}0"),
701            format!(" {known_receipt}"),
702            format!("{known_fn}\n"),
703            known_receipt.get(..63).unwrap_or_default().to_string(),
704        ];
705        probes.extend(unknown_fn);
706        // Receipts that exist but are not loaded: an input of computed-two-inputs and the
707        // first input of first-input-missing.
708        for name in ["computed-two-inputs", "first-input-missing"] {
709            if let Some(r) = self.receipt_of(name) {
710                probes.extend(r.inputs().iter().filter(|h| !self.has_receipt(h)).cloned());
711            }
712        }
713        for p in &probes {
714            if resolver.descriptor(p).is_some() {
715                f.push("lookup", format!("descriptor({}) is not None", shown(p)));
716            }
717            if resolver.receipt(p).is_some() {
718                f.push("lookup", format!("receipt({}) is not None", shown(p)));
719            }
720        }
721        // evaluate with an asof that is not an instant: anything but a panic.
722        if let Some(r) = self.receipt_of("timeline-1") {
723            for asof in ["", "not-an-instant", "\0", "2024-03-16", "' OR '1'='1", "2024-03-16T09:00:00Z"] {
724                let _ = resolver.evaluate(r.cell(), asof);
725            }
726        }
727        f.finish()
728    }
729
730    /// `descriptor` and `receipt` return exactly the bytes loaded.
731    pub fn check_bytes(&self, resolver: &dyn Resolver) -> Result<(), Report> {
732        self.setup_ok()?;
733        let mut f = Failures::new("bytes");
734        for (h, bytes) in &self.fixtures.descriptors {
735            for _ in 0..2 {
736                match resolver.descriptor(h) {
737                    Some(b) if *b == **bytes => {}
738                    Some(b) => {
739                        f.push("descriptor", format!("{h}: {} bytes differ from the {} loaded", b.len(), bytes.len()))
740                    }
741                    None => f.push("descriptor", format!("{h}: None, but it was loaded")),
742                }
743            }
744        }
745        for (h, bytes) in &self.fixtures.receipts {
746            for _ in 0..2 {
747                match resolver.receipt(h) {
748                    Some(b) if *b == **bytes => {}
749                    Some(b) => {
750                        f.push("receipt", format!("{h}: {} bytes differ from the {} loaded", b.len(), bytes.len()))
751                    }
752                    None => f.push("receipt", format!("{h}: None, but it was loaded")),
753                }
754            }
755        }
756        f.finish()
757    }
758
759    /// `evaluate` receives the receipt's `asof`; for a time-aware resolver, a cell's value is
760    /// that of its latest version that answers under the kit's [`AnswerRule`].
761    pub fn check_asof(&self, resolver: &dyn Resolver) -> Result<(), Report> {
762        self.setup_ok()?;
763        let mut f = Failures::new("asof");
764        let keys = self.fixtures.keys();
765        // Pass-through: every evaluate call carries the receipt's cell and asof.
766        for case in &self.cases {
767            let Ok(r) = Receipt::from_json(&case.bytes) else { continue };
768            let rec = Recording::new(resolver);
769            let _ = verify(&case.bytes, &keys, Some(&rec), None);
770            for call in rec.take() {
771                if let Call::Evaluate { cell_id, asof } = call {
772                    if cell_id != r.cell().id() || asof != r.asof() {
773                        f.push(case.name, format!("evaluate got ({cell_id}, {asof}), not the receipt's cell and asof"));
774                    }
775                }
776            }
777        }
778        self.expect(&mut f, resolver, &keys, "stored-number", Want::Recomputed);
779        if !self.time_aware {
780            return f.finish();
781        }
782        // Through verify: each receipt of the EUR/CHF cell recomputes as of its own asof, and
783        // one as of a time before the first version cannot be evaluated.
784        for name in ["timeline-1", "timeline-1-later", "timeline-2"] {
785            self.expect(&mut f, resolver, &keys, name, Want::Recomputed);
786        }
787        self.expect(&mut f, resolver, &keys, "timeline-0", Want::EvalUnavailable);
788        // Directly: None before the first version answers. TxOnly: inclusive at each tx.
789        // TxAndAsof: version 1 (tx 2024-03-15T16:00, asof 2024-03-16T09:00) answers from its
790        // asof on, inclusive, and nothing does from its tx up to then; version 2 has tx = asof.
791        let (Some(v1), Some(v2)) = (self.receipt_of("timeline-1"), self.receipt_of("timeline-2")) else {
792            f.push("timeline", "fixtures missing".to_string());
793            return f.finish();
794        };
795        let cell = v1.cell();
796        let points: Vec<(&str, Option<&Scalar>)> = match self.answer_rule {
797            AnswerRule::TxOnly => vec![
798                ("0001-01-01T00:00:00.000000Z", None),
799                ("2024-03-15T15:59:59.999999Z", None),
800                (v1.tx(), Some(v1.value())),
801                ("2024-03-17T00:00:00.000000Z", Some(v1.value())),
802                ("2024-03-18T09:59:59.999999Z", Some(v1.value())),
803                (v2.tx(), Some(v2.value())),
804                ("9999-12-31T23:59:59.999999Z", Some(v2.value())),
805            ],
806            AnswerRule::TxAndAsof => vec![
807                ("0001-01-01T00:00:00.000000Z", None),
808                ("2024-03-15T15:59:59.999999Z", None),
809                (v1.tx(), None),
810                ("2024-03-16T08:59:59.999999Z", None),
811                (v1.asof(), Some(v1.value())),
812                ("2024-03-17T00:00:00.000000Z", Some(v1.value())),
813                ("2024-03-18T09:59:59.999999Z", Some(v1.value())),
814                (v2.tx(), Some(v2.value())),
815                ("9999-12-31T23:59:59.999999Z", Some(v2.value())),
816            ],
817        };
818        for (asof, want) in points {
819            let got = resolver.evaluate(cell, asof);
820            if got.as_ref() != want {
821                f.push("timeline", format!("evaluate as of {asof} gave {got:?}, want {want:?}"));
822            }
823        }
824        f.finish()
825    }
826
827    fn setup_ok(&self) -> Result<(), Report> {
828        if self.setup.is_empty() {
829            Ok(())
830        } else {
831            Err(Report { failures: self.setup.clone() })
832        }
833    }
834
835    fn case(&self, name: &str) -> Option<&Case> {
836        self.cases.iter().find(|c| c.name == name)
837    }
838
839    fn receipt_of(&self, name: &str) -> Option<Receipt> {
840        self.case(name).and_then(|c| Receipt::from_json(&c.bytes).ok())
841    }
842
843    fn has_receipt(&self, hash: &str) -> bool {
844        self.fixtures.receipts.iter().any(|(h, _)| h == hash)
845    }
846
847    /// Verifies case `name` through `resolver` and records a failure unless it ends as wanted.
848    fn expect(&self, f: &mut Failures, resolver: &dyn Resolver, keys: &Keys, name: &str, want: Want) {
849        let Some(case) = self.case(name) else {
850            f.push(name, "no such fixture".to_string());
851            return;
852        };
853        let out = verify(&case.bytes, keys, Some(resolver), None);
854        let good = match want {
855            Want::Recomputed => out.ok && out.level == Some(Level::Recomputed) && out.code.is_none(),
856            Want::EvalUnavailable => {
857                out.ok && out.level == Some(Level::Signed) && out.code == Some(ErrorCode::EvalUnavailable)
858            }
859            Want::Fails(code) => !out.ok && out.code == Some(code),
860        };
861        if !good {
862            f.push(
863                name,
864                format!(
865                    "want {want:?}, got ok={} level={:?} code={:?} {} {}",
866                    out.ok, out.level, out.code, out.path, out.detail
867                ),
868            );
869        }
870    }
871}
872
873/// Reads the embedded slice fixtures into `sf`; returns the slice cases (the citing receipts
874/// loaded, then the probes).
875fn load_slices(sf: &mut SliceFixtures, setup: &mut Vec<String>) -> Vec<Case> {
876    let mut cases = Vec::new();
877    for (host, text) in data::SLICE_KEY_DOCUMENTS {
878        sf.key_documents.push((host.to_string(), text.as_bytes().to_vec()));
879    }
880    for (source, text) in data::SLICE_DESCRIPTORS {
881        match crate::descriptor::FunctionDescriptor::from_json(text.as_bytes()) {
882            Ok(d) => sf.descriptors.push((d.hash(), text.as_bytes().to_vec())),
883            Err(e) => setup.push(format!("setup: {source}: {e}")),
884        }
885    }
886    for (name, source, text) in data::SLICE_RECEIPTS {
887        match Receipt::from_json(text.as_bytes()) {
888            Ok(r) => sf.receipts.push((r.hash().to_string(), text.as_bytes().to_vec())),
889            Err(e) => setup.push(format!("setup: {source}: {e}")),
890        }
891        if *name != "member" {
892            cases.push(Case { name, bytes: text.as_bytes().to_vec() });
893        }
894    }
895    for (source, text) in data::MANIFESTS {
896        match Manifest::from_json(text.as_bytes()) {
897            Ok(m) => sf.manifests.push((m.entry(), text.as_bytes().to_vec())),
898            Err(e) => setup.push(format!("setup: {source}: {e}")),
899        }
900    }
901    for (name, _, text) in data::SLICE_PROBES {
902        cases.push(Case { name, bytes: text.as_bytes().to_vec() });
903    }
904    cases
905}
906
907/// One of the kit's checks.
908type Check = fn(&Kit, &dyn Resolver) -> Result<(), Report>;
909
910/// How a case should end.
911#[derive(Clone, Copy, Debug)]
912enum Want {
913    Recomputed,
914    EvalUnavailable,
915    Fails(ErrorCode),
916}
917
918/// The calls step 7 needs for receipt `r` (which passed steps 1-6), given how it ended.
919fn expected_calls(r: &Receipt, out: &VerifyResult) -> Vec<Call> {
920    expected_calls_with(r, out, &[])
921}
922
923/// The calls for input entry `e`: its receipt, or its manifest then its first `take`
924/// members (all when `None`), as far as `members` knows them.
925fn entry_calls(e: &str, members: &[(String, Vec<String>)], take: Option<usize>) -> Vec<Call> {
926    if !e.starts_with("slice:") {
927        return vec![Call::LookupReceipt(e.to_string())];
928    }
929    let mut calls = vec![Call::LookupManifest(e.to_string())];
930    if let Some((_, ms)) = members.iter().find(|(x, _)| x == e) {
931        calls.extend(ms.iter().take(take.unwrap_or(ms.len())).map(|h| Call::LookupReceipt(h.clone())));
932    }
933    calls
934}
935
936/// [`expected_calls`] with the members of the manifests the resolver was loaded with.
937fn expected_calls_with(r: &Receipt, out: &VerifyResult, members: &[(String, Vec<String>)]) -> Vec<Call> {
938    let mut calls = vec![Call::LookupDescriptor(r.cell().fn_hash.clone())];
939    let all_inputs = || r.inputs().iter().flat_map(|e| entry_calls(e, members, None));
940    let evaluate = Call::Evaluate { cell_id: r.cell().id(), asof: r.asof().to_string() };
941    match out.code {
942        // 7.6 compared, or could not evaluate (the lineage's receipts may follow a mismatch).
943        None | Some(ErrorCode::ValueMismatch | ErrorCode::EvalUnavailable | ErrorCode::InputSuperseded) => {
944            calls.extend(all_inputs());
945            calls.push(evaluate);
946        }
947        // 7.5: up to and including the failing input; in a slice entry, its manifest and the
948        // members up to the failing one (path `$.inputs.<entry>.members.<hash>`).
949        Some(_) if out.path.starts_with("$.inputs.") => {
950            let rest = &out.path["$.inputs.".len()..];
951            let at = r.inputs().iter().position(|e| {
952                rest == e.as_str() || rest.starts_with(&format!("{e}.")) || rest.starts_with(&format!("{e}["))
953            });
954            let Some(i) = at else {
955                calls.extend(all_inputs());
956                return calls;
957            };
958            calls.extend(r.inputs()[..i].iter().flat_map(|e| entry_calls(e, members, None)));
959            let e = &r.inputs()[i];
960            let member = rest[e.len()..]
961                .strip_prefix(".members.")
962                .and_then(|h| members.iter().find(|(x, _)| x == e).and_then(|(_, ms)| ms.iter().position(|m| m == h)));
963            calls.extend(entry_calls(e, members, Some(member.map_or(0, |k| k + 1))));
964        }
965        // 7.5's last check: tx against the latest input tx.
966        Some(ErrorCode::TimeOrder) if out.path == "$.tx" => calls.extend(all_inputs()),
967        // 7.1-7.4.
968        Some(_) => {}
969    }
970    calls
971}
972
973/// A hash-like argument as shown in a report: long ones shortened.
974fn shown(s: &str) -> String {
975    if s.chars().count() > 80 {
976        format!("{:?}… ({} bytes)", s.chars().take(16).collect::<String>(), s.len())
977    } else {
978        format!("{s:?}")
979    }
980}
981
982struct Failures {
983    check: &'static str,
984    lines: Vec<String>,
985}
986
987impl Failures {
988    fn new(check: &'static str) -> Failures {
989        Failures { check, lines: Vec::new() }
990    }
991
992    fn push(&mut self, case: &str, what: String) {
993        self.lines.push(format!("{}: {case}: {what}", self.check));
994    }
995
996    fn finish(self) -> Result<(), Report> {
997        report(self.lines)
998    }
999}
1000
1001fn report(failures: Vec<String>) -> Result<(), Report> {
1002    if failures.is_empty() {
1003        Ok(())
1004    } else {
1005        Err(Report { failures })
1006    }
1007}