Skip to main content

diskann_benchmark_runner/
benchmark.rs

1/*
2 * Copyright (c) Microsoft Corporation.
3 * Licensed under the MIT license.
4 */
5
6use serde::{Deserialize, Serialize};
7
8use crate::{Checkpoint, Input, Output, internal::visibility::Visibility};
9
10///////////////
11// Benchmark //
12///////////////
13
14/// A registered benchmark.
15///
16/// Benchmarks consist of an [`Input`] and a corresponding serialized `Output`. Inputs will
17/// first be validated with the benchmark using [`try_match`](Self::try_match). Only
18/// successful matches will be passed to [`run`](Self::run).
19pub trait Benchmark: 'static {
20    /// The [`Input`] type this benchmark matches against.
21    type Input: Input + 'static;
22
23    /// The concrete type of the results generated by this benchmark.
24    type Output: Serialize;
25
26    /// Return whether or not this benchmark is compatible with `input`.
27    ///
28    /// Use [`MatchContext::success`] to create an initial [`Score`], then progressively
29    /// refine it with [`Score::penalize`] and [`Score::fail`].
30    ///
31    /// Among successful matches, the benchmark with the lowest score wins. Ties are broken
32    /// by an unspecified procedure.
33    ///
34    /// When no successful match exists, failure scores rank the "nearest misses".
35    /// Implementations should use [`Score::fail`] with descriptive reasons to aid
36    /// diagnostics.
37    fn try_match(&self, input: &Self::Input, context: &MatchContext) -> Score;
38
39    /// Return descriptive information about the benchmark.
40    fn description(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result;
41
42    /// Run the benchmark with `input`.
43    ///
44    /// All prints should be directed to `output`. The `checkpoint` is provided so
45    /// long-running benchmarks can periodically save output to prevent data loss due to
46    /// an early error.
47    ///
48    /// Implementors may assume that [`Self::try_match`] returned a successful [`Score`]
49    /// for `input`.
50    fn run(
51        &self,
52        input: &Self::Input,
53        checkpoint: Checkpoint<'_>,
54        output: &mut dyn Output,
55    ) -> anyhow::Result<Self::Output>;
56}
57
58/// A refinement of [`Benchmark`], that supports before/after comparison of generated results.
59///
60/// Benchmarks are associated with a "tolerance" input, which may contain runtime values
61/// controlling the amount of slack a benchmark is allowed to have between runs before failing.
62///
63/// The semantics of pass or failure are left solely to the discretion of the [`Regression`]
64/// implementation.
65///
66/// See: [`register_regression`](crate::Registry::register_regression).
67pub trait Regression: Benchmark<Output: for<'a> Deserialize<'a>> {
68    /// The tolerance [`Input`] associated with this regression check.
69    type Tolerances: Input + 'static;
70
71    /// The report summary used to describe a successful regression check.
72    type Pass: Serialize + std::fmt::Display + 'static;
73
74    /// The report summary used to describe an unsuccessful regression check.
75    type Fail: Serialize + std::fmt::Display + 'static;
76
77    /// Run any regression checks necessary for two benchmark runs `before` and `after`.
78    /// Argument `tolerances` contain any tuned runtime tolerances to use when determining
79    /// whether or not a regression is detected.
80    ///
81    /// The `input` is the raw input that would have been provided to [`Benchmark::run`]
82    /// when generating the `before` and `after` outputs.
83    ///
84    /// Implementations of `check` should not attempt to print to `stdout` or any other
85    /// stream. Instead, all diagnostics should be encoded in the returned [`PassFail`] type
86    /// for reporting upstream.
87    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/// Describe whether or not a [`Regression`] passed or failed.
97#[derive(Debug, Clone, Copy)]
98pub enum PassFail<P, F> {
99    Pass(P),
100    Fail(F),
101}
102
103//////////////
104// Matching //
105//////////////
106
107/// Context for [`Benchmark::try_match`].
108///
109/// This is used to create matching [`Score`]s via [`Self::success`] and [`Self::fail`].
110/// Users can test their [`Benchmark::try_match`] implementations using [`Self::test`].
111///
112/// Internally, the [`MatchContext`] decides whether or not failure reasons are evaluated,
113/// eliding formatting in situations where the results will not be used.
114#[derive(Debug)]
115pub struct MatchContext {
116    record_failure_reasons: bool,
117}
118
119impl MatchContext {
120    /// Create a new [`Score`] configured for "success" with the given `score`.
121    ///
122    /// Lower scores indicate better successful matches.
123    pub fn success(&self, score: u32) -> Score {
124        Score {
125            inner: ScoreInner::Success(SuccessScore(score)),
126            context: self.hidden_clone(),
127        }
128    }
129
130    /// Create a new [`Score`] configured for "failure" with the given `score`.
131    ///
132    /// Lower scores indicate better "near misses".
133    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    /// Test a [`Benchmark::try_match`] implementation, returning a [`TestScore`] with
140    /// full failure reasons (if applicable).
141    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    /// Create a new [`MatchContext`] that does not evaluate failure reasons.
157    pub(crate) fn new() -> Self {
158        Self {
159            record_failure_reasons: false,
160        }
161    }
162
163    /// Create a new [`MatchContext`] that evaluates failure reasons.
164    pub(crate) fn with_reasons() -> Self {
165        Self {
166            record_failure_reasons: true,
167        }
168    }
169}
170
171/// The result of [`MatchContext::test`], providing low-level access to the final match
172/// scoring and failure reasons.
173#[derive(Debug)]
174pub enum TestScore {
175    Success(u32),
176    Failure {
177        score: u32,
178        reasons: Option<Vec<String>>,
179    },
180}
181
182/// A score for [`Benchmark::try_match`].
183///
184/// A [`Score`] is in one of two states: success or failure. Lower scores indicate better
185/// matches in both states. In the failure case, scores rank "near misses" for diagnostics.
186///
187/// - [`Score::penalize`] increases the score of a successful match.
188/// - [`Score::fail`] transitions to (or worsens) the failure state.
189///
190/// The current state can be queried with [`Score::is_success`].
191#[derive(Debug)]
192pub struct Score {
193    inner: ScoreInner,
194    context: MatchContext,
195}
196
197impl Score {
198    /// If the score is in the "success" state, penalize it by `by`.
199    ///
200    /// Has no effect if the score is already in the "failure" state.
201    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    /// Transition the score to the "failure" state with penalty `by` and a `reason`.
209    ///
210    /// If the score is already in the "failure" state, `by` is added to the existing
211    /// failure score and `reason` is appended to the list of failure reasons.
212    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    /// Return `true` if `self` is in the "success" state. Returning `false` implies the
234    /// "failure" state.
235    #[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    /// Allow `Scores` to be ordered in the following manner:
265    ///
266    /// * Successes are ordered before failures.
267    /// * Successes with lower scores are ordered before those with higher scores.
268    /// * Failures with lower scores are ordered before those with higher scores.
269    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
324//////////////
325// Internal //
326//////////////
327
328pub(crate) mod internal {
329    use super::*;
330
331    use crate::{Features, input::internal::Any};
332
333    use anyhow::Context;
334    use thiserror::Error;
335
336    #[derive(Debug)]
337    pub(crate) enum AnnotatedMatch<'a> {
338        /// User matching indicated success.
339        User(Score),
340        /// We missed because the input type was incorrect.
341        WrongTag,
342        /// We missed because the input type matches, but the benchmark is feature-gated.
343        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    /// Object-safe trait for type-erased benchmarks stored in the registry.
357    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        /// If supported, return an object capable of running regression checks on this benchmark.
372        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        /// Serialize `value` to `serde_json::Value` and box it for future display.
397        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        // Extensions
553        fn as_regression(&self) -> Option<&dyn Regression> {
554            R::as_regression(&self.benchmark)
555        }
556    }
557
558    //--------//
559    // Errors //
560    //--------//
561
562    #[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    // Gated Benchmarks
613    //
614    // We support two flavors:
615    //
616    // 1. Benchmarks that have inputs that are valid and registered.
617    // 2. Benchmarks that have inputs that are *also* feature gated.
618
619    /// A benchmark that is gated behind `features`, but whose input *is* compiled into the
620    /// application. These benchmarks allow their inputs to be inspected and parsed, but
621    /// fail matching.
622    ///
623    /// This custom type allows us to return more precise error messages when benchmark
624    /// matching fails.
625    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    /// A benchmark that is gated behind `features` along with its input.
679    ///
680    /// These benchmarks are completely unreachable as the front-end has no way to parse
681    /// inputs associated with `tag`.
682    ///
683    /// However, they are included to provide a bread-crumb for users that additional
684    /// benchmarks *do* exist behind the associated features.
685    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///////////
733// Tests //
734///////////
735
736#[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        // Ensure that we saturate properly.
781        score.penalize(u32::MAX);
782        assert!(score.is_success());
783        assert_eq!(as_raw(&score), RawScore::success(u32::MAX));
784
785        // Switch to failure
786        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        // Calling `penalize` should have no effect.
795        score.penalize(5);
796        assert!(!score.is_success());
797        assert_eq!(as_raw(&score), RawScore::failure(15));
798
799        // Since we aren't recording reasons - nothing should be returned.
800        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        // Ensure that we saturate properly.
820        score.penalize(u32::MAX);
821        assert!(score.is_success());
822        assert_eq!(as_raw(&score), RawScore::success(u32::MAX));
823
824        // Switch to failure
825        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        // Calling `penalize` should have no effect.
834        score.penalize(5);
835        assert!(!score.is_success());
836        assert_eq!(as_raw(&score), RawScore::failure(15));
837
838        // Reasons are recorded, so both failure reasons should be returned.
839        let expected = "- some reason that is evaluated\n\
840                        - another reason that is evaluated";
841        assert_eq!(score.reason().to_string(), expected);
842    }
843}