Skip to content

Commit

Permalink
deer: testing framework (#1701)
Browse files Browse the repository at this point in the history
  • Loading branch information
indietyp authored Jan 6, 2023
1 parent 814d8df commit f86280a
Show file tree
Hide file tree
Showing 16 changed files with 1,081 additions and 48 deletions.
2 changes: 1 addition & 1 deletion packages/libs/deer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@ std = ['serde/std', 'error-stack/std']
arbitrary-precision = []

[workspace]
members = ['.', 'macros', 'json']
members = ['.', 'macros', 'json', 'desert']
14 changes: 14 additions & 0 deletions packages/libs/deer/desert/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "deer-desert"
version = "0.0.0"
edition = "2021"
# NOTE: THIS PACKAGE IS NEVER INTENDED TO BE PUBLISHED
publish = false

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
deer = { path = ".." }
error-stack = { version = "0.2.4", default_features = false }
serde_json = { version = "1.0.91", default_features = false, features = ['alloc'] }
bitvec = { version = "1", default_features = false, features = ['alloc', 'atomic'] }
6 changes: 6 additions & 0 deletions packages/libs/deer/desert/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# deer-desert

desert is the the internal only deserialization testing framework used throughout the integration tests and should never
be published.

`desert` = `deser` (`deserialization`) + `t` (`test`)
149 changes: 149 additions & 0 deletions packages/libs/deer/desert/src/array.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
use deer::{
error::{
ArrayAccessError, ArrayLengthError, BoundedContractViolationError, ExpectedLength,
ReceivedLength, Variant,
},
Deserialize, Deserializer as _,
};
use error_stack::{Report, Result, ResultExt};

use crate::{
deserializer::{Deserializer, DeserializerNone},
token::Token,
};

pub struct ArrayAccess<'a, 'b, 'de: 'a> {
deserializer: &'a mut Deserializer<'b, 'de>,

length: Option<usize>,
remaining: Option<usize>,
consumed: usize,
}

impl<'a, 'b, 'de> ArrayAccess<'a, 'b, 'de> {
pub fn new(deserializer: &'a mut Deserializer<'b, 'de>, length: Option<usize>) -> Self {
Self {
deserializer,
consumed: 0,
length,
remaining: None,
}
}

fn scan_end(&self) -> Option<usize> {
let mut objects: usize = 0;
let mut arrays: usize = 0;

let mut n = 0;

loop {
let token = self.deserializer.peek_n(n)?;

match token {
Token::Array { .. } => arrays += 1,
Token::ArrayEnd if arrays == 0 && objects == 0 => {
// we're at the outer layer, meaning we can know where we end
return Some(n);
}
Token::ArrayEnd => arrays = arrays.saturating_sub(1),
Token::Object { .. } => objects += 1,
Token::ObjectEnd => objects = objects.saturating_sub(1),
_ => {}
}

n += 1;
}
}
}

impl<'de> deer::ArrayAccess<'de> for ArrayAccess<'_, '_, 'de> {
fn set_bounded(&mut self, length: usize) -> Result<(), ArrayAccessError> {
if self.consumed > 0 {
return Err(
Report::new(BoundedContractViolationError::SetDirty.into_error())
.change_context(ArrayAccessError),
);
}

if self.remaining.is_some() {
return Err(Report::new(
BoundedContractViolationError::SetCalledMultipleTimes.into_error(),
)
.change_context(ArrayAccessError));
}

self.remaining = Some(length);

Ok(())
}

fn next<T>(&mut self) -> Option<Result<T, ArrayAccessError>>
where
T: Deserialize<'de>,
{
self.consumed += 1;

if self.deserializer.peek() == Token::ArrayEnd {
// we have reached the ending, if `self.remaining` is set we use the `DeserializerNone`
// to deserialize any values that require `None`
if let Some(remaining) = &mut self.remaining {
if *remaining == 0 {
return None;
}

*remaining = remaining.saturating_sub(1);

let value = T::deserialize(DeserializerNone {
context: self.deserializer.context(),
});

Some(value.change_context(ArrayAccessError))
} else {
None
}
} else {
let value = T::deserialize(&mut *self.deserializer);
Some(value.change_context(ArrayAccessError))
}
}

fn size_hint(&self) -> Option<usize> {
self.length
}

fn end(self) -> Result<(), ArrayAccessError> {
let mut result = Ok(());

// ensure that we consume the last token, if it is the wrong token error out
if self.deserializer.peek() != Token::ArrayEnd {
let mut error = Report::new(ArrayLengthError.into_error())
.attach(ExpectedLength::new(self.consumed));

if let Some(length) = self.size_hint() {
error = error.attach(ReceivedLength::new(length));
}

result = Err(error);
}

// bump until the very end, which ensures that deserialize calls after this might succeed!
let bump = self
.scan_end()
.unwrap_or_else(|| self.deserializer.tape().remaining());
self.deserializer.tape_mut().bump_n(bump);

if let Some(remaining) = self.remaining {
if remaining > 0 {
let error =
Report::new(BoundedContractViolationError::EndRemainingItems.into_error());

match &mut result {
Err(result) => result.extend_one(error),
result => *result = Err(error),
}
}
}

result.change_context(ArrayAccessError)
}
}
50 changes: 50 additions & 0 deletions packages/libs/deer/desert/src/assert.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
use core::fmt::Debug;

use deer::{error::ReportExt, Context, Deserialize};
use serde_json::to_value;

use crate::{deserializer::Deserializer, token::Token};

