1use serde::{Deserialize, Serialize};
7
8use crate::{internal::visibility::Visibility, 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 reason(&self) -> Reason<'_> {
258 match &self.inner {
259 ScoreInner::Success(_) => Reason::none(),
260 ScoreInner::Failure(_, reasons) => Reason::new(reasons.as_deref()),
261 }
262 }
263
264 pub(crate) fn order(this: &Self, other: &Self) -> std::cmp::Ordering {
270 use ScoreInner::{Failure, Success};
271
272 match (&this.inner, &other.inner) {
273 (Success(this), Success(other)) => this.cmp(other),
274 (Success(_), Failure(..)) => std::cmp::Ordering::Less,
275 (Failure(..), Success(_)) => std::cmp::Ordering::Greater,
276 (Failure(this, _), Failure(other, _)) => this.cmp(other),
277 }
278 }
279}
280
281#[derive(Debug)]
282enum ScoreInner {
283 Success(SuccessScore),
284 Failure(FailureScore, Option<Vec<String>>),
285}
286
287pub(crate) struct Reason<'a>(Option<&'a [String]>);
288
289impl<'a> Reason<'a> {
290 fn new(reasons: Option<&'a [String]>) -> Self {
291 Self(reasons)
292 }
293
294 fn none() -> Self {
295 Self(None)
296 }
297}
298
299impl std::fmt::Display for Reason<'_> {
300 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
301 match self.0 {
302 None => f.write_str("<missing>"),
303 Some(reasons) => {
304 let mut first = true;
305 for reason in reasons.iter() {
306 if !first {
307 writeln!(f)?;
308 }
309 write!(f, "- {}", reason)?;
310 first = false;
311 }
312 Ok(())
313 }
314 }
315 }
316}
317
318#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
319pub(crate) struct SuccessScore(pub(crate) u32);
320
321#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
322pub(crate) struct FailureScore(pub(crate) u32);
323
324pub(crate) mod internal {
329 use super::*;
330
331 use crate::{input::internal::Any, Features};
332
333 use anyhow::Context;
334 use thiserror::Error;
335
336 #[derive(Debug)]
337 pub(crate) enum AnnotatedMatch<'a> {
338 User(Score),
340 WrongTag,
342 Gated(&'a Features),
344 }
345
346 impl AnnotatedMatch<'_> {
347 pub(crate) fn try_into_score(self) -> Option<Score> {
348 if let Self::User(score) = self {
349 Some(score)
350 } else {
351 None
352 }
353 }
354 }
355
356 pub(crate) trait Benchmark {
358 fn try_match(&self, input: &Any, context: &MatchContext) -> AnnotatedMatch<'_>;
359
360 fn description(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result;
361
362 fn run(
363 &self,
364 input: &Any,
365 checkpoint: Checkpoint<'_>,
366 output: &mut dyn Output,
367 ) -> anyhow::Result<serde_json::Value>;
368
369 fn visibility(&self) -> Visibility<'_>;
370
371 fn as_regression(&self) -> Option<&dyn Regression>;
373
374 fn as_string(&self) -> String {
375 Description(self).to_string()
376 }
377 }
378
379 struct Description<'a, T: ?Sized>(&'a T);
380
381 impl<T> std::fmt::Display for Description<'_, T>
382 where
383 T: Benchmark + ?Sized,
384 {
385 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
386 self.0.description(f)
387 }
388 }
389
390 pub(crate) struct Checked {
391 pub(crate) json: serde_json::Value,
392 pub(crate) display: Box<dyn std::fmt::Display>,
393 }
394
395 impl Checked {
396 fn new<T>(value: T) -> Result<Self, serde_json::Error>
398 where
399 T: Serialize + std::fmt::Display + 'static,
400 {
401 Ok(Self {
402 json: serde_json::to_value(&value)?,
403 display: Box::new(value),
404 })
405 }
406 }
407
408 pub(crate) type CheckedPassFail = PassFail<Checked, Checked>;
409
410 pub(crate) trait Regression {
411 fn tolerance(&self) -> &dyn crate::input::internal::DynInput;
412 fn input_tag(&self) -> &'static str;
413 fn check(
414 &self,
415 tolerances: &Any,
416 input: &Any,
417 before: &serde_json::Value,
418 after: &serde_json::Value,
419 ) -> anyhow::Result<CheckedPassFail>;
420 }
421
422 impl std::fmt::Debug for dyn Regression + '_ {
423 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
424 f.debug_struct("dyn Regression")
425 .field("tolerance", &self.tolerance().tag())
426 .field("input_tag", &self.input_tag())
427 .finish()
428 }
429 }
430
431 pub(crate) trait AsRegression<T> {
432 fn as_regression(benchmark: &T) -> Option<&dyn Regression>;
433 }
434
435 #[derive(Debug, Clone, Copy)]
436 pub(crate) struct NoRegression;
437
438 impl<T> AsRegression<T> for NoRegression {
439 fn as_regression(_benchmark: &T) -> Option<&dyn Regression> {
440 None
441 }
442 }
443
444 #[derive(Debug, Clone, Copy)]
445 pub(crate) struct WithRegression;
446
447 impl<T> AsRegression<T> for WithRegression
448 where
449 T: super::Regression,
450 {
451 fn as_regression(benchmark: &T) -> Option<&dyn Regression> {
452 Some(benchmark)
453 }
454 }
455
456 impl<T> Regression for T
457 where
458 T: super::Regression,
459 {
460 fn tolerance(&self) -> &dyn crate::input::internal::DynInput {
461 &crate::input::internal::Wrapper::<T::Tolerances>::INSTANCE
462 }
463
464 fn input_tag(&self) -> &'static str {
465 T::Input::tag()
466 }
467
468 fn check(
469 &self,
470 tolerance: &Any,
471 input: &Any,
472 before: &serde_json::Value,
473 after: &serde_json::Value,
474 ) -> anyhow::Result<CheckedPassFail> {
475 let tolerance = tolerance
476 .downcast_ref::<T::Tolerances>()
477 .ok_or_else(|| BadDownCast::new(T::Tolerances::tag(), tolerance.tag()))
478 .context("failed to obtain tolerance")?;
479
480 let input = input
481 .downcast_ref::<T::Input>()
482 .ok_or_else(|| BadDownCast::new(T::Input::tag(), input.tag()))
483 .context("failed to obtain input")?;
484
485 let before = T::Output::deserialize(before)
486 .map_err(|err| DeserializationError::new(Kind::Before, err))?;
487
488 let after = T::Output::deserialize(after)
489 .map_err(|err| DeserializationError::new(Kind::After, err))?;
490
491 let passfail = match self.check(tolerance, input, &before, &after)? {
492 PassFail::Pass(pass) => PassFail::Pass(Checked::new(pass)?),
493 PassFail::Fail(fail) => PassFail::Fail(Checked::new(fail)?),
494 };
495
496 Ok(passfail)
497 }
498 }
499
500 #[derive(Debug, Clone, Copy)]
501 pub(crate) struct Wrapper<T, R = NoRegression> {
502 benchmark: T,
503 _regression: R,
504 }
505
506 impl<T, R> Wrapper<T, R> {
507 pub(crate) const fn new(benchmark: T, regression: R) -> Self {
508 Self {
509 benchmark,
510 _regression: regression,
511 }
512 }
513 }
514
515 impl<T, R> Benchmark for Wrapper<T, R>
516 where
517 T: super::Benchmark,
518 R: AsRegression<T>,
519 {
520 fn try_match(&self, input: &Any, context: &MatchContext) -> AnnotatedMatch<'_> {
521 if let Some(cast) = input.downcast_ref::<T::Input>() {
522 AnnotatedMatch::User(self.benchmark.try_match(cast, context))
523 } else {
524 AnnotatedMatch::WrongTag
525 }
526 }
527
528 fn description(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
529 writeln!(f, "tag \"{}\"", <T::Input as Input>::tag())?;
530 self.benchmark.description(f)
531 }
532
533 fn run(
534 &self,
535 input: &Any,
536 checkpoint: Checkpoint<'_>,
537 output: &mut dyn Output,
538 ) -> anyhow::Result<serde_json::Value> {
539 match input.downcast_ref::<T::Input>() {
540 Some(input) => {
541 let result = self.benchmark.run(input, checkpoint, output)?;
542 Ok(serde_json::to_value(result)?)
543 }
544 None => Err(BadDownCast::new(T::Input::tag(), input.tag()).into()),
545 }
546 }
547
548 fn visibility(&self) -> Visibility<'_> {
549 Visibility::Available
550 }
551
552 fn as_regression(&self) -> Option<&dyn Regression> {
554 R::as_regression(&self.benchmark)
555 }
556 }
557
558 #[derive(Debug, Clone, Copy, Error)]
563 #[error(
564 "INTERNAL ERROR: bad downcast - expected \"{}\" but got \"{}\"",
565 self.expected,
566 self.got
567 )]
568 struct BadDownCast {
569 expected: &'static str,
570 got: &'static str,
571 }
572
573 impl BadDownCast {
574 fn new(expected: &'static str, got: &'static str) -> Self {
575 Self { expected, got }
576 }
577 }
578
579 #[derive(Debug, Error)]
580 #[error(
581 "the \"{}\" results do not match the output schema expected by this benchmark",
582 self.kind
583 )]
584 struct DeserializationError {
585 kind: Kind,
586 source: serde_json::Error,
587 }
588
589 impl DeserializationError {
590 fn new(kind: Kind, source: serde_json::Error) -> Self {
591 Self { kind, source }
592 }
593 }
594
595 #[derive(Debug, Clone, Copy)]
596 enum Kind {
597 Before,
598 After,
599 }
600
601 impl std::fmt::Display for Kind {
602 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
603 let as_str = match self {
604 Self::Before => "before",
605 Self::After => "after",
606 };
607
608 write!(f, "{}", as_str)
609 }
610 }
611
612 pub(crate) struct PartiallyGated<I> {
626 features: Features,
627 description: String,
628 _input: std::marker::PhantomData<I>,
629 }
630
631 impl<I> PartiallyGated<I> {
632 pub(crate) fn new(features: Features, description: String) -> Self {
633 Self {
634 features,
635 description,
636 _input: std::marker::PhantomData,
637 }
638 }
639 }
640
641 impl<I> Benchmark for PartiallyGated<I>
642 where
643 I: Input,
644 {
645 fn try_match(&self, input: &Any, _context: &MatchContext) -> AnnotatedMatch<'_> {
646 if input.is::<I>() {
647 AnnotatedMatch::Gated(&self.features)
648 } else {
649 AnnotatedMatch::WrongTag
650 }
651 }
652
653 fn description(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
654 writeln!(f, "tag \"{}\"", I::tag())?;
655 f.write_str(&self.description)
656 }
657
658 fn run(
659 &self,
660 _input: &Any,
661 _checkpoint: Checkpoint<'_>,
662 _output: &mut dyn Output,
663 ) -> anyhow::Result<serde_json::Value> {
664 anyhow::bail!("tried to run a gated benchmark - this should be unreachable")
665 }
666
667 fn visibility(&self) -> Visibility<'_> {
668 Visibility::Gated {
669 features: &self.features,
670 }
671 }
672
673 fn as_regression(&self) -> Option<&dyn Regression> {
674 None
675 }
676 }
677
678 pub(crate) struct FullyGated {
686 tag: &'static str,
687 features: Features,
688 description: String,
689 }
690
691 impl FullyGated {
692 pub(crate) fn new(tag: &'static str, features: Features, description: String) -> Self {
693 Self {
694 tag,
695 features,
696 description,
697 }
698 }
699 }
700
701 impl Benchmark for FullyGated {
702 fn try_match(&self, _input: &Any, _context: &MatchContext) -> AnnotatedMatch<'_> {
703 AnnotatedMatch::WrongTag
704 }
705
706 fn description(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
707 writeln!(f, "tag \"{}\"", self.tag)?;
708 f.write_str(&self.description)
709 }
710
711 fn run(
712 &self,
713 _input: &Any,
714 _checkpoint: Checkpoint<'_>,
715 _output: &mut dyn Output,
716 ) -> anyhow::Result<serde_json::Value> {
717 anyhow::bail!("tried to run a gated benchmark - this should be unreachable")
718 }
719
720 fn visibility(&self) -> Visibility<'_> {
721 Visibility::Gated {
722 features: &self.features,
723 }
724 }
725
726 fn as_regression(&self) -> Option<&dyn Regression> {
727 None
728 }
729 }
730}
731
732#[cfg(test)]
737mod tests {
738 use super::*;
739
740 #[derive(Debug, Clone, Copy, PartialEq)]
741 pub(crate) enum RawScore {
742 Success(SuccessScore),
743 Failure(FailureScore),
744 }
745
746 fn as_raw(score: &Score) -> RawScore {
747 match &score.inner {
748 ScoreInner::Success(s) => RawScore::Success(*s),
749 ScoreInner::Failure(s, _) => RawScore::Failure(*s),
750 }
751 }
752
753 impl RawScore {
754 #[cfg(test)]
755 fn success(score: u32) -> Self {
756 Self::Success(SuccessScore(score))
757 }
758
759 #[cfg(test)]
760 fn failure(score: u32) -> Self {
761 Self::Failure(FailureScore(score))
762 }
763 }
764
765 #[test]
766 fn test_score_no_reasons() {
767 let context = MatchContext::new();
768 let mut score = context.success(0);
769 assert!(score.is_success());
770 assert_eq!(as_raw(&score), RawScore::success(0));
771
772 score.penalize(10);
773 assert!(score.is_success());
774 assert_eq!(as_raw(&score), RawScore::success(10));
775
776 score.penalize(10);
777 assert!(score.is_success());
778 assert_eq!(as_raw(&score), RawScore::success(20));
779
780 score.penalize(u32::MAX);
782 assert!(score.is_success());
783 assert_eq!(as_raw(&score), RawScore::success(u32::MAX));
784
785 score.fail(5, &"some reason that is not evaluated");
787 assert!(!score.is_success());
788 assert_eq!(as_raw(&score), RawScore::failure(5));
789
790 score.fail(10, &"another reason that is not evaluated");
791 assert!(!score.is_success());
792 assert_eq!(as_raw(&score), RawScore::failure(15));
793
794 score.penalize(5);
796 assert!(!score.is_success());
797 assert_eq!(as_raw(&score), RawScore::failure(15));
798
799 assert_eq!(score.reason().to_string(), "<missing>");
801 }
802
803 #[test]
804 fn test_score_with_reasons() {
805 let context = MatchContext::with_reasons();
806 let mut score = context.success(0);
807 assert!(score.is_success());
808 assert_eq!(as_raw(&score), RawScore::success(0));
809
810 score.penalize(10);
811 assert!(score.is_success());
812 assert_eq!(as_raw(&score), RawScore::success(10));
813
814 score.penalize(10);
815 assert!(score.is_success());
816 assert_eq!(as_raw(&score), RawScore::success(20));
817 assert_eq!(score.reason().to_string(), "<missing>");
818
819 score.penalize(u32::MAX);
821 assert!(score.is_success());
822 assert_eq!(as_raw(&score), RawScore::success(u32::MAX));
823
824 score.fail(5, &"some reason that is evaluated");
826 assert!(!score.is_success());
827 assert_eq!(as_raw(&score), RawScore::failure(5));
828
829 score.fail(10, &"another reason that is evaluated");
830 assert!(!score.is_success());
831 assert_eq!(as_raw(&score), RawScore::failure(15));
832
833 score.penalize(5);
835 assert!(!score.is_success());
836 assert_eq!(as_raw(&score), RawScore::failure(15));
837
838 let expected = "- some reason that is evaluated\n\
840 - another reason that is evaluated";
841 assert_eq!(score.reason().to_string(), expected);
842 }
843}