use proptest::prelude::*;
use super::{TagItem, parse_tag, parse_tag_spanned};
fn fragment() -> impl Strategy<Value = &'static str> {
prop::sample::select(vec![
"foo", "bar-1", "0", "$foo", "@foo", ".cls", "#id", "primary", "f(", ")", "(", "[", "]",
"{", "}", ",", ":", "=", ".", "/", "-", "\"", "\"a\"", "\"\\", "\\n", "1", "-1.5", "1a",
"true", "false", "null", "$$mdtype", " ", " ", "\n", "\t", "\r", "é", "\u{0}",
])
}
fn tag_shaped() -> impl Strategy<Value = String> {
prop::collection::vec(fragment(), 0..32).prop_map(|parts| parts.concat())
}
fn identifier() -> impl Strategy<Value = &'static str> {
prop::sample::select(vec!["foo", "bar", "type", "a", "b-1", "title"])
}
fn literal_value() -> impl Strategy<Value = &'static str> {
prop::sample::select(vec![
"1",
"-1.5",
"0",
"true",
"false",
"null",
"\"text\"",
"\"a b\"",
"\"a \\\"q\\\" b\"",
"\"héllo wörld\"",
"[1, 2]",
"{a: 1}",
"$foo",
"$foo.bar",
])
}
fn item() -> impl Strategy<Value = String> {
prop_oneof![
identifier().prop_map(|name| format!("#{name}")),
identifier().prop_map(|name| format!(".{name}")),
(identifier(), literal_value()).prop_map(|(name, value)| format!("{name}={value}")),
]
}
fn valid_tag_body() -> impl Strategy<Value = String> {
(
prop::option::of(identifier()),
prop::collection::vec(item(), 1..6),
any::<bool>(),
)
.prop_map(|(name, items, self_closing)| {
let mut body = String::new();
if let Some(name) = name {
body.push_str(name);
body.push(' ');
}
body.push_str(&items.join(" "));
if name.is_some() && self_closing {
body.push_str(" /");
}
body
})
}
fn check_error_invariants(input: &str) {
let Err(error) = parse_tag(input) else {
return;
};
assert!(
error.start() <= error.end(),
"start {} is past end {} for {input:?}",
error.start(),
error.end()
);
assert!(
error.end() <= input.len(),
"end {} is past the input length {} for {input:?}",
error.end(),
input.len()
);
assert!(
input.get(error.start()..error.end()).is_some(),
"offsets {}..{} do not land on character boundaries for {input:?}",
error.start(),
error.end()
);
assert!(
!error.message().is_empty(),
"an error with no message for {input:?}"
);
}
fn check_span_invariants(input: &str) {
let Ok((item, spans)) = parse_tag_spanned(input) else {
return;
};
let attributes = match &item {
TagItem::Annotation { attributes } | TagItem::TagOpen { attributes, .. } => {
attributes.len()
}
_ => 0,
};
assert_eq!(
attributes,
spans.len(),
"one span per attribute for {input:?}"
);
for span in &spans {
assert!(
span.all.start <= span.all.end,
"span {:?} is reversed for {input:?}",
span.all
);
assert!(
input.get(span.all.clone()).is_some(),
"span {:?} does not land on character boundaries for {input:?}",
span.all
);
let Some(value) = span.value.clone() else {
continue;
};
assert!(
input.get(value.clone()).is_some(),
"value span {value:?} does not land on character boundaries for {input:?}"
);
assert!(
span.all.start <= value.start && value.end <= span.all.end,
"value span {value:?} escapes its item {:?} for {input:?}",
span.all
);
}
}
proptest! {
#[test]
fn never_panics_on_arbitrary_input(input in any::<String>()) {
let _ = parse_tag(&input);
}
#[test]
fn never_panics_on_tag_shaped_input(input in tag_shaped()) {
let _ = parse_tag(&input);
}
#[test]
fn errors_carry_sliceable_offsets(input in any::<String>()) {
check_error_invariants(&input);
}
#[test]
fn errors_carry_sliceable_offsets_for_tag_shaped_input(input in tag_shaped()) {
check_error_invariants(&input);
}
#[test]
fn spans_carry_sliceable_offsets(input in any::<String>()) {
check_span_invariants(&input);
}
#[test]
fn spans_carry_sliceable_offsets_for_tag_shaped_input(input in tag_shaped()) {
check_span_invariants(&input);
}
#[test]
fn spans_of_a_body_that_parses_are_sliceable(input in valid_tag_body()) {
prop_assert!(
parse_tag_spanned(&input).is_ok(),
"the generator produced a body the grammar rejects: {input:?}"
);
check_span_invariants(&input);
}
#[test]
fn parsing_is_deterministic(input in tag_shaped()) {
let first = parse_tag(&input);
let second = parse_tag(&input);
match (first, second) {
(Ok(first), Ok(second)) => prop_assert_eq!(first, second),
(Err(first), Err(second)) => {
prop_assert_eq!(first.message(), second.message());
prop_assert_eq!(first.start(), second.start());
}
_ => prop_assert!(false, "one run parsed and the other did not"),
}
}
#[test]
fn deep_nesting_returns_rather_than_overflowing(depth in 1usize..2_000) {
let input = format!("a={}{}", "[".repeat(depth), "]".repeat(depth));
let _ = parse_tag(&input);
let unbalanced = format!("a={}", "{".repeat(depth));
let _ = parse_tag(&unbalanced);
}
}