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