use serde::{Deserialize, Serialize};
use crate::{Checkpoint, Input, Output};
pub trait Benchmark: 'static {
type Input: Input + 'static;
type Output: Serialize;
fn try_match(&self, input: &Self::Input, context: &MatchContext) -> Score;
fn description(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result;
fn run(
&self,
input: &Self::Input,
checkpoint: Checkpoint<'_>,
output: &mut dyn Output,
) -> anyhow::Result<Self::Output>;
}
pub trait Regression: Benchmark<Output: for<'a> Deserialize<'a>> {
type Tolerances: Input + 'static;
type Pass: Serialize + std::fmt::Display + 'static;
type Fail: Serialize + std::fmt::Display + 'static;
fn check(
&self,
tolerances: &Self::Tolerances,
input: &Self::Input,
before: &Self::Output,
after: &Self::Output,
) -> anyhow::Result<PassFail<Self::Pass, Self::Fail>>;
}
#[derive(Debug, Clone, Copy)]
pub enum PassFail<P, F> {
Pass(P),
Fail(F),
}
#[derive(Debug)]
pub struct MatchContext {
record_failure_reasons: bool,
}
impl MatchContext {
pub fn success(&self, score: u32) -> Score {
Score {
inner: ScoreInner::Success(SuccessScore(score)),
context: self.hidden_clone(),
}
}
pub fn fail(&self, score: u32, reason: &dyn std::fmt::Display) -> Score {
let mut s = self.success(u32::MAX);
s.fail(score, reason);
s
}
pub fn test<T>(benchmark: &T, input: &T::Input) -> TestScore
where
T: Benchmark,
{
benchmark
.try_match(input, &Self::with_reasons())
.into_test()
}
fn hidden_clone(&self) -> Self {
Self {
record_failure_reasons: self.record_failure_reasons,
}
}
pub(crate) fn new() -> Self {
Self {
record_failure_reasons: false,
}
}
pub(crate) fn with_reasons() -> Self {
Self {
record_failure_reasons: true,
}
}
}
#[derive(Debug)]
pub enum TestScore {
Success(u32),
Failure {
score: u32,
reasons: Option<Vec<String>>,
},
}
#[derive(Debug)]
pub struct Score {
inner: ScoreInner,
context: MatchContext,
}
impl Score {
pub fn penalize(&mut self, by: u32) {
match self.inner {
ScoreInner::Success(ref mut v) => v.0 = v.0.saturating_add(by),
ScoreInner::Failure(..) => {}
}
}
pub fn fail(&mut self, by: u32, reason: &dyn std::fmt::Display) {
match &mut self.inner {
ScoreInner::Success(_) => {
self.inner = ScoreInner::Failure(
FailureScore(by),
self.context
.record_failure_reasons
.then(|| vec![reason.to_string()]),
);
}
ScoreInner::Failure(score, reasons) => {
score.0 = (score.0).saturating_add(by);
if self.context.record_failure_reasons {
reasons
.get_or_insert_with(|| Vec::with_capacity(1))
.push(reason.to_string())
}
}
}
}
#[must_use = "this function has no side-effects"]
pub fn is_success(&self) -> bool {
matches!(self.inner, ScoreInner::Success(_))
}
fn into_test(self) -> TestScore {
match self.inner {
ScoreInner::Success(score) => TestScore::Success(score.0),
ScoreInner::Failure(score, reasons) => TestScore::Failure {
score: score.0,
reasons,
},
}
}
pub(crate) fn match_score(&self) -> Option<SuccessScore> {
match self.inner {
ScoreInner::Success(score) => Some(score),
ScoreInner::Failure(..) => None,
}
}
pub(crate) fn as_raw(&self) -> RawScore {
match self.inner {
ScoreInner::Success(s) => RawScore::Success(s),
ScoreInner::Failure(s, _) => RawScore::Failure(s),
}
}
pub(crate) fn reason(&self) -> Reason<'_> {
match &self.inner {
ScoreInner::Success(_) => Reason::none(),
ScoreInner::Failure(_, reasons) => Reason::new(reasons.as_deref()),
}
}
}
#[derive(Debug)]
enum ScoreInner {
Success(SuccessScore),
Failure(FailureScore, Option<Vec<String>>),
}
pub(crate) struct Reason<'a>(Option<&'a [String]>);
impl<'a> Reason<'a> {
fn new(reasons: Option<&'a [String]>) -> Self {
Self(reasons)
}
fn none() -> Self {
Self(None)
}
}
impl std::fmt::Display for Reason<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.0 {
None => f.write_str("<missing>"),
Some(reasons) => {
let mut first = true;
for reason in reasons.iter() {
if !first {
writeln!(f)?;
}
write!(f, "- {}", reason)?;
first = false;
}
Ok(())
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) enum RawScore {
Success(SuccessScore),
Failure(FailureScore),
}
impl RawScore {
#[cfg(test)]
fn success(score: u32) -> Self {
Self::Success(SuccessScore(score))
}
#[cfg(test)]
fn failure(score: u32) -> Self {
Self::Failure(FailureScore(score))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) struct SuccessScore(pub(crate) u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) struct FailureScore(pub(crate) u32);
pub(crate) mod internal {
use super::*;
use crate::input::internal::Any;
use anyhow::Context;
use thiserror::Error;
pub(crate) trait Benchmark {
fn try_match(&self, input: &Any, context: &MatchContext) -> Score;
fn description(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result;
fn run(
&self,
input: &Any,
checkpoint: Checkpoint<'_>,
output: &mut dyn Output,
) -> anyhow::Result<serde_json::Value>;
fn as_regression(&self) -> Option<&dyn Regression>;
fn as_string(&self) -> String {
Description(self).to_string()
}
}
struct Description<'a, T: ?Sized>(&'a T);
impl<T> std::fmt::Display for Description<'_, T>
where
T: Benchmark + ?Sized,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.description(f)
}
}
pub(crate) struct Checked {
pub(crate) json: serde_json::Value,
pub(crate) display: Box<dyn std::fmt::Display>,
}
impl Checked {
fn new<T>(value: T) -> Result<Self, serde_json::Error>
where
T: Serialize + std::fmt::Display + 'static,
{
Ok(Self {
json: serde_json::to_value(&value)?,
display: Box::new(value),
})
}
}
pub(crate) type CheckedPassFail = PassFail<Checked, Checked>;
pub(crate) trait Regression {
fn tolerance(&self) -> &dyn crate::input::internal::DynInput;
fn input_tag(&self) -> &'static str;
fn check(
&self,
tolerances: &Any,
input: &Any,
before: &serde_json::Value,
after: &serde_json::Value,
) -> anyhow::Result<CheckedPassFail>;
}
impl std::fmt::Debug for dyn Regression + '_ {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("dyn Regression")
.field("tolerance", &self.tolerance().tag())
.field("input_tag", &self.input_tag())
.finish()
}
}
pub(crate) trait AsRegression<T> {
fn as_regression(benchmark: &T) -> Option<&dyn Regression>;
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct NoRegression;
impl<T> AsRegression<T> for NoRegression {
fn as_regression(_benchmark: &T) -> Option<&dyn Regression> {
None
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct WithRegression;
impl<T> AsRegression<T> for WithRegression
where
T: super::Regression,
{
fn as_regression(benchmark: &T) -> Option<&dyn Regression> {
Some(benchmark)
}
}
impl<T> Regression for T
where
T: super::Regression,
{
fn tolerance(&self) -> &dyn crate::input::internal::DynInput {
&crate::input::internal::Wrapper::<T::Tolerances>::INSTANCE
}
fn input_tag(&self) -> &'static str {
T::Input::tag()
}
fn check(
&self,
tolerance: &Any,
input: &Any,
before: &serde_json::Value,
after: &serde_json::Value,
) -> anyhow::Result<CheckedPassFail> {
let tolerance = tolerance
.downcast_ref::<T::Tolerances>()
.ok_or_else(|| BadDownCast::new(T::Tolerances::tag(), tolerance.tag()))
.context("failed to obtain tolerance")?;
let input = input
.downcast_ref::<T::Input>()
.ok_or_else(|| BadDownCast::new(T::Input::tag(), input.tag()))
.context("failed to obtain input")?;
let before = T::Output::deserialize(before)
.map_err(|err| DeserializationError::new(Kind::Before, err))?;
let after = T::Output::deserialize(after)
.map_err(|err| DeserializationError::new(Kind::After, err))?;
let passfail = match self.check(tolerance, input, &before, &after)? {
PassFail::Pass(pass) => PassFail::Pass(Checked::new(pass)?),
PassFail::Fail(fail) => PassFail::Fail(Checked::new(fail)?),
};
Ok(passfail)
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Wrapper<T, R = NoRegression> {
benchmark: T,
_regression: R,
}
impl<T, R> Wrapper<T, R> {
pub(crate) const fn new(benchmark: T, regression: R) -> Self {
Self {
benchmark,
_regression: regression,
}
}
}
const MATCH_FAIL: u32 = 10_000;
impl<T, R> Benchmark for Wrapper<T, R>
where
T: super::Benchmark,
R: AsRegression<T>,
{
fn try_match(&self, input: &Any, context: &MatchContext) -> Score {
if let Some(cast) = input.downcast_ref::<T::Input>() {
self.benchmark.try_match(cast, context)
} else {
struct TagMismatch<T>(&'static str, std::marker::PhantomData<T>);
impl<T> std::fmt::Display for TagMismatch<T>
where
T: super::Benchmark,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"expected tag \"{}\" - instead got \"{}\"",
T::Input::tag(),
self.0,
)
}
}
context.fail(
MATCH_FAIL,
&TagMismatch::<T>(input.tag(), std::marker::PhantomData),
)
}
}
fn description(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "tag \"{}\"", <T::Input as Input>::tag())?;
self.benchmark.description(f)
}
fn run(
&self,
input: &Any,
checkpoint: Checkpoint<'_>,
output: &mut dyn Output,
) -> anyhow::Result<serde_json::Value> {
match input.downcast_ref::<T::Input>() {
Some(input) => {
let result = self.benchmark.run(input, checkpoint, output)?;
Ok(serde_json::to_value(result)?)
}
None => Err(BadDownCast::new(T::Input::tag(), input.tag()).into()),
}
}
fn as_regression(&self) -> Option<&dyn Regression> {
R::as_regression(&self.benchmark)
}
}
#[derive(Debug, Clone, Copy, Error)]
#[error(
"INTERNAL ERROR: bad downcast - expected \"{}\" but got \"{}\"",
self.expected,
self.got
)]
struct BadDownCast {
expected: &'static str,
got: &'static str,
}
impl BadDownCast {
fn new(expected: &'static str, got: &'static str) -> Self {
Self { expected, got }
}
}
#[derive(Debug, Error)]
#[error(
"the \"{}\" results do not match the output schema expected by this benchmark",
self.kind
)]
struct DeserializationError {
kind: Kind,
source: serde_json::Error,
}
impl DeserializationError {
fn new(kind: Kind, source: serde_json::Error) -> Self {
Self { kind, source }
}
}
#[derive(Debug, Clone, Copy)]
enum Kind {
Before,
After,
}
impl std::fmt::Display for Kind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let as_str = match self {
Self::Before => "before",
Self::After => "after",
};
write!(f, "{}", as_str)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_score_no_reasons() {
let context = MatchContext::new();
let mut score = context.success(0);
assert!(score.is_success());
assert_eq!(score.as_raw(), RawScore::success(0));
score.penalize(10);
assert!(score.is_success());
assert_eq!(score.as_raw(), RawScore::success(10));
score.penalize(10);
assert!(score.is_success());
assert_eq!(score.as_raw(), RawScore::success(20));
score.penalize(u32::MAX);
assert!(score.is_success());
assert_eq!(score.as_raw(), RawScore::success(u32::MAX));
score.fail(5, &"some reason that is not evaluated");
assert!(!score.is_success());
assert_eq!(score.as_raw(), RawScore::failure(5));
score.fail(10, &"another reason that is not evaluated");
assert!(!score.is_success());
assert_eq!(score.as_raw(), RawScore::failure(15));
score.penalize(5);
assert!(!score.is_success());
assert_eq!(score.as_raw(), RawScore::failure(15));
assert_eq!(score.reason().to_string(), "<missing>");
}
#[test]
fn test_score_with_reasons() {
let context = MatchContext::with_reasons();
let mut score = context.success(0);
assert!(score.is_success());
assert_eq!(score.as_raw(), RawScore::success(0));
score.penalize(10);
assert!(score.is_success());
assert_eq!(score.as_raw(), RawScore::success(10));
score.penalize(10);
assert!(score.is_success());
assert_eq!(score.as_raw(), RawScore::success(20));
assert_eq!(score.reason().to_string(), "<missing>");
score.penalize(u32::MAX);
assert!(score.is_success());
assert_eq!(score.as_raw(), RawScore::success(u32::MAX));
score.fail(5, &"some reason that is evaluated");
assert!(!score.is_success());
assert_eq!(score.as_raw(), RawScore::failure(5));
score.fail(10, &"another reason that is evaluated");
assert!(!score.is_success());
assert_eq!(score.as_raw(), RawScore::failure(15));
score.penalize(5);
assert!(!score.is_success());
assert_eq!(score.as_raw(), RawScore::failure(15));
let expected = "- some reason that is evaluated\n\
- another reason that is evaluated";
assert_eq!(score.reason().to_string(), expected);
}
}