1use 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#[non_exhaustive]
104#[derive(Clone, Debug, PartialEq, Eq)]
105pub struct Fixtures {
106 pub key_documents: Vec<(String, Vec<u8>)>,
109 pub descriptors: Vec<(String, Vec<u8>)>,
111 pub receipts: Vec<(String, Vec<u8>)>,
113 pub versions: Vec<Version>,
115}
116
117impl Fixtures {
118 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#[non_exhaustive]
132#[derive(Clone, Debug, PartialEq, Eq)]
133pub struct Version {
134 pub cell_id: String,
136 pub tx: String,
138 pub receipt_hash: String,
140 pub value: Scalar,
142}
143
144#[non_exhaustive]
149#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
150pub enum AnswerRule {
151 #[default]
155 TxAndAsof,
156 TxOnly,
159}
160
161#[non_exhaustive]
167#[derive(Clone, Debug, PartialEq, Eq)]
168pub enum Call {
169 LookupDescriptor(String),
171 LookupReceipt(String),
173 Evaluate {
175 cell_id: String,
177 asof: String,
179 },
180}
181
182pub struct Recording<'a> {
184 inner: &'a dyn Resolver,
185 calls: RefCell<Vec<Call>>,
186}
187
188impl<'a> Recording<'a> {
189 pub fn new(inner: &'a dyn Resolver) -> Recording<'a> {
191 Recording { inner, calls: RefCell::new(Vec::new()) }
192 }
193
194 pub fn calls(&self) -> Vec<Call> {
196 self.calls.borrow().clone()
197 }
198
199 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#[derive(Clone, Debug, PartialEq, Eq)]
232pub struct Report {
233 failures: Vec<String>,
234}
235
236impl Report {
237 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#[derive(Clone, Debug)]
259struct Case {
260 name: &'static str,
261 bytes: Vec<u8>,
262}
263
264#[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
280const NOT_JSON: &str = "not-json";
282
283impl Kit {
284 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 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 pub fn time_aware(mut self, yes: bool) -> Kit {
339 self.time_aware = yes;
340 self
341 }
342
343 pub fn answer_rule(mut self, rule: AnswerRule) -> Kit {
347 self.answer_rule = rule;
348 self
349 }
350
351 pub fn fixtures(&self) -> &Fixtures {
353 &self.fixtures
354 }
355
356 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 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 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 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 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 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 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 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 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 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 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 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 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 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#[derive(Clone, Copy, Debug)]
623enum Want {
624 Recomputed,
625 EvalUnavailable,
626 Fails(ErrorCode),
627}
628
629fn 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 None | Some(ErrorCode::ValueMismatch | ErrorCode::EvalUnavailable) => {
637 calls.extend(all_inputs());
638 calls.push(evaluate);
639 }
640 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 Some(ErrorCode::TimeOrder) if out.path == "$.tx" => calls.extend(all_inputs()),
648 Some(_) => {}
650 }
651 calls
652}
653
654fn 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}