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