use proptest::prelude::*;
use crate::wrap::{
BlockKind,
classify_block,
link_reference::{LinkReferenceMatcher, LinkTitleWindow, LinkTitleWindowOutcome},
};
fn arb_label() -> impl Strategy<Value = String> { "[A-Za-z0-9 _-]{1,40}".prop_map(|s| s) }
fn arb_bare_url() -> impl Strategy<Value = String> {
"[a-z]{3,8}://[A-Za-z0-9./-]{1,40}".prop_map(|s| s)
}
fn arb_angle_url() -> impl Strategy<Value = String> {
arb_bare_url().prop_map(|url| format!("<{url}>"))
}
fn arb_dq_title() -> impl Strategy<Value = String> {
"[A-Za-z0-9 ]{0,30}".prop_map(|s| format!("\"{s}\""))
}
fn arb_sq_title() -> impl Strategy<Value = String> {
"[A-Za-z0-9 ]{0,30}".prop_map(|s| format!("'{s}'"))
}
fn arb_paren_title() -> impl Strategy<Value = String> {
"[A-Za-z0-9 ]{0,30}".prop_map(|s| format!("({s})"))
}
fn arb_title_form() -> impl Strategy<Value = String> {
prop_oneof![arb_dq_title(), arb_sq_title(), arb_paren_title()]
}
fn arb_escaped_title_body() -> impl Strategy<Value = String> {
prop_oneof![
Just("\\\"".to_string()),
Just("\\\\".to_string()),
"[A-Za-z0-9 ]{0,10}".prop_map(|s| format!("{s}\\\"tail")),
]
}
fn arb_escaped_dq_title() -> impl Strategy<Value = String> {
arb_escaped_title_body().prop_map(|body| format!("\"{body}\""))
}
proptest! {
#[test]
fn valid_bare_link_ref_matches(label in arb_label(), url in arb_bare_url()) {
let matcher = LinkReferenceMatcher::production();
let line = format!("[{label}]: {url}");
prop_assert!(matcher.is_definition(&line), "bare link ref should match: {line}");
prop_assert!(
matcher.standalone_title_need(&line) == Some(true),
"bare link ref should need title: {line}"
);
}
#[test]
fn angle_bracket_url_matches(label in arb_label(), url in arb_angle_url()) {
let matcher = LinkReferenceMatcher::production();
let line = format!("[{label}]: {url}");
prop_assert!(matcher.is_definition(&line), "angle URL link ref should match: {line}");
prop_assert_eq!(matcher.standalone_title_need(&line), Some(true));
}
#[test]
fn definition_indentation_bounds(
label in arb_label(),
url in arb_bare_url(),
indent in 0usize..=6usize,
) {
let matcher = LinkReferenceMatcher::production();
let line = format!("{:indent$}[{label}]: {url}", "", indent = indent);
let expected = if indent < 4 {
Some(BlockKind::LinkReferenceDefinition)
} else {
None
};
prop_assert!(
classify_block(&line, matcher) == expected,
"indent {indent} mismatch for: {line}"
);
}
#[test]
fn valid_inline_title_link_ref_matches(
label in arb_label(),
url in arb_bare_url(),
title in arb_dq_title(),
) {
let matcher = LinkReferenceMatcher::production();
let line = format!("[{label}]: {url} {title}");
prop_assert!(matcher.is_definition(&line), "inline-title link ref should match: {line}");
prop_assert!(
matcher.standalone_title_need(&line) == Some(false),
"inline-title link ref should not need title: {line}"
);
}
#[test]
fn single_quoted_inline_title_matches(
label in arb_label(),
url in arb_bare_url(),
title in arb_sq_title(),
) {
let matcher = LinkReferenceMatcher::production();
let line = format!("[{label}]: {url} {title}");
prop_assert!(matcher.is_definition(&line), "sq-title link ref should match: {line}");
prop_assert_eq!(matcher.standalone_title_need(&line), Some(false));
}
#[test]
fn parenthesis_inline_title_matches(
label in arb_label(),
url in arb_bare_url(),
title in arb_paren_title(),
) {
let matcher = LinkReferenceMatcher::production();
let line = format!("[{label}]: {url} {title}");
prop_assert!(matcher.is_definition(&line), "paren-title link ref should match: {line}");
prop_assert_eq!(matcher.standalone_title_need(&line), Some(false));
}
#[test]
fn escaped_inline_title_matches(
label in arb_label(),
url in arb_bare_url(),
title in arb_escaped_dq_title(),
) {
let matcher = LinkReferenceMatcher::production();
let line = format!("[{label}]: {url} {title}");
prop_assert!(matcher.is_definition(&line), "escaped-title link ref should match: {line}");
prop_assert_eq!(matcher.standalone_title_need(&line), Some(false));
}
#[test]
fn plain_prose_does_not_match(text in "[A-Za-z ]{1,60}") {
let matcher = LinkReferenceMatcher::production();
prop_assume!(!text.trim_start().starts_with('['));
prop_assert!(!matcher.is_definition(&text), "plain prose should not match: {text}");
}
#[test]
fn valid_standalone_title_matches(
spaces in 0usize..=3usize,
title in arb_title_form(),
) {
let matcher = LinkReferenceMatcher::production();
let line = format!("{:spaces$}{title}", "", spaces = spaces);
prop_assert!(matcher.is_standalone_title_line(&line), "standalone title should match: {line}");
}
#[test]
fn escaped_standalone_title_matches(
spaces in 0usize..=3usize,
title in arb_escaped_dq_title(),
) {
let matcher = LinkReferenceMatcher::production();
let line = format!("{:spaces$}{title}", "", spaces = spaces);
prop_assert!(
matcher.is_standalone_title_line(&line),
"escaped standalone title should match: {line}"
);
}
#[test]
fn over_indented_standalone_title_does_not_match(
extra in 4usize..=8usize,
title in arb_title_form(),
) {
let matcher = LinkReferenceMatcher::production();
let line = format!("{:spaces$}{title}", "", spaces = extra);
prop_assert!(
!matcher.is_standalone_title_line(&line),
"over-indented title should not match: {line}"
);
}
#[test]
fn bare_label_only_matches(label in arb_label(), trailing_spaces in 0usize..=4usize) {
let matcher = LinkReferenceMatcher::production();
let line = format!("[{label}]:{}", " ".repeat(trailing_spaces));
prop_assert!(matcher.is_bare_label_only(&line));
prop_assert!(!matcher.is_definition(&line));
}
#[test]
fn complete_definition_is_not_bare_label(label in arb_label(), url in arb_bare_url()) {
let matcher = LinkReferenceMatcher::production();
let line = format!("[{label}]: {url}");
prop_assert!(!matcher.is_bare_label_only(&line));
}
#[test]
fn indented_url_continuation_matches(url in arb_bare_url(), indent in 1usize..=4usize) {
let matcher = LinkReferenceMatcher::production();
let line = format!("{:indent$}{url}", "", indent = indent);
prop_assert!(matcher.is_url_continuation_line(&line));
}
#[test]
fn invalid_url_continuations_do_not_match(text in "[A-Za-z][A-Za-z0-9 ]{1,60}") {
let matcher = LinkReferenceMatcher::production();
prop_assert!(!matcher.is_url_continuation_line(&text));
prop_assert!(!matcher.is_url_continuation_line(""));
prop_assert!(!matcher.is_url_continuation_line(" "));
}
#[test]
fn closed_window_is_inert(line in "\\PC*") {
let matcher = LinkReferenceMatcher::production();
let mut window = LinkTitleWindow::Closed;
prop_assert_eq!(window.observe_next_line(&line, matcher), None);
prop_assert_eq!(window, LinkTitleWindow::Closed);
}
#[test]
fn fence_context_resets_window(
prior in prop_oneof![
Just(LinkTitleWindow::Closed),
Just(LinkTitleWindow::AwaitingStandaloneTitle),
Just(LinkTitleWindow::AwaitingUrlContinuation),
],
) {
let mut window = prior;
window.observe_fence_context();
prop_assert_eq!(window, LinkTitleWindow::Closed);
}
#[test]
fn awaiting_title_line_emits_verbatim(
spaces in 0usize..=3usize,
title in arb_title_form(),
) {
let matcher = LinkReferenceMatcher::production();
let line = format!("{:spaces$}{title}", "", spaces = spaces);
prop_assume!(matcher.is_standalone_title_line(&line));
let mut window = LinkTitleWindow::AwaitingStandaloneTitle;
prop_assert_eq!(
window.observe_next_line(&line, matcher),
Some(LinkTitleWindowOutcome::EmitVerbatim)
);
prop_assert_eq!(window, LinkTitleWindow::Closed);
}
#[test]
fn awaiting_blank_line_emits_verbatim(pad in 0usize..=4usize) {
let matcher = LinkReferenceMatcher::production();
let line = " ".repeat(pad);
prop_assume!(line.trim().is_empty());
let mut window = LinkTitleWindow::AwaitingStandaloneTitle;
prop_assert_eq!(
window.observe_next_line(&line, matcher),
Some(LinkTitleWindowOutcome::EmitVerbatim)
);
prop_assert_eq!(window, LinkTitleWindow::Closed);
}
#[test]
fn awaiting_prose_reprocesses(text in "[A-Za-z][A-Za-z0-9 ]{1,60}") {
let matcher = LinkReferenceMatcher::production();
prop_assume!(!matcher.is_standalone_title_line(&text));
let mut window = LinkTitleWindow::AwaitingStandaloneTitle;
prop_assert_eq!(
window.observe_next_line(&text, matcher),
Some(LinkTitleWindowOutcome::Reprocess)
);
prop_assert_eq!(window, LinkTitleWindow::Closed);
}
#[test]
fn awaiting_url_continuation_emits_verbatim(url in arb_bare_url(), indent in 1usize..=3usize) {
let matcher = LinkReferenceMatcher::production();
let line = format!("{:indent$}{url}", "", indent = indent);
let mut window = LinkTitleWindow::AwaitingUrlContinuation;
prop_assert_eq!(
window.observe_next_line(&line, matcher),
Some(LinkTitleWindowOutcome::EmitVerbatim)
);
prop_assert_eq!(window, LinkTitleWindow::AwaitingStandaloneTitle);
}
#[test]
fn url_continuation_inline_title_closes_window(
url in arb_bare_url(),
title in arb_dq_title(),
indent in 1usize..=3usize,
) {
let matcher = LinkReferenceMatcher::production();
let line = format!("{:indent$}{url} {title}", "", indent = indent);
let mut window = LinkTitleWindow::AwaitingUrlContinuation;
prop_assert_eq!(
window.observe_next_line(&line, matcher),
Some(LinkTitleWindowOutcome::EmitVerbatim)
);
prop_assert_eq!(window, LinkTitleWindow::Closed);
}
#[test]
fn awaiting_url_continuation_blank_line_emits_verbatim(pad in 0usize..=4usize) {
let matcher = LinkReferenceMatcher::production();
let line = " ".repeat(pad);
let mut window = LinkTitleWindow::AwaitingUrlContinuation;
prop_assert_eq!(
window.observe_next_line(&line, matcher),
Some(LinkTitleWindowOutcome::EmitVerbatim)
);
prop_assert_eq!(window, LinkTitleWindow::Closed);
}
#[test]
fn awaiting_url_continuation_prose_reprocesses(text in "[A-Za-z][A-Za-z0-9 ]{1,60}") {
let matcher = LinkReferenceMatcher::production();
let mut window = LinkTitleWindow::AwaitingUrlContinuation;
prop_assert_eq!(
window.observe_next_line(&text, matcher),
Some(LinkTitleWindowOutcome::Reprocess)
);
prop_assert_eq!(window, LinkTitleWindow::Closed);
}
}
proptest! {
#[test]
fn some_outcome_always_closes_terminal_window(
prior in prop_oneof![
Just(LinkTitleWindow::AwaitingUrlContinuation),
Just(LinkTitleWindow::AwaitingStandaloneTitle),
],
line in "[ -~]{1,80}",
) {
let matcher = LinkReferenceMatcher::production();
let mut window = prior;
let outcome = window.observe_next_line(&line, matcher);
prop_assume!(outcome.is_some());
prop_assume!(window != LinkTitleWindow::AwaitingStandaloneTitle);
prop_assert_eq!(window, LinkTitleWindow::Closed);
}
#[test]
fn url_continuation_then_title_or_prose_closes_window(
url in arb_bare_url(),
second_line in prop_oneof![arb_title_form(), Just(String::new()), "[ -~]{1,80}".prop_map(|s| s)],
) {
let matcher = LinkReferenceMatcher::production();
let mut window = LinkTitleWindow::Closed;
let first_line = format!(" {url}");
prop_assume!(matcher.is_url_continuation_line(&first_line));
window.observe_bare_label();
prop_assert_eq!(
window.observe_next_line(&first_line, matcher),
Some(LinkTitleWindowOutcome::EmitVerbatim)
);
prop_assert_eq!(window, LinkTitleWindow::AwaitingStandaloneTitle);
prop_assert!(window.observe_next_line(&second_line, matcher).is_some());
prop_assert_eq!(window, LinkTitleWindow::Closed);
}
}