1use std::borrow::Cow;
64use std::cell::RefCell;
65use std::fmt;
66
67use crate::descriptor::Descriptor;
68use crate::error::ErrorCode;
69use crate::keys::Keys;
70use crate::model::{Cell, Scalar};
71use crate::receipt::Receipt;
72use crate::verify::{verify, Level, Resolver, VerifyResult};
73
74#[rustfmt::skip]
75mod data;
76
77#[non_exhaustive]
85#[derive(Clone, Debug, PartialEq, Eq)]
86pub struct Fixtures {
87 pub key_documents: Vec<(String, Vec<u8>)>,
90 pub descriptors: Vec<(String, Vec<u8>)>,
92 pub receipts: Vec<(String, Vec<u8>)>,
94 pub versions: Vec<Version>,
96}
97
98impl Fixtures {
99 pub fn keys(&self) -> Keys {
101 let mut keys = Keys::new();
102 for (host, body) in &self.key_documents {
103 keys.insert_response(host, 200, Some(body));
104 }
105 keys
106 }
107}
108
109#[non_exhaustive]
111#[derive(Clone, Debug, PartialEq, Eq)]
112pub struct Version {
113 pub cell_id: String,
115 pub tx: String,
117 pub receipt_hash: String,
119 pub value: Scalar,
121}
122
123#[non_exhaustive]
129#[derive(Clone, Debug, PartialEq, Eq)]
130pub enum Call {
131 LookupDescriptor(String),
133 LookupReceipt(String),
135 Evaluate {
137 cell_id: String,
139 asof: String,
141 },
142}
143
144pub struct Recording<'a> {
146 inner: &'a dyn Resolver,
147 calls: RefCell<Vec<Call>>,
148}
149
150impl<'a> Recording<'a> {
151 pub fn new(inner: &'a dyn Resolver) -> Recording<'a> {
153 Recording { inner, calls: RefCell::new(Vec::new()) }
154 }
155
156 pub fn calls(&self) -> Vec<Call> {
158 self.calls.borrow().clone()
159 }
160
161 pub fn take(&self) -> Vec<Call> {
163 self.calls.take()
164 }
165}
166
167impl fmt::Debug for Recording<'_> {
168 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169 f.debug_struct("Recording").field("calls", &self.calls.borrow()).finish_non_exhaustive()
170 }
171}
172
173impl Resolver for Recording<'_> {
174 fn descriptor(&self, fn_hash: &str) -> Option<Cow<'_, [u8]>> {
175 self.calls.borrow_mut().push(Call::LookupDescriptor(fn_hash.to_string()));
176 self.inner.descriptor(fn_hash)
177 }
178
179 fn receipt(&self, hash: &str) -> Option<Cow<'_, [u8]>> {
180 self.calls.borrow_mut().push(Call::LookupReceipt(hash.to_string()));
181 self.inner.receipt(hash)
182 }
183
184 fn evaluate(&self, cell: &Cell, asof: &str) -> Option<Scalar> {
185 self.calls.borrow_mut().push(Call::Evaluate { cell_id: cell.id(), asof: asof.to_string() });
186 self.inner.evaluate(cell, asof)
187 }
188}
189
190#[derive(Clone, Debug, PartialEq, Eq)]
194pub struct Report {
195 failures: Vec<String>,
196}
197
198impl Report {
199 pub fn failures(&self) -> &[String] {
201 &self.failures
202 }
203}
204
205impl fmt::Display for Report {
206 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
207 for (i, line) in self.failures.iter().enumerate() {
208 if i > 0 {
209 f.write_str("\n")?;
210 }
211 f.write_str(line)?;
212 }
213 Ok(())
214 }
215}
216
217impl std::error::Error for Report {}
218
219#[derive(Clone, Debug)]
221struct Case {
222 name: &'static str,
223 bytes: Vec<u8>,
224}
225
226#[derive(Clone, Debug)]
228pub struct Kit {
229 fixtures: Fixtures,
230 cases: Vec<Case>,
231 time_aware: bool,
232 setup: Vec<String>,
233}
234
235impl Default for Kit {
236 fn default() -> Kit {
237 Kit::new()
238 }
239}
240
241const NOT_JSON: &str = "not-json";
243
244impl Kit {
245 pub fn new() -> Kit {
247 let mut setup = Vec::new();
248 let mut fx =
249 Fixtures { key_documents: Vec::new(), descriptors: Vec::new(), receipts: Vec::new(), versions: Vec::new() };
250 let mut cases = Vec::new();
251 for (host, text) in data::KEY_DOCUMENTS {
252 fx.key_documents.push((host.to_string(), text.as_bytes().to_vec()));
253 }
254 let mut stored = Vec::new();
255 for (source, text) in data::DESCRIPTORS {
256 match Descriptor::from_json(text.as_bytes()) {
257 Ok(d) => {
258 if d.lang == "stored" {
259 stored.push(d.hash());
260 }
261 fx.descriptors.push((d.hash(), text.as_bytes().to_vec()));
262 }
263 Err(e) => setup.push(format!("setup: {source}: {e}")),
264 }
265 }
266 for (name, source, text) in data::RECEIPTS {
267 match Receipt::from_json(text.as_bytes()) {
268 Ok(r) => {
269 if stored.contains(&r.cell().fn_hash) {
270 fx.versions.push(Version {
271 cell_id: r.cell().id(),
272 tx: r.tx().to_string(),
273 receipt_hash: r.hash().to_string(),
274 value: r.value().clone(),
275 });
276 }
277 fx.receipts.push((r.hash().to_string(), text.as_bytes().to_vec()));
278 }
279 Err(e) => setup.push(format!("setup: {source}: {e}")),
280 }
281 cases.push(Case { name, bytes: text.as_bytes().to_vec() });
282 }
283 for (name, _, text) in data::PROBES {
284 cases.push(Case { name, bytes: text.as_bytes().to_vec() });
285 }
286 cases.push(Case { name: NOT_JSON, bytes: b"{".to_vec() });
287 fx.versions.sort_by(|a, b| a.cell_id.cmp(&b.cell_id).then_with(|| a.tx.cmp(&b.tx)));
289 Kit { fixtures: fx, cases, time_aware: false, setup }
290 }
291
292 pub fn time_aware(mut self, yes: bool) -> Kit {
297 self.time_aware = yes;
298 self
299 }
300
301 pub fn fixtures(&self) -> &Fixtures {
303 &self.fixtures
304 }
305
306 pub fn run<R: Resolver, F: FnOnce(&Fixtures) -> R>(&self, load: F) -> Result<(), Report> {
308 self.setup_ok()?;
309 let r = load(&self.fixtures);
310 self.check(&r)
311 }
312
313 pub fn check(&self, resolver: &dyn Resolver) -> Result<(), Report> {
315 self.setup_ok()?;
316 let mut failures = Vec::new();
317 for check in [Kit::check_lazy_calls, Kit::check_unavailable, Kit::check_bytes, Kit::check_asof] {
318 if let Err(mut r) = check(self, resolver) {
319 failures.append(&mut r.failures);
320 }
321 }
322 report(failures)
323 }
324
325 pub fn check_lazy_calls(&self, resolver: &dyn Resolver) -> Result<(), Report> {
328 self.setup_ok()?;
329 let keys = self.fixtures.keys();
330 let mut f = Failures::new("lazy_calls");
331 for case in &self.cases {
332 let rec = Recording::new(resolver);
333 let without = verify(&case.bytes, &keys, None, None);
334 let out = verify(&case.bytes, &keys, Some(&rec), None);
335 let calls = rec.take();
336 if !without.ok {
337 if !calls.is_empty() {
338 f.push(
339 case.name,
340 format!("steps 1-6 fail ({:?}), yet the resolver was asked {calls:?}", without.code),
341 );
342 }
343 if out != without {
344 f.push(case.name, format!("a resolver changed a failure before step 7: {out:?}"));
345 }
346 continue;
347 }
348 match Receipt::from_json(&case.bytes) {
349 Ok(r) => {
350 let want = expected_calls(&r, &out);
351 if calls != want {
352 f.push(case.name, format!("asked {calls:?}, step 7 needs {want:?} (result {:?})", out.code));
353 }
354 }
355 Err(e) => f.push(case.name, format!("steps 1-6 passed but the receipt does not parse: {e}")),
356 }
357 }
358 for name in ["tampered-value", "duplicate-member", NOT_JSON] {
359 if let Some(case) = self.case(name) {
360 if verify(&case.bytes, &keys, None, None).ok {
361 f.push(name, "expected to fail before step 7".to_string());
362 }
363 }
364 }
365 f.finish()
366 }
367
368 pub fn check_unavailable(&self, resolver: &dyn Resolver) -> Result<(), Report> {
370 self.setup_ok()?;
371 let mut f = Failures::new("unavailable");
372 let keys = self.fixtures.keys();
373 self.expect(&mut f, resolver, &keys, "instant-no-valid-time", Want::Fails(ErrorCode::FnUnavailable));
375 self.expect(&mut f, resolver, &keys, "computed-two-inputs", Want::Fails(ErrorCode::InputUnavailable));
376 self.expect(&mut f, resolver, &keys, "first-input-missing", Want::Fails(ErrorCode::InputUnavailable));
377 self.expect(&mut f, resolver, &keys, "computed-with-lineage", Want::EvalUnavailable);
378 let unknown_fn = self.receipt_of("instant-no-valid-time").map(|r| r.cell().fn_hash.clone());
380 let known_receipt = self.fixtures.receipts.first().map(|(h, _)| h.clone()).unwrap_or_default();
381 let known_fn = self.fixtures.descriptors.first().map(|(h, _)| h.clone()).unwrap_or_default();
382 let mut probes: Vec<String> = vec![
383 "0".repeat(64),
384 "f".repeat(64),
385 String::new(),
386 "abc".into(),
387 "g".repeat(64),
388 "\0".into(),
389 format!("{known_receipt}\0"),
390 "../../etc/passwd".into(),
391 "' OR '1'='1".into(),
392 "%".into(),
393 "_".repeat(64),
394 "é".repeat(32),
395 "a".repeat(100_000),
396 known_receipt.to_uppercase(),
397 known_fn.to_uppercase(),
398 format!("{known_receipt}0"),
399 format!(" {known_receipt}"),
400 format!("{known_fn}\n"),
401 known_receipt.get(..63).unwrap_or_default().to_string(),
402 ];
403 probes.extend(unknown_fn);
404 for name in ["computed-two-inputs", "first-input-missing"] {
407 if let Some(r) = self.receipt_of(name) {
408 probes.extend(r.inputs().iter().filter(|h| !self.has_receipt(h)).cloned());
409 }
410 }
411 for p in &probes {
412 if resolver.descriptor(p).is_some() {
413 f.push("lookup", format!("descriptor({}) is not None", shown(p)));
414 }
415 if resolver.receipt(p).is_some() {
416 f.push("lookup", format!("receipt({}) is not None", shown(p)));
417 }
418 }
419 if let Some(r) = self.receipt_of("timeline-1") {
421 for asof in ["", "not-an-instant", "\0", "2024-03-16", "' OR '1'='1", "2024-03-16T09:00:00Z"] {
422 let _ = resolver.evaluate(r.cell(), asof);
423 }
424 }
425 f.finish()
426 }
427
428 pub fn check_bytes(&self, resolver: &dyn Resolver) -> Result<(), Report> {
430 self.setup_ok()?;
431 let mut f = Failures::new("bytes");
432 for (h, bytes) in &self.fixtures.descriptors {
433 for _ in 0..2 {
434 match resolver.descriptor(h) {
435 Some(b) if *b == **bytes => {}
436 Some(b) => {
437 f.push("descriptor", format!("{h}: {} bytes differ from the {} loaded", b.len(), bytes.len()))
438 }
439 None => f.push("descriptor", format!("{h}: None, but it was loaded")),
440 }
441 }
442 }
443 for (h, bytes) in &self.fixtures.receipts {
444 for _ in 0..2 {
445 match resolver.receipt(h) {
446 Some(b) if *b == **bytes => {}
447 Some(b) => {
448 f.push("receipt", format!("{h}: {} bytes differ from the {} loaded", b.len(), bytes.len()))
449 }
450 None => f.push("receipt", format!("{h}: None, but it was loaded")),
451 }
452 }
453 }
454 f.finish()
455 }
456
457 pub fn check_asof(&self, resolver: &dyn Resolver) -> Result<(), Report> {
460 self.setup_ok()?;
461 let mut f = Failures::new("asof");
462 let keys = self.fixtures.keys();
463 for case in &self.cases {
465 let Ok(r) = Receipt::from_json(&case.bytes) else { continue };
466 let rec = Recording::new(resolver);
467 let _ = verify(&case.bytes, &keys, Some(&rec), None);
468 for call in rec.take() {
469 if let Call::Evaluate { cell_id, asof } = call {
470 if cell_id != r.cell().id() || asof != r.asof() {
471 f.push(case.name, format!("evaluate got ({cell_id}, {asof}), not the receipt's cell and asof"));
472 }
473 }
474 }
475 }
476 self.expect(&mut f, resolver, &keys, "stored-number", Want::Recomputed);
477 if !self.time_aware {
478 return f.finish();
479 }
480 for name in ["timeline-1", "timeline-1-later", "timeline-2"] {
483 self.expect(&mut f, resolver, &keys, name, Want::Recomputed);
484 }
485 self.expect(&mut f, resolver, &keys, "timeline-0", Want::EvalUnavailable);
486 let (Some(v1), Some(v2)) = (self.receipt_of("timeline-1"), self.receipt_of("timeline-2")) else {
488 f.push("timeline", "fixtures missing".to_string());
489 return f.finish();
490 };
491 let cell = v1.cell();
492 let points: [(&str, Option<&Scalar>); 7] = [
493 ("0001-01-01T00:00:00.000000Z", None),
494 ("2024-03-15T15:59:59.999999Z", None),
495 (v1.tx(), Some(v1.value())),
496 ("2024-03-17T00:00:00.000000Z", Some(v1.value())),
497 ("2024-03-18T09:59:59.999999Z", Some(v1.value())),
498 (v2.tx(), Some(v2.value())),
499 ("9999-12-31T23:59:59.999999Z", Some(v2.value())),
500 ];
501 for (asof, want) in points {
502 let got = resolver.evaluate(cell, asof);
503 if got.as_ref() != want {
504 f.push("timeline", format!("evaluate as of {asof} gave {got:?}, want {want:?}"));
505 }
506 }
507 f.finish()
508 }
509
510 fn setup_ok(&self) -> Result<(), Report> {
511 if self.setup.is_empty() {
512 Ok(())
513 } else {
514 Err(Report { failures: self.setup.clone() })
515 }
516 }
517
518 fn case(&self, name: &str) -> Option<&Case> {
519 self.cases.iter().find(|c| c.name == name)
520 }
521
522 fn receipt_of(&self, name: &str) -> Option<Receipt> {
523 self.case(name).and_then(|c| Receipt::from_json(&c.bytes).ok())
524 }
525
526 fn has_receipt(&self, hash: &str) -> bool {
527 self.fixtures.receipts.iter().any(|(h, _)| h == hash)
528 }
529
530 fn expect(&self, f: &mut Failures, resolver: &dyn Resolver, keys: &Keys, name: &str, want: Want) {
532 let Some(case) = self.case(name) else {
533 f.push(name, "no such fixture".to_string());
534 return;
535 };
536 let out = verify(&case.bytes, keys, Some(resolver), None);
537 let good = match want {
538 Want::Recomputed => out.ok && out.level == Some(Level::Recomputed) && out.code.is_none(),
539 Want::EvalUnavailable => {
540 out.ok && out.level == Some(Level::Signed) && out.code == Some(ErrorCode::EvalUnavailable)
541 }
542 Want::Fails(code) => !out.ok && out.code == Some(code),
543 };
544 if !good {
545 f.push(
546 name,
547 format!(
548 "want {want:?}, got ok={} level={:?} code={:?} {} {}",
549 out.ok, out.level, out.code, out.path, out.detail
550 ),
551 );
552 }
553 }
554}
555
556#[derive(Clone, Copy, Debug)]
558enum Want {
559 Recomputed,
560 EvalUnavailable,
561 Fails(ErrorCode),
562}
563
564fn expected_calls(r: &Receipt, out: &VerifyResult) -> Vec<Call> {
566 let mut calls = vec![Call::LookupDescriptor(r.cell().fn_hash.clone())];
567 let all_inputs = || r.inputs().iter().map(|h| Call::LookupReceipt(h.clone()));
568 let evaluate = Call::Evaluate { cell_id: r.cell().id(), asof: r.asof().to_string() };
569 match out.code {
570 None | Some(ErrorCode::ValueMismatch | ErrorCode::EvalUnavailable) => {
572 calls.extend(all_inputs());
573 calls.push(evaluate);
574 }
575 Some(_) if out.path.starts_with("$.inputs.") => {
577 let failing = &out.path["$.inputs.".len()..];
578 let n = r.inputs().iter().position(|h| h == failing).map_or(r.inputs().len(), |i| i + 1);
579 calls.extend(all_inputs().take(n));
580 }
581 Some(ErrorCode::TimeOrder) if out.path == "$.tx" => calls.extend(all_inputs()),
583 Some(_) => {}
585 }
586 calls
587}
588
589fn shown(s: &str) -> String {
591 if s.chars().count() > 80 {
592 format!("{:?}… ({} bytes)", s.chars().take(16).collect::<String>(), s.len())
593 } else {
594 format!("{s:?}")
595 }
596}
597
598struct Failures {
599 check: &'static str,
600 lines: Vec<String>,
601}
602
603impl Failures {
604 fn new(check: &'static str) -> Failures {
605 Failures { check, lines: Vec::new() }
606 }
607
608 fn push(&mut self, case: &str, what: String) {
609 self.lines.push(format!("{}: {case}: {what}", self.check));
610 }
611
612 fn finish(self) -> Result<(), Report> {
613 report(self.lines)
614 }
615}
616
617fn report(failures: Vec<String>) -> Result<(), Report> {
618 if failures.is_empty() {
619 Ok(())
620 } else {
621 Err(Report { failures })
622 }
623}