use proptest::prelude::*;
proptest! {
#[test]
fn any_markdown_no_panic(input in "\\PC{0,500}") {
let result = surf_parse::parse(&input);
let _ = result.doc.blocks.len();
let _ = result.diagnostics.len();
}
#[test]
fn roundtrip_preserves_content(
heading in "[A-Za-z ]{1,30}",
body in "[A-Za-z0-9 .,!?]{1,100}"
) {
let input = format!("# {heading}\n\n{body}\n");
let result = surf_parse::parse(&input);
let md = result.doc.to_markdown();
assert!(
md.contains(heading.trim()),
"Round-trip should preserve heading '{heading}', got: {md}"
);
assert!(
md.contains(body.trim()),
"Round-trip should preserve body '{body}', got: {md}"
);
}
#[test]
fn attrs_parser_completeness(input in "[a-z0-9=\", ]{0,100}") {
let bracketed = format!("[{input}]");
let result = surf_parse::attrs::parse_attrs(&bracketed);
match result {
Ok(attrs) => {
let _ = attrs.len();
}
Err(_e) => {
}
}
}
}
use surf_parse::render_html::to_html_fragment;
use surf_parse::types::*;
fn synth_code(lang: Option<&str>, file: Option<&str>, content: &str) -> Block {
Block::Code {
lang: lang.map(|s| s.to_string()),
file: file.map(|s| s.to_string()),
highlight: vec![],
content: content.to_string(),
span: Span::SYNTHETIC,
}
}
fn synth_markdown(content: &str) -> Block {
Block::Markdown {
content: content.to_string(),
span: Span::SYNTHETIC,
}
}
fn synth_callout(ct: CalloutType, title: Option<&str>, content: &str) -> Block {
Block::Callout {
callout_type: ct,
title: title.map(|s| s.to_string()),
content: content.to_string(),
span: Span::SYNTHETIC,
}
}
fn synth_form(fields: Vec<FormField>, submit_label: Option<&str>) -> Block {
Block::Form {
fields,
submit_label: submit_label.map(|s| s.to_string()),
action: None,
method: None,
honeypot: false,
span: Span::SYNTHETIC,
}
}
fn synth_gallery(items: Vec<GalleryItem>, columns: Option<u32>) -> Block {
Block::Gallery {
items,
columns,
span: Span::SYNTHETIC,
}
}
fn synth_section(
bg: Option<&str>,
headline: Option<&str>,
subtitle: Option<&str>,
children: Vec<Block>,
) -> Block {
Block::Section {
bg: bg.map(|s| s.to_string()),
headline: headline.map(|s| s.to_string()),
subtitle: subtitle.map(|s| s.to_string()),
content: String::new(),
children,
span: Span::SYNTHETIC,
}
}
fn arb_callout_type() -> impl Strategy<Value = CalloutType> {
prop_oneof![
Just(CalloutType::Info),
Just(CalloutType::Warning),
Just(CalloutType::Danger),
Just(CalloutType::Tip),
Just(CalloutType::Note),
Just(CalloutType::Success),
]
}
fn arb_form_field_type() -> impl Strategy<Value = FormFieldType> {
prop_oneof![
Just(FormFieldType::Text),
Just(FormFieldType::Email),
Just(FormFieldType::Tel),
Just(FormFieldType::Date),
Just(FormFieldType::Number),
Just(FormFieldType::Select),
Just(FormFieldType::Textarea),
]
}
fn arb_form_field() -> impl Strategy<Value = FormField> {
(
"[a-zA-Z ]{1,20}", "[a-z_]{1,15}", arb_form_field_type(),
any::<bool>(), proptest::option::of("[a-zA-Z0-9 ]{0,20}"), proptest::collection::vec("[a-zA-Z]{1,10}", 0..4), )
.prop_map(|(label, name, field_type, required, placeholder, options)| FormField {
label,
name,
field_type,
required,
placeholder,
options,
})
}
fn arb_gallery_item() -> impl Strategy<Value = GalleryItem> {
(
"[a-z/]{1,30}\\.jpg", proptest::option::of("[a-zA-Z ]{1,30}"), proptest::option::of("[a-zA-Z ]{1,30}"), proptest::option::of("[a-zA-Z]{1,10}"), )
.prop_map(|(src, caption, alt, category)| GalleryItem {
src,
caption,
alt,
category,
})
}
fn arb_block() -> impl Strategy<Value = Block> {
prop_oneof![
"\\PC{0,100}".prop_map(|content| synth_markdown(&content)),
(
proptest::option::of("[a-z]{1,10}"),
proptest::option::of("[a-z/.]{1,20}"),
"\\PC{0,200}",
)
.prop_map(|(lang, file, content)| synth_code(
lang.as_deref(),
file.as_deref(),
&content,
)),
(arb_callout_type(), proptest::option::of("[a-zA-Z ]{1,20}"), "\\PC{0,100}")
.prop_map(|(ct, title, content)| synth_callout(ct, title.as_deref(), &content)),
proptest::option::of("[a-zA-Z ]{1,20}")
.prop_map(|label| Block::Divider {
label,
span: Span::SYNTHETIC,
}),
"\\PC{0,100}".prop_map(|content| Block::Summary {
content,
span: Span::SYNTHETIC,
}),
(
"[a-z/]{1,20}\\.png",
proptest::option::of("[a-zA-Z ]{1,30}"),
proptest::option::of("[a-zA-Z ]{1,30}"),
)
.prop_map(|(src, caption, alt)| Block::Figure {
src,
caption,
alt,
width: None,
span: Span::SYNTHETIC,
}),
(
prop_oneof!["architecture", "erd", "[a-z]{0,10}"],
proptest::option::of("[a-zA-Z ]{1,20}"),
"\\PC{0,100}",
)
.prop_map(|(diagram_type, title, content)| Block::Diagram {
diagram_type,
title,
content,
span: Span::SYNTHETIC,
}),
("\\PC{0,100}", proptest::option::of("[a-zA-Z ]{1,20}"))
.prop_map(|(content, attribution)| Block::Quote {
content,
attribution,
cite: None,
span: Span::SYNTHETIC,
}),
(proptest::option::of("[a-zA-Z ]{1,20}"), any::<bool>(), "\\PC{0,100}")
.prop_map(|(title, open, content)| Block::Details {
title,
open,
content,
span: Span::SYNTHETIC,
}),
(
proptest::collection::vec(arb_form_field(), 0..4),
proptest::option::of("[a-zA-Z ]{1,15}"),
)
.prop_map(|(fields, submit_label)| synth_form(fields, submit_label.as_deref())),
(
proptest::collection::vec(arb_gallery_item(), 0..4),
proptest::option::of(1u32..6),
)
.prop_map(|(items, columns)| synth_gallery(items, columns)),
(
proptest::option::of("[a-zA-Z0-9]{1,10}"),
proptest::option::of("[a-zA-Z ]{1,20}"),
proptest::option::of("[a-zA-Z ]{1,30}"),
)
.prop_map(|(bg, headline, subtitle)| synth_section(
bg.as_deref(),
headline.as_deref(),
subtitle.as_deref(),
vec![],
)),
]
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(200))]
#[test]
fn fragment_never_panics(blocks in proptest::collection::vec(arb_block(), 0..10)) {
let html = to_html_fragment(&blocks);
let _ = html.len();
}
}
#[cfg(feature = "native")]
mod native_props {
use super::*;
use surf_parse::render_native::to_native_blocks;
proptest! {
#![proptest_config(ProptestConfig::with_cases(200))]
#[test]
fn native_conversion_length_le_input(blocks in proptest::collection::vec(arb_block(), 0..10)) {
let doc = SurfDoc {
blocks: blocks.clone(),
front_matter: None,
source: String::new(),
};
let native = to_native_blocks(&doc);
prop_assert!(
native.len() <= blocks.len(),
"NativeBlock count ({}) should be <= input block count ({})",
native.len(),
blocks.len(),
);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn form_conversion_preserves_fields(
fields in proptest::collection::vec(arb_form_field(), 1..5),
submit_label in proptest::option::of("[a-zA-Z ]{1,15}"),
) {
let block = synth_form(fields.clone(), submit_label.as_deref());
let doc = SurfDoc {
blocks: vec![block],
front_matter: None,
source: String::new(),
};
let native = to_native_blocks(&doc);
prop_assert_eq!(native.len(), 1);
if let surf_parse::render_native::NativeBlock::Form {
fields: native_fields,
submit_label: native_submit,
} = &native[0]
{
prop_assert_eq!(native_fields.len(), fields.len());
for (nf, f) in native_fields.iter().zip(fields.iter()) {
prop_assert_eq!(&nf.label, &f.label);
prop_assert_eq!(&nf.name, &f.name);
prop_assert_eq!(nf.required, f.required);
prop_assert_eq!(&nf.placeholder, &f.placeholder);
prop_assert_eq!(&nf.options, &f.options);
}
let expected_label = submit_label.unwrap_or_else(|| "Submit".to_string());
prop_assert_eq!(native_submit, &expected_label);
} else {
prop_assert!(false, "Expected NativeBlock::Form, got {:?}", native[0]);
}
}
#[test]
fn gallery_conversion_preserves_fields(
items in proptest::collection::vec(arb_gallery_item(), 1..5),
columns in proptest::option::of(1u32..6),
) {
let block = synth_gallery(items.clone(), columns);
let doc = SurfDoc {
blocks: vec![block],
front_matter: None,
source: String::new(),
};
let native = to_native_blocks(&doc);
prop_assert_eq!(native.len(), 1);
if let surf_parse::render_native::NativeBlock::Gallery {
items: native_items,
columns: native_cols,
} = &native[0]
{
prop_assert_eq!(native_items.len(), items.len());
for (ni, i) in native_items.iter().zip(items.iter()) {
prop_assert_eq!(&ni.src, &i.src);
prop_assert_eq!(&ni.caption, &i.caption);
prop_assert_eq!(&ni.alt, &i.alt);
prop_assert_eq!(&ni.category, &i.category);
}
let expected_cols = columns.unwrap_or(3);
prop_assert_eq!(*native_cols, expected_cols);
} else {
prop_assert!(false, "Expected NativeBlock::Gallery, got {:?}", native[0]);
}
}
#[test]
fn section_container_conversion_preserves_fields(
bg in proptest::option::of("[a-zA-Z0-9]{1,10}"),
headline in proptest::option::of("[a-zA-Z ]{1,20}"),
subtitle in proptest::option::of("[a-zA-Z ]{1,30}"),
child_content in "[a-zA-Z0-9 ]{1,50}",
) {
let child = synth_markdown(&child_content);
let block = synth_section(
bg.as_deref(),
headline.as_deref(),
subtitle.as_deref(),
vec![child],
);
let doc = SurfDoc {
blocks: vec![block],
front_matter: None,
source: String::new(),
};
let native = to_native_blocks(&doc);
prop_assert_eq!(native.len(), 1);
if let surf_parse::render_native::NativeBlock::SectionContainer {
bg: native_bg,
headline: native_headline,
subtitle: native_subtitle,
children,
} = &native[0]
{
prop_assert_eq!(native_bg, &bg);
prop_assert_eq!(native_headline, &headline);
prop_assert_eq!(native_subtitle, &subtitle);
prop_assert_eq!(children.len(), 1);
if let surf_parse::render_native::NativeBlock::Markdown { content } = &children[0] {
prop_assert_eq!(content, &child_content);
} else {
prop_assert!(false, "Expected NativeBlock::Markdown child, got {:?}", children[0]);
}
} else {
prop_assert!(false, "Expected NativeBlock::SectionContainer, got {:?}", native[0]);
}
}
}
}
use surf_parse::types::ModelFieldType;
use surf_parse::parse_schema_field_type;
proptest! {
#![proptest_config(ProptestConfig::with_cases(200))]
#[test]
fn valid_type_strings_always_parse(
type_str in prop_oneof![
Just("uuid"),
Just("string"),
Just("str"),
Just("varchar"),
Just("text"),
Just("int"),
Just("integer"),
Just("i64"),
Just("float"),
Just("f64"),
Just("double"),
Just("bool"),
Just("boolean"),
Just("datetime"),
Just("timestamp"),
Just("json"),
Just("jsonb"),
Just("money"),
Just("cents"),
Just("price"),
Just("image"),
Just("img"),
Just("photo"),
Just("email"),
Just("url"),
Just("uri"),
Just("link"),
Just("enum:a,b,c"),
Just("ref:User"),
]
) {
let result = parse_schema_field_type(type_str);
prop_assert!(
result.is_ok(),
"parse_schema_field_type({:?}) should succeed but got {:?}",
type_str,
result,
);
}
#[test]
fn parse_schema_field_type_deterministic(input in "\\PC{0,50}") {
let r1 = parse_schema_field_type(&input);
let r2 = parse_schema_field_type(&input);
match (&r1, &r2) {
(Ok(a), Ok(b)) => prop_assert_eq!(a, b, "Determinism violated for {:?}", input),
(Err(_), Err(_)) => {} _ => prop_assert!(false, "Determinism violated: one Ok one Err for {:?}", input),
}
}
}
#[test]
fn all_simple_model_field_type_variants_reachable() {
let mapping: Vec<(&str, ModelFieldType)> = vec![
("uuid", ModelFieldType::Uuid),
("string", ModelFieldType::String),
("text", ModelFieldType::Text),
("int", ModelFieldType::Int),
("float", ModelFieldType::Float),
("bool", ModelFieldType::Bool),
("datetime", ModelFieldType::Datetime),
("json", ModelFieldType::Json),
("money", ModelFieldType::Money),
("image", ModelFieldType::Image),
("email", ModelFieldType::Email),
("url", ModelFieldType::Url),
("enum:x,y", ModelFieldType::Enum(vec!["x".to_string(), "y".to_string()])),
("ref:Post", ModelFieldType::Ref("Post".to_string())),
];
for (input, expected) in mapping {
let result = parse_schema_field_type(input)
.unwrap_or_else(|e| panic!("Failed to parse {input:?}: {e:?}"));
assert_eq!(result, expected, "Variant mismatch for {input:?}");
}
}