pub fn assert_tokens_with_context<'de, T>(expected: &T, tokens: &'de [Token], context: &Context)
where
T: Deserialize<'de> + PartialEq + Debug,
{
let mut de = Deserializer::new(tokens, context);
let received = T::deserialize(&mut de).expect("should deserialize");

if de.remaining() > 0 {
panic!("{} remaining tokens", de.remaining());
}

assert_eq!(received, *expected);
}

pub fn assert_tokens<'de, T>(value: &T, tokens: &'de [Token])
where
T: Deserialize<'de> + PartialEq + Debug,
{
assert_tokens_with_context(value, tokens, &Context::new());
}

pub fn assert_tokens_with_context_error<'de, T>(
error: &serde_json::Value,
tokens: &'de [Token],
context: &Context,
) where
T: Deserialize<'de> + Debug,
{
let mut de = Deserializer::new(tokens, context);
let received = T::deserialize(&mut de).expect_err("value of type T should fail serialization");

let received = received.export();
let received = to_value(received).expect("error should serialize");

assert_eq!(received, *error)
}

pub fn assert_tokens_error<'de, T>(error: &serde_json::Value, tokens: &'de [Token])
where
T: Deserialize<'de> + Debug,
{
assert_tokens_with_context_error::<T>(error, tokens, &Context::new());
}
145 changes: 145 additions & 0 deletions packages/libs/deer/desert/src/deserializer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
use alloc::borrow::ToOwned;
use core::ops::Range;

use deer::{error::DeserializerError, Context, Visitor};
use error_stack::{Result, ResultExt};

use crate::{array::ArrayAccess, object::ObjectAccess, tape::Tape, token::Token};

macro_rules! forward {
($($method:ident),*) => {
$(
fn $method<V>(self, visitor: V) -> Result<V::Value, DeserializerError>
where
V: Visitor<'de>,
{
self.deserialize_any(visitor)
}
)*
};
}

#[derive(Debug)]
pub struct Deserializer<'a, 'de> {
context: &'a Context,
tape: Tape<'a, 'de>,
}

impl<'a, 'de> Deserializer<'a, 'de> {
pub(crate) fn erase(&mut self, range: Range<usize>) {
self.tape.set_trivia(range);
}
}

impl<'a, 'de> deer::Deserializer<'de> for &mut Deserializer<'a, 'de> {
forward!(
deserialize_null,
deserialize_bool,
deserialize_number,
deserialize_char,
deserialize_string,
deserialize_str,
deserialize_bytes,
deserialize_bytes_buffer,
deserialize_array,
deserialize_object
);

fn context(&self) -> &Context {
self.context
}

fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, DeserializerError>
where
V: Visitor<'de>,
{
let token = self.next();

match token {
Token::Bool(value) => visitor.visit_bool(value),
Token::Number(value) => visitor.visit_number(value.clone()),
Token::Char(value) => visitor.visit_char(value),
Token::Str(value) => visitor.visit_str(value),
Token::BorrowedStr(value) => visitor.visit_borrowed_str(value),
Token::String(value) => visitor.visit_string(value.to_owned()),
Token::Bytes(value) => visitor.visit_bytes(value),
Token::BorrowedBytes(value) => visitor.visit_borrowed_bytes(value),
Token::BytesBuf(value) => visitor.visit_bytes_buffer(value.to_vec()),
Token::Array { length } => visitor.visit_array(ArrayAccess::new(self, length)),
Token::Object { length } => visitor.visit_object(ObjectAccess::new(self, length)),
_ => {
panic!("Deserializer did not expect {token}");
}
}
.change_context(DeserializerError)
}
}

impl<'a, 'de> Deserializer<'a, 'de> {
pub(crate) fn new_bare(tape: Tape<'a, 'de>, context: &'a Context) -> Self {
Self { tape, context }
}

pub fn new(tokens: &'de [Token], context: &'a Context) -> Self {
Self::new_bare(tokens.into(), context)
}

pub(crate) fn peek(&self) -> Token {
self.tape.peek().expect("should have token to deserialize")
}

pub(crate) fn peek_n(&self, n: usize) -> Option<Token> {
self.tape.peek_n(n)
}

pub(crate) fn next(&mut self) -> Token {
self.tape.next().expect("should have token to deserialize")
}

pub(crate) fn tape(&self) -> &Tape<'a, 'de> {
&self.tape
}

pub(crate) fn tape_mut(&mut self) -> &mut Tape<'a, 'de> {
&mut self.tape
}

pub fn remaining(&self) -> usize {
self.tape.remaining()
}

pub fn is_empty(&self) -> bool {
self.tape.is_empty()
}
}

#[derive(Debug)]
pub(crate) struct DeserializerNone<'a> {
pub(crate) context: &'a Context,
}

impl<'de> deer::Deserializer<'de> for DeserializerNone<'_> {
forward!(
deserialize_null,
deserialize_bool,
deserialize_number,
deserialize_char,
deserialize_string,
deserialize_str,
deserialize_bytes,
deserialize_bytes_buffer,
deserialize_array,
deserialize_object
);

fn context(&self) -> &Context {
self.context
}

fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, DeserializerError>
where
V: Visitor<'de>,
{
visitor.visit_none().change_context(DeserializerError)
}
}
16 changes: 16 additions & 0 deletions packages/libs/deer/desert/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#![no_std]

extern crate alloc;

pub(crate) mod array;
mod assert;
mod deserializer;
pub(crate) mod object;
pub(crate) mod tape;
mod token;

pub use assert::{
assert_tokens, assert_tokens_error, assert_tokens_with_context,
assert_tokens_with_context_error,
};
pub use token::Token;
Loading

0 comments on commit f86280a

Please sign in to comment.