1use serde::{Deserialize, Serialize};
7
8use crate::{Checkpoint, Input, Output};
9
10pub trait Benchmark: 'static {
20 type Input: Input + 'static;
22
23 type Output: Serialize;
25
26 fn try_match(&self, input: &Self::Input, context: &MatchContext) -> Score;
38
39 fn description(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result;
41
42 fn run(
51 &self,
52 input: &Self::Input,
53 checkpoint: Checkpoint<'_>,
54 output: &mut dyn Output,
55 ) -> anyhow::Result<Self::Output>;
56}
57
58pub trait Regression: Benchmark<Output: for<'a> Deserialize<'a>> {
68 type Tolerances: Input + 'static;
70
71 type Pass: Serialize + std::fmt::Display + 'static;
73
74 type Fail: Serialize + std::fmt::Display + 'static;
76
77 fn check(
88 &self,
89 tolerances: &Self::Tolerances,
90 input: &Self::Input,
91 before: &Self::Output,
92 after: &Self::Output,
93 ) -> anyhow::Result<PassFail<Self::Pass, Self::Fail>>;
94}
95
96#[derive(Debug, Clone, Copy)]
98pub enum PassFail<P, F> {
99 Pass(P),
100 Fail(F),
101}
102
103#[derive(Debug)]
115pub struct MatchContext {
116 record_failure_reasons: bool,
117}
118
119impl MatchContext {
120 pub fn success(&self, score: u32) -> Score {
124 Score {
125 inner: ScoreInner::Success(SuccessScore(score)),
126 context: self.hidden_clone(),
127 }
128 }
129
130 pub fn fail(&self, score: u32, reason: &dyn std::fmt::Display) -> Score {
134 let mut s = self.success(u32::MAX);
135 s.fail(score, reason);
136 s
137 }
138
139 pub fn test<T>(benchmark: &T, input: &T::Input) -> TestScore
142 where
143 T: Benchmark,
144 {
145 benchmark
146 .try_match(input, &Self::with_reasons())
147 .into_test()
148 }
149
150 fn hidden_clone(&self) -> Self {
151 Self {
152 record_failure_reasons: self.record_failure_reasons,
153 }
154 }
155
156 pub(crate) fn new() -> Self {
158 Self {
159 record_failure_reasons: false,
160 }
161 }
162
163 pub(crate) fn with_reasons() -> Self {
165 Self {
166 record_failure_reasons: true,
167 }
168 }
169}
170
171#[derive(Debug)]
174pub enum TestScore {
175 Success(u32),
176 Failure {
177 score: u32,
178 reasons: Option<Vec<String>>,
179 },
180}
181
182#[derive(Debug)]
192pub struct Score {
193 inner: ScoreInner,
194 context: MatchContext,
195}
196
197impl Score {
198 pub fn penalize(&mut self, by: u32) {
202 match self.inner {
203 ScoreInner::Success(ref mut v) => v.0 = v.0.saturating_add(by),
204 ScoreInner::Failure(..) => {}
205 }
206 }
207
208 pub fn fail(&mut self, by: u32, reason: &dyn std::fmt::Display) {
213 match &mut self.inner {
214 ScoreInner::Success(_) => {
215 self.inner = ScoreInner::Failure(
216 FailureScore(by),
217 self.context
218 .record_failure_reasons
219 .then(|| vec![reason.to_string()]),
220 );
221 }
222 ScoreInner::Failure(score, reasons) => {
223 score.0 = (score.0).saturating_add(by);
224 if self.context.record_failure_reasons {
225 reasons
226 .get_or_insert_with(|| Vec::with_capacity(1))
227 .push(reason.to_string())
228 }
229 }
230 }
231 }
232
233 #[must_use = "this function has no side-effects"]
236 pub fn is_success(&self) -> bool {
237 matches!(self.inner, ScoreInner::Success(_))
238 }
239
240 fn into_test(self) -> TestScore {
241 match self.inner {
242 ScoreInner::Success(score) => TestScore::Success(score.0),
243 ScoreInner::Failure(score, reasons) => TestScore::Failure {
244 score: score.0,
245 reasons,
246 },
247 }
248 }
249
250 pub(crate) fn match_score(&self) -> Option<SuccessScore> {
251 match self.inner {
252 ScoreInner::Success(score) => Some(score),
253 ScoreInner::Failure(..) => None,
254 }
255 }
256
257 pub(crate) fn as_raw(&self) -> RawScore {
258 match self.inner {
259 ScoreInner::Success(s) => RawScore::Success(s),
260 ScoreInner::Failure(s, _) => RawScore::Failure(s),
261 }
262 }
263
264 pub(crate) fn reason(&self) -> Reason<'_> {
265 match &self.inner {
266 ScoreInner::Success(_) => Reason::none(),
267 ScoreInner::Failure(_, reasons) => Reason::new(reasons.as_deref()),
268 }
269 }
270}
271
272#[derive(Debug)]
273enum ScoreInner {
274 Success(SuccessScore),
275 Failure(FailureScore, Option<Vec<String>>),
276}
277
278pub(crate) struct Reason<'a>(Option<&'a [String]>);
279
280impl<'a> Reason<'a> {
281 fn new(reasons: Option<&'a [String]>) -> Self {
282 Self(reasons)
283 }
284
285 fn none() -> Self {
286 Self(None)
287 }
288}
289
290impl std::fmt::Display for Reason<'_> {
291 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
292 match self.0 {
293 None => f.write_str("<missing>"),
294 Some(reasons) => {
295 let mut first = true;
296 for reason in reasons.iter() {
297 if !first {
298 writeln!(f)?;
299 }
300 write!(f, "- {}", reason)?;
301 first = false;
302 }
303 Ok(())
304 }
305 }
306 }
307}
308
309#[derive(Debug, Clone, Copy, PartialEq)]
310pub(crate) enum RawScore {
311 Success(SuccessScore),
312 Failure(FailureScore),
313}
314
315impl RawScore {
316 #[cfg(test)]
317 fn success(score: u32) -> Self {
318 Self::Success(SuccessScore(score))
319 }
320
321 #[cfg(test)]
322 fn failure(score: u32) -> Self {
323 Self::Failure(FailureScore(score))
324 }
325}
326
327#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
328pub(crate) struct SuccessScore(pub(crate) u32);
329
330#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
331pub(crate) struct FailureScore(pub(crate) u32);
332
333pub(crate) mod internal {
338 use super::*;
339
340 use crate::input::internal::Any;
341
342 use anyhow::Context;
343 use thiserror::Error;
344
345 pub(crate) trait Benchmark {
347 fn try_match(&self, input: &Any, context: &MatchContext) -> Score;
348
349 fn description(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result;
350
351 fn run(
352 &self,
353 input: &Any,
354 checkpoint: Checkpoint<'_>,
355 output: &mut dyn Output,
356 ) -> anyhow::Result<serde_json::Value>;
357
358 fn as_regression(&self) -> Option<&dyn Regression>;
360
361 fn as_string(&self) -> String {
362 Description(self).to_string()
363 }
364 }
365
366 struct Description<'a, T: ?Sized>(&'a T);
367
368 impl<T> std::fmt::Display for Description<'_, T>
369 where
370 T: Benchmark + ?Sized,
371 {
372 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
373 self.0.description(f)
374 }
375 }
376
377 pub(crate) struct Checked {
378 pub(crate) json: serde_json::Value,
379 pub(crate) display: Box<dyn std::fmt::Display>,
380 }
381
382 impl Checked {
383 fn new<T>(value: T) -> Result<Self, serde_json::Error>
385 where
386 T: Serialize + std::fmt::Display + 'static,
387 {
388 Ok(Self {
389 json: serde_json::to_value(&value)?,
390 display: Box::new(value),
391 })
392 }
393 }
394
395 pub(crate) type CheckedPassFail = PassFail<Checked, Checked>;
396
397 pub(crate) trait Regression {
398 fn tolerance(&self) -> &dyn crate::input::internal::DynInput;
399 fn input_tag(&self) -> &'static str;
400 fn check(
401 &self,
402 tolerances: &Any,
403 input: &Any,
404 before: &serde_json::Value,
405 after: &serde_json::Value,
406 ) -> anyhow::Result<CheckedPassFail>;
407 }
408
409 impl std::fmt::Debug for dyn Regression + '_ {
410 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
411 f.debug_struct("dyn Regression")
412 .field("tolerance", &self.tolerance().tag())
413 .field("input_tag", &self.input_tag())
414 .finish()
415 }
416 }
417
418 pub(crate) trait AsRegression<T> {
419 fn as_regression(benchmark: &T) -> Option<&dyn Regression>;
420 }
421
422 #[derive(Debug, Clone, Copy)]
423 pub(crate) struct NoRegression;
424
425 impl<T> AsRegression<T> for NoRegression {
426 fn as_regression(_benchmark: &T) -> Option<&dyn Regression> {
427 None
428 }
429 }
430
431 #[derive(Debug, Clone, Copy)]
432 pub(crate) struct WithRegression;
433
434 impl<T> AsRegression<T> for WithRegression
435 where
436 T: super::Regression,
437 {
438 fn as_regression(benchmark: &T) -> Option<&dyn Regression> {
439 Some(benchmark)
440 }
441 }
442
443 impl<T> Regression for T
444 where
445 T: super::Regression,
446 {
447 fn tolerance(&self) -> &dyn crate::input::internal::DynInput {
448 &crate::input::internal::Wrapper::<T::Tolerances>::INSTANCE
449 }
450
451 fn input_tag(&self) -> &'static str {
452 T::Input::tag()
453 }
454
455 fn check(
456 &self,
457 tolerance: &Any,
458 input: &Any,
459 before: &serde_json::Value,
460 after: &serde_json::Value,
461 ) -> anyhow::Result<CheckedPassFail> {
462 let tolerance = tolerance
463 .downcast_ref::<T::Tolerances>()
464 .ok_or_else(|| BadDownCast::new(T::Tolerances::tag(), tolerance.tag()))
465 .context("failed to obtain tolerance")?;
466
467 let input = input
468 .downcast_ref::<T::Input>()
469 .ok_or_else(|| BadDownCast::new(T::Input::tag(), input.tag()))
470 .context("failed to obtain input")?;
471
472 let before = T::Output::deserialize(before)
473 .map_err(|err| DeserializationError::new(Kind::Before, err))?;
474
475 let after = T::Output::deserialize(after)
476 .map_err(|err| DeserializationError::new(Kind::After, err))?;
477
478 let passfail = match self.check(tolerance, input, &before, &after)? {
479 PassFail::Pass(pass) => PassFail::Pass(Checked::new(pass)?),
480 PassFail::Fail(fail) => PassFail::Fail(Checked::new(fail)?),
481 };
482
483 Ok(passfail)
484 }
485 }
486
487 #[derive(Debug, Clone, Copy)]
488 pub(crate) struct Wrapper<T, R = NoRegression> {
489 benchmark: T,
490 _regression: R,
491 }
492
493 impl<T, R> Wrapper<T, R> {
494 pub(crate) const fn new(benchmark: T, regression: R) -> Self {
495 Self {
496 benchmark,
497 _regression: regression,
498 }
499 }
500 }
501
502 const MATCH_FAIL: u32 = 10_000;
503
504 impl<T, R> Benchmark for Wrapper<T, R>
505 where
506 T: super::Benchmark,
507 R: AsRegression<T>,
508 {
509 fn try_match(&self, input: &Any, context: &MatchContext) -> Score {
510 if let Some(cast) = input.downcast_ref::<T::Input>() {
511 self.benchmark.try_match(cast, context)
512 } else {
513 struct TagMismatch<T>(&'static str, std::marker::PhantomData<T>);
514
515 impl<T> std::fmt::Display for TagMismatch<T>
516 where
517 T: super::Benchmark,
518 {
519 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
520 write!(
521 f,
522 "expected tag \"{}\" - instead got \"{}\"",
523 T::Input::tag(),
524 self.0,
525 )
526 }
527 }
528
529 context.fail(
530 MATCH_FAIL,
531 &TagMismatch::<T>(input.tag(), std::marker::PhantomData),
532 )
533 }
534 }
535
536 fn description(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
537 writeln!(f, "tag \"{}\"", <T::Input as Input>::tag())?;
538 self.benchmark.description(f)
539 }
540
541 fn run(
542 &self,
543 input: &Any,
544 checkpoint: Checkpoint<'_>,
545 output: &mut dyn Output,
546 ) -> anyhow::Result<serde_json::Value> {
547 match input.downcast_ref::<T::Input>() {
548 Some(input) => {
549 let result = self.benchmark.run(input, checkpoint, output)?;
550 Ok(serde_json::to_value(result)?)
551 }
552 None => Err(BadDownCast::new(T::Input::tag(), input.tag()).into()),
553 }
554 }
555
556 fn as_regression(&self) -> Option<&dyn Regression> {
558 R::as_regression(&self.benchmark)
559 }
560 }
561
562 #[derive(Debug, Clone, Copy, Error)]
567 #[error(
568 "INTERNAL ERROR: bad downcast - expected \"{}\" but got \"{}\"",
569 self.expected,
570 self.got
571 )]
572 struct BadDownCast {
573 expected: &'static str,
574 got: &'static str,
575 }
576
577 impl BadDownCast {
578 fn new(expected: &'static str, got: &'static str) -> Self {
579 Self { expected, got }
580 }
581 }
582
583 #[derive(Debug, Error)]
584 #[error(
585 "the \"{}\" results do not match the output schema expected by this benchmark",
586 self.kind
587 )]
588 struct DeserializationError {
589 kind: Kind,
590 source: serde_json::Error,
591 }
592
593 impl DeserializationError {
594 fn new(kind: Kind, source: serde_json::Error) -> Self {
595 Self { kind, source }
596 }
597 }
598
599 #[derive(Debug, Clone, Copy)]
600 enum Kind {
601 Before,
602 After,
603 }
604
605 impl std::fmt::Display for Kind {
606 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
607 let as_str = match self {
608 Self::Before => "before",
609 Self::After => "after",
610 };
611
612 write!(f, "{}", as_str)
613 }
614 }
615}
616
617#[cfg(test)]
622mod tests {
623 use super::*;
624
625 #[test]
626 fn test_score_no_reasons() {
627 let context = MatchContext::new();
628 let mut score = context.success(0);
629 assert!(score.is_success());
630 assert_eq!(score.as_raw(), RawScore::success(0));
631
632 score.penalize(10);
633 assert!(score.is_success());
634 assert_eq!(score.as_raw(), RawScore::success(10));
635
636 score.penalize(10);
637 assert!(score.is_success());
638 assert_eq!(score.as_raw(), RawScore::success(20));
639
640 score.penalize(u32::MAX);
642 assert!(score.is_success());
643 assert_eq!(score.as_raw(), RawScore::success(u32::MAX));
644
645 score.fail(5, &"some reason that is not evaluated");
647 assert!(!score.is_success());
648 assert_eq!(score.as_raw(), RawScore::failure(5));
649
650 score.fail(10, &"another reason that is not evaluated");
651 assert!(!score.is_success());
652 assert_eq!(score.as_raw(), RawScore::failure(15));
653
654 score.penalize(5);
656 assert!(!score.is_success());
657 assert_eq!(score.as_raw(), RawScore::failure(15));
658
659 assert_eq!(score.reason().to_string(), "<missing>");
661 }
662
663 #[test]
664 fn test_score_with_reasons() {
665 let context = MatchContext::with_reasons();
666 let mut score = context.success(0);
667 assert!(score.is_success());
668 assert_eq!(score.as_raw(), RawScore::success(0));
669
670 score.penalize(10);
671 assert!(score.is_success());
672 assert_eq!(score.as_raw(), RawScore::success(10));
673
674 score.penalize(10);
675 assert!(score.is_success());
676 assert_eq!(score.as_raw(), RawScore::success(20));
677 assert_eq!(score.reason().to_string(), "<missing>");
678
679 score.penalize(u32::MAX);
681 assert!(score.is_success());
682 assert_eq!(score.as_raw(), RawScore::success(u32::MAX));
683
684 score.fail(5, &"some reason that is evaluated");
686 assert!(!score.is_success());
687 assert_eq!(score.as_raw(), RawScore::failure(5));
688
689 score.fail(10, &"another reason that is evaluated");
690 assert!(!score.is_success());
691 assert_eq!(score.as_raw(), RawScore::failure(15));
692
693 score.penalize(5);
695 assert!(!score.is_success());
696 assert_eq!(score.as_raw(), RawScore::failure(15));
697
698 let expected = "- some reason that is evaluated\n\
700 - another reason that is evaluated";
701 assert_eq!(score.reason().to_string(), expected);
702 }
703}