use sup_xml::{parse_str, serialize_to_string, NodeKind, ParseOptions};
macro_rules! fixture {
($name:expr) => {
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../tests/fixtures/", $name))
};
}
#[test]
fn unusual_fixture_parses() {
parse_str(fixture!("unusual.xml"), &ParseOptions::default())
.expect("unusual.xml should parse cleanly");
}
#[test]
fn unusual_fixture_roundtrip_stable() {
let doc1 = parse_str(fixture!("unusual.xml"), &ParseOptions::default()).unwrap();
let text1 = serialize_to_string(&doc1);
let doc2 = parse_str(&text1, &ParseOptions::default()).unwrap();
let text2 = serialize_to_string(&doc2);
assert_eq!(text1, text2, "unusual.xml roundtrip is not stable");
}
#[test]
fn utf8_bom_stripped() {
let with_bom = "\u{FEFF}<?xml version=\"1.0\"?><root/>";
let doc = parse_str(with_bom, &ParseOptions::default())
.expect("UTF-8 BOM should be silently stripped");
let root = doc.root();
assert_eq!(root.kind, NodeKind::Element);
assert_eq!(root.name(), "root");
}
#[test]
fn bom_only_before_xml_decl() {
let with_bom = "\u{FEFF}<root/>";
parse_str(with_bom, &ParseOptions::default())
.expect("BOM without XML decl should still parse");
}
#[test]
fn null_char_ref_rejected() {
assert!(parse_str("<r>�</r>", &ParseOptions::default()).is_err(),
"U+0000 must be rejected");
assert!(parse_str("<r>�</r>", &ParseOptions::default()).is_err(),
"U+0000 decimal must be rejected");
}
#[test]
fn control_chars_rejected() {
for cp in [0x1u32, 0x2, 0x3, 0x7, 0x8, 0xB, 0xC, 0xE, 0xF, 0x1F] {
let xml = format!("<r>&#x{cp:X};</r>");
assert!(
parse_str(&xml, &ParseOptions::default()).is_err(),
"U+{cp:04X} should be rejected as invalid XML character: {xml}"
);
}
}
#[test]
fn tab_lf_cr_are_valid_xml_chars() {
parse_str("<r>	

</r>", &ParseOptions::default())
.expect("TAB/LF/CR must be accepted");
}
#[test]
fn surrogate_char_refs_rejected() {
for cp in [0xD800u32, 0xDBFF, 0xDC00, 0xDFFF] {
let xml = format!("<r>&#x{cp:X};</r>");
assert!(
parse_str(&xml, &ParseOptions::default()).is_err(),
"surrogate U+{cp:04X} must be rejected: {xml}"
);
}
}
#[test]
fn fffe_ffff_rejected() {
assert!(parse_str("<r></r>", &ParseOptions::default()).is_err(),
"U+FFFE must be rejected");
assert!(parse_str("<r></r>", &ParseOptions::default()).is_err(),
"U+FFFF must be rejected");
}
#[test]
fn out_of_unicode_range_rejected() {
assert!(parse_str("<r>�</r>", &ParseOptions::default()).is_err(),
"U+110000 must be rejected");
assert!(parse_str("<r>�</r>", &ParseOptions::default()).is_err(),
"over-range value must be rejected");
}
#[test]
fn highest_valid_codepoints_accepted() {
let doc = parse_str("<r></r>", &ParseOptions::default())
.expect("U+10FFFF is a valid XML character");
let root = doc.root();
assert_eq!(root.kind, NodeKind::Element);
let text = root.children().next().unwrap().text_content().unwrap();
assert_eq!(text, "\u{10FFFF}");
}
#[test]
fn supplementary_plane_roundtrip() {
let xml = "<r>𝐀𠀀</r>";
let doc1 = parse_str(xml, &ParseOptions::default()).unwrap();
let out = serialize_to_string(&doc1);
let doc2 = parse_str(&out, &ParseOptions::default()).unwrap();
let t1 = doc1.root().children().next().unwrap().text_content().unwrap().to_string();
let t2 = doc2.root().children().next().unwrap().text_content().unwrap().to_string();
assert_eq!(t1, t2);
}
#[test]
fn duplicate_attr_rejected() {
assert!(parse_str(r#"<r a="1" a="2"/>"#, &ParseOptions::default()).is_err(),
"duplicate attribute must be rejected");
}
#[test]
fn duplicate_attr_different_case_accepted() {
parse_str(r#"<r A="1" a="2"/>"#, &ParseOptions::default())
.expect("different-case attribute names are distinct");
}
#[test]
fn lt_in_comment_valid() {
parse_str("<r><!-- a < b && b > c --></r>", &ParseOptions::default())
.expect("< inside comment is valid");
}
#[test]
fn empty_cdata_section() {
let doc = parse_str("<r><![CDATA[]]></r>", &ParseOptions::default()).unwrap();
let root = doc.root();
let child = root.children().next().unwrap();
assert_eq!(child.kind, NodeKind::CData);
assert!(child.content().is_empty());
}
#[test]
fn cdata_with_double_bracket_not_ending() {
let doc = parse_str("<r><![CDATA[a]]b]]>after</r>", &ParseOptions::default()).unwrap();
let root = doc.root();
let mut iter = root.children();
let first = iter.next().unwrap();
assert_eq!(first.kind, NodeKind::CData);
assert_eq!(first.content(), "a]]b");
let second = iter.next().unwrap();
assert_eq!(second.text_content(), Some("after"));
}
#[test]
fn pi_with_no_content() {
let doc = parse_str("<?xml version=\"1.0\"?><r><?solo-pi?></r>", &ParseOptions::default()).unwrap();
let root = doc.root();
let child = root.children().next().unwrap();
assert_eq!(child.kind, NodeKind::Pi);
assert_eq!(child.name(), "solo-pi");
assert!(child.content().is_empty());
}
#[test]
fn many_attributes_preserved() {
let attrs: String = (1..=50).map(|i| format!(r#" a{i}="v{i}""#)).collect();
let xml = format!("<r{attrs}/>");
let doc = parse_str(&xml, &ParseOptions::default()).unwrap();
let el = doc.root();
assert_eq!(el.kind, NodeKind::Element);
let collected: Vec<_> = el.attributes().collect();
assert_eq!(collected.len(), 50);
assert_eq!(collected[49].name(), "a50");
assert_eq!(collected[49].value(), "v50");
}
#[test]
fn very_long_element_name() {
let name = "a".repeat(500);
let xml = format!("<{name}/>");
let doc = parse_str(&xml, &ParseOptions::default()).unwrap();
let root = doc.root();
assert_eq!(root.kind, NodeKind::Element);
assert_eq!(root.name(), name.as_str());
}
#[test]
fn very_long_text_content() {
let content = "x".repeat(100_000);
let xml = format!("<r>{content}</r>");
let doc = parse_str(&xml, &ParseOptions::default()).unwrap();
let root = doc.root();
assert_eq!(root.children().next().unwrap().text_content().unwrap().len(), 100_000);
}
#[test]
fn deeply_nested_100_levels() {
let open: String = (1..=100).map(|i| format!("<l{i}>")).collect();
let close: String = (1..=100).rev().map(|i| format!("</l{i}>")).collect();
let xml = format!("{open}deep{close}");
parse_str(&xml, &ParseOptions::default())
.expect("100 levels of nesting should not overflow the stack");
}
#[test]
fn unicode_element_names_via_utf8() {
let xml = "<résumé>content</résumé>";
parse_str(xml, &ParseOptions::default())
.expect("UTF-8 element names should parse");
}
#[test]
fn mixed_content_preserved() {
let xml = "<p>Hello <em>world</em>! How are <strong>you</strong>?</p>";
let doc = parse_str(xml, &ParseOptions::default()).unwrap();
let root = doc.root();
let kids: Vec<_> = root.children().collect();
assert_eq!(kids.len(), 5);
assert_eq!(kids[0].text_content(), Some("Hello "));
assert_eq!(kids[1].kind, NodeKind::Element);
assert_eq!(kids[1].name(), "em");
assert_eq!(kids[2].text_content(), Some("! How are "));
assert_eq!(kids[3].kind, NodeKind::Element);
assert_eq!(kids[3].name(), "strong");
assert_eq!(kids[4].text_content(), Some("?"));
}
#[test]
fn attribute_value_whitespace_is_normalized() {
let xml = "<r a=\"line1\nline2\ttabbed\"/>";
let doc = parse_str(xml, &ParseOptions::default()).unwrap();
let el = doc.root();
assert_eq!(el.attributes().next().unwrap().value(), "line1 line2 tabbed");
}
#[test]
fn attribute_value_char_ref_whitespace_is_preserved() {
let xml = "<r a=\"line1
line2	tabbed\"/>";
let doc = parse_str(xml, &ParseOptions::default()).unwrap();
let el = doc.root();
assert_eq!(el.attributes().next().unwrap().value(), "line1\nline2\ttabbed");
}
#[test]
fn attribute_value_entity_ref_whitespace_is_normalized() {
let xml = "<!DOCTYPE r [<!ENTITY tabby \"a\tb\">]><r v=\"X&tabby;Y\"/>";
let doc = parse_str(xml, &ParseOptions::default()).unwrap();
let v = doc.root().attributes().next().unwrap().value().to_string();
assert_eq!(v, "Xa bY");
}
#[test]
fn char_ref_in_attr_value() {
let doc = parse_str(r#"<r a="𝐀"/>"#, &ParseOptions::default()).unwrap();
let el = doc.root();
assert_eq!(el.attributes().next().unwrap().value(), "\u{1D400}");
}
#[test]
fn gt_unescaped_in_text_valid() {
let doc = parse_str("<r>a > b</r>", &ParseOptions::default()).unwrap();
let root = doc.root();
assert_eq!(root.children().next().unwrap().text_content(), Some("a > b"));
}
#[test]
fn cdata_gt_unescaped_in_serialize() {
let doc = parse_str("<r>a > b</r>", &ParseOptions::default()).unwrap();
let out = serialize_to_string(&doc);
assert!(out.contains(">"), "serializer must escape '>' in text: {out}");
}