proptesting

This commit is contained in:
Jana Dönszelmann 2026-04-01 13:59:56 +02:00
parent e3648ad8e6
commit bedaa49754
No known key found for this signature in database
4 changed files with 393 additions and 31 deletions

View file

@ -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}`");
}
}
}