use proptest::prelude::*;
use rstest::rstest;
use super::*;
fn footnote_label_part_strategy() -> impl Strategy<Value = String> {
prop::collection::vec(
prop_oneof![
(b'a'..=b'z').prop_map(char::from),
(b'A'..=b'Z').prop_map(char::from),
(b'0'..=b'9').prop_map(char::from),
Just('-'),
Just('_')
],
0..12,
)
.prop_map(|chars| chars.into_iter().collect())
}
fn link_text_safe_fragment() -> impl Strategy<Value = String> {
prop_oneof![
prop_oneof![
(b'a'..=b'z').prop_map(char::from),
Just('['),
Just('('),
Just(')'),
Just(' '),
]
.prop_map(|ch| ch.to_string()),
prop_oneof![Just(']'), Just('['), Just('\\'), Just('('), Just('a')]
.prop_map(|escaped| format!("\\{escaped}")),
]
}
fn link_text_safe_inner() -> impl Strategy<Value = String> {
prop::collection::vec(link_text_safe_fragment(), 0..8).prop_map(|parts| parts.concat())
}
fn url_fragment() -> impl Strategy<Value = String> {
let leaf = prop_oneof![
(b'a'..=b'z').prop_map(|byte| char::from(byte).to_string()),
prop_oneof![Just('('), Just(')'), Just('\\'), Just('a')]
.prop_map(|escaped| format!("\\{escaped}")),
];
leaf.prop_recursive(4, 24, 4, |fragment| {
prop::collection::vec(fragment, 0..4).prop_map(|parts| format!("({})", parts.concat()))
})
}
fn balanced_url_inner() -> impl Strategy<Value = String> {
prop::collection::vec(url_fragment(), 0..5).prop_map(|parts| parts.concat())
}
#[test]
fn parse_link_or_image_handles_nested_parentheses() {
let text = "c)) more";
let (token, idx) = parse_link_or_image(text, 0);
assert_eq!(token, "c))");
assert_eq!(idx, token.len());
}
#[test]
fn parse_link_or_image_falls_back_on_malformed_input() {
let text = "[broken";
let (token, idx) = parse_link_or_image(text, 0);
assert_eq!(token, "[");
assert_eq!(idx, "[".len());
}
#[test]
fn parse_link_or_image_handles_deeply_nested_parentheses() {
let text = "[link](url(a(b(c)d)e)) tail";
let (token, idx) = parse_link_or_image(text, 0);
assert_eq!(token, "[link](url(a(b(c)d)e))");
assert_eq!(idx, token.len());
}
#[test]
fn parse_link_or_image_handles_nested_parentheses_for_images() {
let text = "d)e))";
let (token, idx) = parse_link_or_image(text, 0);
assert_eq!(token, "d)e))");
assert_eq!(idx, token.len());
}
#[test]
fn parse_link_or_image_handles_text_ending_at_bracket() {
let text = "[";
let (token, idx) = parse_link_or_image(text, 0);
assert_eq!(token, "[");
assert_eq!(idx, 1);
}
#[test]
fn parse_link_or_image_preserves_footnote_reference() {
let text = "[^4] tail";
let (token, idx) = parse_link_or_image(text, 0);
assert_eq!(token, "[^4]");
assert_eq!(idx, token.len());
}
#[test]
fn parse_link_or_image_preserves_footnote_reference_with_escaped_bracket() {
let text = r"[^a\]b] tail";
let (token, idx) = parse_link_or_image(text, 0);
assert_eq!(token, r"[^a\]b]");
assert_eq!(idx, token.len());
}
#[test]
fn parse_link_or_image_preserves_footnote_at_end() {
let text = "[^4]";
let (token, idx) = parse_link_or_image(text, 0);
assert_eq!(token, "[^4]");
assert_eq!(idx, token.len());
}
#[test]
fn parse_link_or_image_keeps_caret_text_links_as_links() {
let text = "[^label](https://example.com) tail";
let (token, idx) = parse_link_or_image(text, 0);
assert_eq!(token, "[^label](https://example.com)");
assert_eq!(idx, token.len());
}
#[rstest]
fn parse_link_or_image_preserves_reference_style_link() {
let input = "[trybuild][implicit-fixture-trybuild]";
assert_eq!(
parse_link_or_image(input, 0),
(input.to_string(), input.len())
);
}
proptest! {
#[test]
fn parse_link_or_image_preserves_footnote_references_with_escaped_brackets(
prefix in footnote_label_part_strategy(),
suffix in footnote_label_part_strategy(),
) {
let expected = format!(r"[^{prefix}\]{suffix}]");
let expected_len = expected.len();
let text = format!("{expected} tail");
let (token, idx) = parse_link_or_image(&text, 0);
prop_assert_eq!(token, expected);
prop_assert_eq!(idx, expected_len);
}
#[test]
fn parse_link_text_terminates_at_first_unescaped_bracket(
inner in link_text_safe_inner(),
tail in r"[a-z\]]{0,5}",
) {
let text = format!("[{inner}]{tail}");
let expected_end = format!("[{inner}]").len();
prop_assert_eq!(parse_link_text(&text, 0), Some(expected_end));
}
#[test]
fn parse_link_text_respects_backslash_parity(run in 0usize..8) {
let text = format!("[{}]", "\\".repeat(run));
let result = parse_link_text(&text, 0);
if run % 2 == 0 {
prop_assert_eq!(result, Some(text.len()));
} else {
prop_assert_eq!(result, None);
}
}
#[test]
fn parse_link_url_closes_on_balanced_destination(
inner in balanced_url_inner(),
tail in "[a-z ]{0,5}",
) {
let text = format!("({inner}){tail}");
let expected_end = format!("({inner})").len();
prop_assert_eq!(parse_link_url(&text, 0), Some(expected_end));
}
#[test]
fn parse_link_url_respects_backslash_parity(run in 0usize..8) {
let text = format!("({})", "\\".repeat(run));
let result = parse_link_url(&text, 0);
if run % 2 == 0 {
prop_assert_eq!(result, Some(text.len()));
} else {
prop_assert_eq!(result, None);
}
}
}
mod tracing_tests {
use test_macros::traced_test;
use super::*;
#[traced_test]
#[test]
fn parse_link_or_image_logs_footnote_reference() {
let _ = parse_link_or_image("[^4] tail", 0);
assert!(logs_contain("footnote reference parsed"));
assert!(logs_contain("token_length=4"));
assert!(!logs_contain("[^4]"));
}
#[traced_test]
#[test]
fn parse_link_or_image_logs_link_parsed() {
let _ = parse_link_or_image("[link](url)", 0);
assert!(logs_contain("link or image parsed"));
assert!(logs_contain("token_length=11"));
assert!(!logs_contain("[link](url)"));
assert!(logs_contain("is_image="));
}
#[traced_test]
#[test]
fn find_footnote_end_logs_prefix_mismatch() {
let _ = find_footnote_end("no-caret", 0);
assert!(logs_contain("footnote end not found"));
assert!(logs_contain("reason="));
assert!(logs_contain("prefix_mismatch"));
}
#[traced_test]
#[test]
fn parse_link_or_image_logs_footnote_label_span() {
let _ = parse_link_or_image("[^4] tail", 0);
assert!(logs_contain("footnote label span recognized"));
assert!(logs_contain("start="));
assert!(logs_contain("end="));
assert!(logs_contain("token_length=4"));
assert!(!logs_contain("[^4]"));
}
#[traced_test]
#[test]
fn find_footnote_end_logs_unterminated_bracket() {
let _ = find_footnote_end("[^unterminated", 0);
assert!(logs_contain("footnote end not found"));
assert!(logs_contain("reason="));
assert!(logs_contain("unterminated_bracket"));
}
}