mod support;
use proptest::prelude::*;
use proptest::test_runner::TestCaseError;
use xdoc::core::Document;
use xdoc::core::XmlResult;
use xdoc::testing;
use xdoc::writer::{escape_attribute, escape_text, to_string_compact};
const VALID_FIXTURES: &[&str] = &["simple.xml", "namespaces.xml", "mixed_content.xml"];
const INVALID_FIXTURES: &[&str] = &[
"invalid.xml",
"mismatched_tags.xml",
"multiple_roots.xml",
"unbound_namespace.xml",
];
#[test]
fn testing_valid_fixtures_parse_and_roundtrip() -> XmlResult<()> {
for fixture in VALID_FIXTURES {
let result = support::assert_fixture_roundtrip(fixture)?;
assert_eq!(
result.compact_xml(),
result.reserialized_xml(),
"{fixture} should serialize deterministically"
);
}
Ok(())
}
#[test]
fn testing_invalid_fixtures_are_rejected() -> XmlResult<()> {
for fixture in INVALID_FIXTURES {
let xml = support::read_xml_fixture(fixture)?;
let error = support::parse_xml(&xml).expect_err("invalid XML fixture must fail");
assert!(
!error.message().is_empty(),
"{fixture} should fail with a useful parser error"
);
}
Ok(())
}
#[test]
fn golden_helper_compares_writer_output() -> XmlResult<()> {
let document = support::parse_xml("<Root><Child>value</Child></Root>")?;
let xml = to_string_compact(&document)?;
support::assert_golden("writer_simple.xml", &xml)
}
#[test]
fn golden_helper_reports_actionable_diff() {
let diff = testing::compare_xml("<Root><A/></Root>", "<Root><B/></Root>")
.expect_err("XML must differ");
assert!(diff.summary().contains("line 1"));
assert!(diff.summary().contains("<Root><A/></Root>"));
assert!(diff.summary().contains("<Root><B/></Root>"));
}
#[test]
fn roundtrip_helper_rejects_invalid_xml() -> XmlResult<()> {
let xml = support::read_xml_fixture("mismatched_tags.xml")?;
let error = testing::assert_compact_roundtrip(&xml).expect_err("roundtrip must reject invalid");
assert!(!error.message().is_empty());
Ok(())
}
fn xml_text_strategy() -> impl Strategy<Value = String> {
prop::collection::vec(xml_text_char_strategy(), 0..64)
.prop_map(|chars| chars.into_iter().collect())
}
fn xml_text_char_strategy() -> impl Strategy<Value = char> {
prop_oneof![
Just('&'),
Just('<'),
Just('>'),
Just('"'),
Just('\''),
Just(' '),
proptest::char::range('a', 'z'),
proptest::char::range('A', 'Z'),
proptest::char::range('0', '9'),
]
}
fn parse_prop(xml: &str) -> Result<Document, TestCaseError> {
support::parse_xml(xml).map_err(|error| TestCaseError::fail(error.to_string()))
}
fn serialize_prop(document: &Document) -> Result<String, TestCaseError> {
to_string_compact(document).map_err(|error| TestCaseError::fail(error.to_string()))
}
proptest! {
#[test]
fn property_testing_escape_text_roundtrips(value in xml_text_strategy()) {
let escaped = escape_text(&value);
let xml = format!("<Root>{escaped}</Root>");
let parsed = parse_prop(&xml)?;
let serialized = serialize_prop(&parsed)?;
let expected = if value.is_empty() {
"<Root/>".to_owned()
} else {
xml
};
prop_assert_eq!(serialized, expected);
}
#[test]
fn property_testing_escape_attribute_roundtrips(value in xml_text_strategy()) {
let escaped = escape_attribute(&value);
let xml = format!("<Root value=\"{escaped}\"/>");
let parsed = parse_prop(&xml)?;
let serialized = serialize_prop(&parsed)?;
prop_assert_eq!(serialized, xml);
}
}