proptesting
This commit is contained in:
parent
e3648ad8e6
commit
bedaa49754
4 changed files with 393 additions and 31 deletions
|
|
@ -3,15 +3,13 @@ use std::{
|
|||
fmt::{self, Display},
|
||||
};
|
||||
|
||||
use winnow::{
|
||||
Parser,
|
||||
ascii::dec_uint,
|
||||
combinator::impls::ByRef,
|
||||
error::{FromExternalError, ParserError},
|
||||
stream,
|
||||
};
|
||||
#[cfg(test)]
|
||||
use proptest::prelude::*;
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
use proptest_derive::Arbitrary;
|
||||
use winnow::{Parser, error::ParserError};
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub enum Separator {
|
||||
Eq,
|
||||
Colon,
|
||||
|
|
@ -38,7 +36,7 @@ impl Display for Separator {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
#[derive(Copy, Clone, Debug, Arbitrary, PartialEq)]
|
||||
pub enum QuoteType {
|
||||
Single,
|
||||
Double,
|
||||
|
|
@ -68,7 +66,7 @@ impl Display for QuoteType {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Arbitrary, PartialEq)]
|
||||
pub enum Delimiter {
|
||||
Paren,
|
||||
Bracket,
|
||||
|
|
@ -96,7 +94,7 @@ impl Delimiter {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct AnyString<'a> {
|
||||
prefix: Cow<'a, str>,
|
||||
ty: QuoteType,
|
||||
|
|
@ -105,6 +103,27 @@ pub struct AnyString<'a> {
|
|||
suffix: Cow<'a, str>,
|
||||
}
|
||||
|
||||
impl AnyString<'static> {
|
||||
#[cfg(test)]
|
||||
fn arb() -> impl Strategy<Value = Self> {
|
||||
let prefix = "\\w*";
|
||||
let ty = any::<QuoteType>();
|
||||
let contents = "\\w*";
|
||||
let num_hashtags = 0usize..3;
|
||||
let suffix = "\\w*";
|
||||
|
||||
(prefix, ty, contents, num_hashtags, suffix).prop_map(
|
||||
|(prefix, ty, contents, num_hashtags, suffix)| Self {
|
||||
prefix: prefix.into(),
|
||||
ty,
|
||||
contents: contents.into(),
|
||||
num_hashtags,
|
||||
suffix: suffix.into(),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> AnyString<'a> {
|
||||
fn parse<E: ParserError<&'a str>>() -> impl Parser<&'a str, Self, E> {
|
||||
use winnow::{combinator::*, prelude::*, token::*};
|
||||
|
|
@ -162,9 +181,16 @@ impl<'a> Display for AnyString<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Space<'a>(Cow<'a, str>);
|
||||
|
||||
impl Space<'static> {
|
||||
#[cfg(test)]
|
||||
fn arb() -> impl Strategy<Value = Self> {
|
||||
" *".prop_map(|spaces| Self(spaces.into()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Space<'a> {
|
||||
fn parse<E: ParserError<&'a str>>() -> impl Parser<&'a str, Self, E> {
|
||||
use winnow::{prelude::*, token::*};
|
||||
|
|
@ -179,7 +205,7 @@ impl<'a> Display for Space<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Arbitrary)]
|
||||
pub enum PathSep {
|
||||
Slash,
|
||||
Backslash,
|
||||
|
|
@ -202,12 +228,22 @@ impl Display for PathSep {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct PathSegment<'a> {
|
||||
leading_separator: PathSep,
|
||||
segment: Cow<'a, str>,
|
||||
}
|
||||
|
||||
impl PathSegment<'static> {
|
||||
#[cfg(test)]
|
||||
fn arb() -> impl Strategy<Value = Self> {
|
||||
(any::<PathSep>(), "\\w*").prop_map(|(leading_separator, segment)| Self {
|
||||
leading_separator,
|
||||
segment: segment.into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Display for PathSegment<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.leading_separator)?;
|
||||
|
|
@ -215,12 +251,23 @@ impl<'a> Display for PathSegment<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct FileLocation<'a> {
|
||||
line: Cow<'a, str>,
|
||||
offset: Option<Cow<'a, str>>,
|
||||
}
|
||||
|
||||
impl FileLocation<'static> {
|
||||
#[cfg(test)]
|
||||
fn arb() -> impl Strategy<Value = Self> {
|
||||
use proptest::option::*;
|
||||
("[0-9]{0,4}", of("[0-9]{0,4}")).prop_map(|(line, offset)| Self {
|
||||
line: line.into(),
|
||||
offset: offset.map(Into::into),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Display for FileLocation<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.line)?;
|
||||
|
|
@ -231,7 +278,7 @@ impl<'a> Display for FileLocation<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct FileName<'a> {
|
||||
leading_separator: PathSep,
|
||||
segment: Cow<'a, str>,
|
||||
|
|
@ -239,6 +286,27 @@ pub struct FileName<'a> {
|
|||
location: Option<FileLocation<'a>>,
|
||||
}
|
||||
|
||||
impl FileName<'static> {
|
||||
#[cfg(test)]
|
||||
fn arb() -> impl Strategy<Value = Self> {
|
||||
use proptest::option::*;
|
||||
(
|
||||
any::<PathSep>(),
|
||||
"\\w*",
|
||||
of(".{0,3}"),
|
||||
of(FileLocation::arb()),
|
||||
)
|
||||
.prop_map(
|
||||
|(leading_separator, segment, ext_excluding_dot, location)| Self {
|
||||
leading_separator,
|
||||
segment: segment.into(),
|
||||
ext_excluding_dot: ext_excluding_dot.map(Into::into),
|
||||
location,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> FileName<'a> {
|
||||
fn parse(segment: PathSegment<'a>) -> Self {
|
||||
fn rsplit<'a>(
|
||||
|
|
@ -308,13 +376,30 @@ impl<'a> Display for FileName<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Path<'a> {
|
||||
drive_excluding_colon: Option<char>,
|
||||
segments: Vec<PathSegment<'a>>,
|
||||
filename: FileName<'a>,
|
||||
}
|
||||
|
||||
impl Path<'static> {
|
||||
#[cfg(test)]
|
||||
fn arb() -> impl Strategy<Value = Self> {
|
||||
use proptest::{char::*, collection::*, option::*};
|
||||
(
|
||||
of(range('A', 'Z')),
|
||||
vec(PathSegment::arb(), 0..3),
|
||||
FileName::arb(),
|
||||
)
|
||||
.prop_map(|(drive_excluding_colon, segments, filename)| Self {
|
||||
drive_excluding_colon,
|
||||
segments,
|
||||
filename,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Path<'a> {
|
||||
fn parse<E: ParserError<&'a str>>() -> impl Parser<&'a str, Self, E> {
|
||||
use winnow::{combinator::*, prelude::*, token::*};
|
||||
|
|
@ -374,9 +459,19 @@ impl<'a> Display for Path<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Number<'a>(Cow<'a, str>);
|
||||
|
||||
impl Number<'static> {
|
||||
#[cfg(test)]
|
||||
fn arb() -> impl Strategy<Value = Self> {
|
||||
prop_oneof![
|
||||
any::<i64>().prop_map(|number| Self(number.to_string().into())),
|
||||
any::<f64>().prop_map(|number| Self(number.to_string().into()))
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Number<'a> {
|
||||
fn parse<E: ParserError<&'a str>>() -> impl Parser<&'a str, Self, E> {
|
||||
use winnow::{ascii::*, combinator::*, prelude::*};
|
||||
|
|
@ -393,7 +488,7 @@ impl<'a> Display for Number<'a> {
|
|||
|
||||
/// Anything that doesn't contain spaces, and that can be a prefix of `Delimited`.
|
||||
/// i.e. an english word, or rust `::`-separated Path
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Atom<'a> {
|
||||
Text(Cow<'a, str>),
|
||||
}
|
||||
|
|
@ -406,6 +501,13 @@ impl<'a> Display for Atom<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
impl Atom<'static> {
|
||||
#[cfg(test)]
|
||||
fn arb() -> impl Strategy<Value = Self> {
|
||||
"\\w*".prop_map(|i| Self::Text(i.into()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Atom<'a> {
|
||||
fn parse<E: ParserError<&'a str>>() -> impl Parser<&'a str, Self, E> {
|
||||
use winnow::{combinator::*, prelude::*, token::*};
|
||||
|
|
@ -414,7 +516,7 @@ impl<'a> Atom<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Token<'a> {
|
||||
True,
|
||||
False,
|
||||
|
|
@ -436,10 +538,32 @@ pub enum Token<'a> {
|
|||
Atom(Atom<'a>),
|
||||
}
|
||||
|
||||
impl Token<'static> {
|
||||
#[cfg(test)]
|
||||
fn arb() -> impl Strategy<Value = Self> {
|
||||
let leaf = prop_oneof![
|
||||
Just(Self::True),
|
||||
Just(Self::False),
|
||||
Just(Self::None),
|
||||
Path::arb().prop_map(Self::Path),
|
||||
AnyString::arb().prop_map(Self::String),
|
||||
Number::arb().prop_map(Self::Number),
|
||||
Atom::arb().prop_map(Self::Atom),
|
||||
];
|
||||
|
||||
leaf.prop_recursive(4, 64, 16, |token| {
|
||||
Delimited::arb(token).prop_map(Self::Delimited).boxed()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Token<'a> {
|
||||
fn parse<E: ParserError<&'a str>>() -> impl Parser<&'a str, Self, E> {
|
||||
fn parse<E: ParserError<&'a str> + 'a>() -> impl Parser<&'a str, Self, E> {
|
||||
use winnow::{combinator::*, prelude::*};
|
||||
|
||||
let delimited: Box<dyn Parser<&'a str, Self, E>> =
|
||||
Box::new(Delimited::parse().map(Self::Delimited));
|
||||
|
||||
alt((
|
||||
"true".value(Self::True),
|
||||
"false".value(Self::False),
|
||||
|
|
@ -447,6 +571,8 @@ impl<'a> Token<'a> {
|
|||
Path::parse().map(Self::Path),
|
||||
AnyString::parse().map(Self::String),
|
||||
Number::parse().map(Self::Number),
|
||||
delimited,
|
||||
Atom::parse().map(Self::Atom),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
@ -472,15 +598,28 @@ impl<'a> Display for Token<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Delimited<'a> {
|
||||
prefix: Atom<'a>,
|
||||
delimiter: Delimiter,
|
||||
contents: Segments<'a>,
|
||||
}
|
||||
|
||||
impl Delimited<'static> {
|
||||
#[cfg(test)]
|
||||
fn arb(token: impl Strategy<Value = Token<'static>>) -> impl Strategy<Value = Self> {
|
||||
(Atom::arb(), any::<Delimiter>(), Segments::arb(token)).prop_map(
|
||||
|(prefix, delimiter, contents)| Self {
|
||||
prefix,
|
||||
delimiter,
|
||||
contents,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Delimited<'a> {
|
||||
fn parse<E: ParserError<&'a str>>() -> impl Parser<&'a str, Self, E> {
|
||||
fn parse<E: ParserError<&'a str> + 'a>() -> impl Parser<&'a str, Self, E> {
|
||||
use winnow::{combinator::*, prelude::*, token::*};
|
||||
|
||||
(
|
||||
|
|
@ -509,14 +648,24 @@ impl<'a> Display for Delimited<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Segment<'a> {
|
||||
leading_space: Space<'a>,
|
||||
token: Token<'a>,
|
||||
}
|
||||
|
||||
impl Segment<'static> {
|
||||
#[cfg(test)]
|
||||
fn arb(token: impl Strategy<Value = Token<'static>>) -> impl Strategy<Value = Self> {
|
||||
(Space::arb(), token).prop_map(|(leading_space, token)| Self {
|
||||
leading_space,
|
||||
token,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Segment<'a> {
|
||||
fn parse<E: ParserError<&'a str>>() -> impl Parser<&'a str, Self, E> {
|
||||
fn parse<E: ParserError<&'a str> + 'a>() -> impl Parser<&'a str, Self, E> {
|
||||
use winnow::prelude::*;
|
||||
|
||||
(Space::parse(), Token::parse()).map(|(leading_space, token)| Self {
|
||||
|
|
@ -533,14 +682,28 @@ impl<'a> Display for Segment<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Segments<'a> {
|
||||
segments: Vec<Segment<'a>>,
|
||||
trailing_space: Space<'a>,
|
||||
}
|
||||
|
||||
impl Segments<'static> {
|
||||
#[cfg(test)]
|
||||
fn arb(token: impl Strategy<Value = Token<'static>>) -> impl Strategy<Value = Self> {
|
||||
use proptest::collection::*;
|
||||
|
||||
(vec(Segment::arb(token), 1..10), Space::arb()).prop_map(|(segments, trailing_space)| {
|
||||
Self {
|
||||
segments,
|
||||
trailing_space,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Segments<'a> {
|
||||
fn parse<E: ParserError<&'a str>, End: 'a>(
|
||||
fn parse<E: ParserError<&'a str> + 'a, End: 'a>(
|
||||
end: impl Parser<&'a str, End, E>,
|
||||
) -> impl Parser<&'a str, (Self, End), E> {
|
||||
use winnow::{combinator::*, prelude::*};
|
||||
|
|
@ -567,3 +730,27 @@ impl<'a> Display for Segments<'a> {
|
|||
write!(f, "{}", self.trailing_space)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_input<'a>(i: &'a str) -> Result<Segments<'a>, String> {
|
||||
use winnow::combinator::eof;
|
||||
Segments::parse(eof::<&str, winnow::error::EmptyError>)
|
||||
.map(|(segments, _)| segments)
|
||||
.parse(i)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use proptest::proptest;
|
||||
|
||||
use crate::format_debug_output::{Segments, Token, parse_input};
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn test_some_function(original in Segments::arb(Token::arb())) {
|
||||
let stringified = original.to_string();
|
||||
let parsed = parse_input(&stringified).unwrap();
|
||||
assert_eq!(parsed, original, "left:\n `{parsed}`\nright: `{stringified}`");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue