use serde_json::Value;
use std::fmt;
use jmespath::{Expression, Rcvar, RuntimeError, Variable, compile};
pub enum BenchType {
Parse,
Interpret,
Full,
}
impl BenchType {
fn from_json(bench_type: &Value) -> Result<Self, TestCaseError> {
bench_type
.as_str()
.ok_or(TestCaseError::BenchIsNotString)
.and_then(|b| match b {
"parse" => Ok(BenchType::Parse),
"interpret" => Ok(BenchType::Interpret),
"full" => Ok(BenchType::Full),
s => Err(TestCaseError::UnknownBenchType(s.to_string())),
})
}
}
impl fmt::Display for BenchType {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
match *self {
BenchType::Parse => write!(fmt, "parse"),
BenchType::Interpret => write!(fmt, "interpret"),
BenchType::Full => write!(fmt, "full"),
}
}
}
pub enum ErrorType {
InvalidArity,
InvalidType,
InvalidSlice,
UnknownFunction,
SyntaxError,
}
impl ErrorType {
fn from_json(error_type: &Value) -> Result<Self, TestCaseError> {
error_type
.as_str()
.ok_or(TestCaseError::ErrorIsNotString)
.and_then(|b| match b {
"syntax" => Ok(ErrorType::SyntaxError),
"invalid-type" => Ok(ErrorType::InvalidType),
"invalid-value" => Ok(ErrorType::InvalidSlice),
"invalid-arity" => Ok(ErrorType::InvalidArity),
"unknown-function" => Ok(ErrorType::UnknownFunction),
e => Err(TestCaseError::UnknownErrorType(e.to_string())),
})
}
}
impl fmt::Display for ErrorType {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
use self::ErrorType::*;
match *self {
InvalidArity => write!(fmt, "invalid-arity"),
InvalidType => write!(fmt, "invalid-type"),
InvalidSlice => write!(fmt, "invalid-value"),
UnknownFunction => write!(fmt, "unknown-function"),
SyntaxError => write!(fmt, "syntax"),
}
}
}
pub enum Assertion {
Error(ErrorType),
Bench(BenchType),
ValidResult(Rcvar),
}
impl Assertion {
pub fn assert(&self, suite: &str, case: &TestCase, given: Rcvar) -> Result<(), String> {
match self {
&Assertion::Bench(_) => Ok(()),
Assertion::ValidResult(expected_result) => {
let expr = self.try_parse(suite, case)?;
match expr.search(given) {
Err(e) => Err(self.err_message(suite, case, format!("{e}"))),
Ok(r) => {
if *r == **expected_result {
Ok(())
} else {
Err(self.err_message(
suite,
case,
format!("{:?}, {}", r, expr.as_ast()),
))
}
}
}
}
Assertion::Error(error_type) => {
use jmespath::ErrorReason::*;
let result = self.try_parse(suite, case);
match *error_type {
ErrorType::InvalidArity => match result?.search(given).map_err(|e| e.reason) {
Err(Runtime(RuntimeError::NotEnoughArguments { .. })) => Ok(()),
Err(Runtime(RuntimeError::TooManyArguments { .. })) => Ok(()),
Err(e) => Err(self.err_message(suite, case, format!("{e}"))),
Ok(r) => Err(self.err_message(suite, case, r.to_string())),
},
ErrorType::InvalidType => match result?.search(given).map_err(|e| e.reason) {
Err(Runtime(RuntimeError::InvalidType { .. })) => Ok(()),
Err(Runtime(RuntimeError::InvalidReturnType { .. })) => Ok(()),
Err(e) => Err(self.err_message(suite, case, format!("{e}"))),
Ok(r) => Err(self.err_message(suite, case, r.to_string())),
},
ErrorType::InvalidSlice => match result?.search(given).map_err(|e| e.reason) {
Err(Runtime(RuntimeError::InvalidSlice)) => Ok(()),
Err(e) => Err(self.err_message(suite, case, format!("{e}"))),
Ok(r) => Err(self.err_message(suite, case, r.to_string())),
},
ErrorType::UnknownFunction => {
match result?.search(given).map_err(|e| e.reason) {
Err(Runtime(RuntimeError::UnknownFunction(_))) => Ok(()),
Err(e) => Err(self.err_message(suite, case, format!("{e}"))),
Ok(r) => Err(self.err_message(suite, case, r.to_string())),
}
}
ErrorType::SyntaxError => match result {
Err(_) => Ok(()),
Ok(expr) => Err(self.err_message(suite, case, format!("Parsed {expr:?}"))),
},
}
}
}
}
fn try_parse(&self, suite: &str, case: &TestCase) -> Result<Expression<'_>, String> {
match compile(&case.expression) {
Err(e) => Err(self.err_message(suite, case, format!("{e}"))),
Ok(expr) => Ok(expr),
}
}
fn err_message(&self, suite: &str, case: &TestCase, message: String) -> String {
format!(
"Test suite: {}\nExpression: {}\nAssertion: {}\nResult: {}\n==============",
suite, case.expression, self, message
)
.to_string()
}
}
impl fmt::Display for Assertion {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
match self {
Assertion::Error(e) => write!(fmt, "expects error({e})"),
Assertion::Bench(b) => write!(fmt, "expects bench({b})"),
Assertion::ValidResult(r) => write!(fmt, "expects result({r:?})"),
}
}
}
#[allow(dead_code)]
pub struct TestSuite<'a> {
filename: &'a str,
given: Rcvar,
cases: Vec<TestCase>,
}
impl<'a> TestSuite<'a> {
pub fn from_str(filename: &'a str, suite: &str) -> Result<TestSuite<'a>, String> {
serde_json::from_str::<Value>(suite)
.map_err(|e| e.to_string())
.and_then(|j| TestSuite::from_json(filename, &j))
}
fn from_json(filename: &'a str, suite: &Value) -> Result<TestSuite<'a>, String> {
let suite = suite.as_object().ok_or("test suite is not an object")?;
let test_case = suite.get("cases").ok_or("No cases value".to_string())?;
let case_array = test_case
.as_array()
.ok_or("cases is not an array".to_string())?;
let mut cases = vec![];
for case in case_array {
cases.push(TestCase::from_json(case).map_err(|e| e.to_string())?);
}
let value = suite
.get("given")
.ok_or("No given value".to_string())?
.clone();
let given = serde_json::from_value::<Variable>(value).map_err(|e| format!("{e}"))?;
Ok(TestSuite {
filename,
given: Rcvar::new(given),
cases,
})
}
}
pub enum TestCaseError {
InvalidJSON(String),
NoCaseType,
NoResult,
ResultCannotToString,
NoExpression,
ExpressionIsNotString,
ErrorIsNotString,
UnknownErrorType(String),
UnknownBenchType(String),
BenchIsNotString,
}
impl fmt::Display for TestCaseError {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
use self::TestCaseError::*;
match self {
InvalidJSON(msg) => write!(fmt, "invalid test case JSON: {msg}"),
&NoCaseType => write!(fmt, "case has no result, error, or bench"),
&NoResult => write!(fmt, "test case has no result key"),
&ResultCannotToString => write!(fmt, "result could not be cast to string"),
&NoExpression => write!(fmt, "test case has no expression key"),
&ExpressionIsNotString => write!(fmt, "test case expression is not a string"),
&ErrorIsNotString => write!(fmt, "test case error value is not a string"),
UnknownErrorType(t) => write!(fmt, "unknown error type: {t}"),
&BenchIsNotString => write!(fmt, "bench value is not a string"),
UnknownBenchType(bench) => write!(
fmt,
"unknown bench value: {bench}, expected one of of parse|full"
),
}
}
}
impl fmt::Debug for TestCaseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{self}")
}
}
pub struct TestCase {
pub expression: String,
pub assertion: Assertion,
}
impl TestCase {
#[allow(clippy::should_implement_trait)]
pub fn from_str(case: &str) -> Result<TestCase, TestCaseError> {
serde_json::from_str::<Value>(case)
.map_err(|e| TestCaseError::InvalidJSON(e.to_string()))
.and_then(|json| TestCase::from_json(&json))
}
fn from_json(case: &Value) -> Result<TestCase, TestCaseError> {
use crate::TestCaseError::*;
let case = case
.as_object()
.ok_or(InvalidJSON("not an object".to_string()))?;
Ok(TestCase {
expression: case
.get("expression")
.ok_or(NoExpression)
.and_then(|expression| {
expression
.as_str()
.ok_or(ExpressionIsNotString)
.map(|expression_str| expression_str.to_string())
})?,
assertion: match case.get("error") {
Some(err) => Assertion::Error(ErrorType::from_json(err)?),
None if case.contains_key("result") => {
let value = case.get("result").unwrap();
let var = serde_json::from_value::<Variable>(value.clone()).unwrap();
Assertion::ValidResult(Rcvar::new(var))
}
None if case.contains_key("bench") => {
Assertion::Bench(BenchType::from_json(case.get("bench").unwrap())?)
}
_ => return Err(NoCaseType),
},
})
}
pub fn assert(&self, suite_filename: &str, given: Rcvar) -> Result<(), String> {
self.assertion.assert(suite_filename, self, given)
}
}
include!(concat!(env!("OUT_DIR"), "/compliance_tests.rs"));