fn assert_xpath(xml: &[u8], expr: &str, expected_count: usize) {
let index = simdxml::parse(xml).unwrap();
let results = index.xpath(expr).unwrap();
assert_eq!(
results.len(),
expected_count,
"XPath: {} on {:?} — got {} nodes, expected {}",
expr,
std::str::from_utf8(xml).unwrap(),
results.len(),
expected_count,
);
}
fn assert_xpath_text(xml: &[u8], expr: &str, expected: &[&str]) {
let index = simdxml::parse(xml).unwrap();
let results = index.xpath_text(expr).unwrap();
assert_eq!(
results, expected,
"XPath text: {} on {:?}",
expr,
std::str::from_utf8(xml).unwrap(),
);
}
#[test]
fn test_position_predicates() {
let xml = b"<r><a>1</a><a>2</a><a>3</a><a>4</a><a>5</a></r>";
assert_xpath(xml, "/r/a[1]", 1);
assert_xpath_text(xml, "/r/a[1]", &["1"]);
assert_xpath(xml, "/r/a[last()]", 1);
assert_xpath(xml, "/r/a[position()>2]", 3);
assert_xpath(xml, "/r/a[position()=last()]", 1);
assert_xpath_text(xml, "/r/a[position()=last()]", &["5"]);
assert_xpath(xml, "/r/a[position()<3]", 2);
assert_xpath(xml, "/r/a[0]", 0);
assert_xpath(xml, "/r/a[-1]", 0);
assert_xpath(xml, "/r/a[99]", 0);
assert_xpath(xml, "/r/a[2.5]", 0); }
#[test]
fn test_position_predicate_per_parent() {
let xml = b"<r><x><a>1</a><a>2</a></x><y><a>3</a><a>4</a></y></r>";
assert_xpath(xml, "//a[1]", 2);
assert_xpath_text(xml, "//a[1]", &["1", "3"]);
assert_xpath(xml, "//a[2]", 2);
assert_xpath_text(xml, "//a[2]", &["2", "4"]);
assert_xpath(xml, "//a[last()]", 2);
assert_xpath(xml, "(//a)[1]", 1);
assert_xpath_text(xml, "(//a)[1]", &["1"]);
assert_xpath(xml, "(//a)[last()]", 1);
assert_xpath(xml, "(//a)[3]", 1);
assert_xpath_text(xml, "(//a)[3]", &["3"]);
}
#[test]
fn test_nested_predicates() {
let xml = b"<r><a><b><c/></b></a><a><b/></a><a><b><c/><c/></b></a></r>";
assert_xpath(xml, "//a[b[c]]", 2);
assert_xpath(xml, "//a[b]", 3);
assert_xpath(xml, "//a[b[c[d]]]", 0);
assert_xpath(xml, "//a[b[1]]", 3);
assert_xpath(xml, "//a[b[2]]", 0);
}
#[test]
fn test_following_axis() {
let xml = b"<r><a/><b/><c><d/></c><e/></r>";
assert_xpath(xml, "/r/a/following::*", 4);
assert_xpath(xml, "/r/c/following::*", 1);
}
#[test]
fn test_preceding_axis() {
let xml = b"<r><a/><b/><c><d/></c><e/></r>";
assert_xpath(xml, "/r/e/preceding::*", 4);
assert_xpath(xml, "/r/c/d/preceding::*", 2);
assert_xpath(xml, "/r/a/preceding::*", 0); }
#[test]
fn test_ancestor_or_self_axis() {
let xml = b"<r><a><b><c/></b></a></r>";
assert_xpath(xml, "/r/a/b/c/ancestor-or-self::*", 4);
assert_xpath(xml, "/r/a/b/c/ancestor::*", 3);
assert_xpath(xml, "/r/a/b/c/ancestor-or-self::r", 1);
assert_xpath(xml, "/r/a/b/c/ancestor-or-self::c", 1);
}
#[test]
fn test_following_sibling_axis() {
let xml = b"<r><a/><b/><c/><d/></r>";
assert_xpath(xml, "/r/b/following-sibling::*", 2);
assert_xpath(xml, "/r/d/following-sibling::*", 0);
assert_xpath(xml, "/r/a/following-sibling::c", 1);
}
#[test]
fn test_preceding_sibling_axis() {
let xml = b"<r><a/><b/><c/><d/></r>";
assert_xpath(xml, "/r/c/preceding-sibling::*", 2);
assert_xpath(xml, "/r/a/preceding-sibling::*", 0);
}
#[test]
fn test_substring_edge_cases() {
let xml = b"<r><a>12345</a></r>";
assert_xpath_text(xml, "/r/a[substring(., -1, 4)='12']", &["12345"]);
assert_xpath_text(xml, "/r/a[substring(., 0, 3)='12']", &["12345"]);
assert_xpath_text(xml, "/r/a[substring(., 2)='2345']", &["12345"]);
assert_xpath_text(xml, "/r/a[substring(., 1, 0)='']", &["12345"]);
assert_xpath_text(xml, "/r/a[substring(., 0 div 0, 3)='']", &["12345"]);
}
#[test]
fn test_contains_edge_cases() {
let xml = b"<r><a>hello</a><a></a><a> </a></r>";
assert_xpath(xml, "/r/a[contains(., '')]", 3);
assert_xpath(xml, "/r/a[contains(., 'x')]", 0);
assert_xpath(xml, "/r/a[contains(., ' ')]", 1);
}
#[test]
fn test_starts_with_edge_cases() {
let xml = b"<r><a>hello</a><a></a></r>";
assert_xpath(xml, "/r/a[starts-with(., '')]", 2);
}
#[test]
fn test_string_length_edge_cases() {
let xml = b"<r><a>hello</a><a></a><a> </a></r>";
assert_xpath(xml, "/r/a[string-length(.)=0]", 1);
assert_xpath(xml, "/r/a[string-length(.)=2]", 1);
}
#[test]
fn test_normalize_space() {
let xml = b"<r><a> hello world </a></r>";
assert_xpath(xml, "/r/a[normalize-space(.)='hello world']", 1);
}
#[test]
fn test_translate_function() {
let xml = b"<r><a>Hello World</a></r>";
assert_xpath(xml, "/r/a[translate(., 'HW', 'hw')='hello world']", 1);
assert_xpath(xml, "/r/a[translate(., 'lo', 'L')='HeLLo WorLd']", 0);
assert_xpath(xml, "/r/a[translate(., 'lo', 'L')='HeLL WrLd']", 1);
}
#[test]
fn test_substring_before_after() {
let xml = b"<r><a>hello-world</a></r>";
assert_xpath(xml, "/r/a[substring-before(., '-')='hello']", 1);
assert_xpath(xml, "/r/a[substring-after(., '-')='world']", 1);
assert_xpath(xml, "/r/a[substring-before(., 'X')='']", 1);
assert_xpath(xml, "/r/a[substring-after(., 'X')='']", 1);
}
#[test]
fn test_division_by_zero() {
let result = simdxml::xpath::eval_standalone_expr("1 div 0");
assert!(result.is_ok());
let result = simdxml::xpath::eval_standalone_expr("0 div 0");
assert!(result.is_ok());
if let Ok(simdxml::xpath::StandaloneResult::Number(n)) = result {
assert!(n.is_nan(), "0 div 0 should be NaN");
}
}
#[test]
fn test_nan_comparisons() {
let xml = b"<r><a>1</a><a>2</a></r>";
assert_xpath(xml, "/r/a[0 div 0 = 0 div 0]", 0);
assert_xpath(xml, "/r/a[0 div 0 != 0 div 0]", 2);
assert_xpath(xml, "/r/a[0 div 0 < 1]", 0);
assert_xpath(xml, "/r/a[0 div 0 > 1]", 0);
}
#[test]
fn test_modulo() {
let result = simdxml::xpath::eval_standalone_expr("5 mod 2");
if let Ok(simdxml::xpath::StandaloneResult::Number(n)) = result {
assert_eq!(n, 1.0);
}
let result = simdxml::xpath::eval_standalone_expr("5 mod 0");
if let Ok(simdxml::xpath::StandaloneResult::Number(n)) = result {
assert!(n.is_nan(), "5 mod 0 should be NaN");
}
}
#[test]
fn test_boolean_coercion() {
let xml = b"<r><a x='1'/><a/><a x=''/></r>";
assert_xpath(xml, "/r/a[@y]", 0);
assert_xpath(xml, "/r/a[@x]", 2);
assert_xpath(xml, "/r/a[0]", 0);
assert_xpath(xml, "/r/a[not(@y)]", 3);
assert_xpath(xml, "/r/a[not(@x)]", 1);
assert_xpath(xml, "/r/a[true()]", 3);
assert_xpath(xml, "/r/a[false()]", 0);
}
#[test]
fn test_union_basic() {
let xml = b"<r><a/><b/><c/></r>";
assert_xpath(xml, "/r/a | /r/c", 2);
assert_xpath(xml, "/r/a | /r/a", 1);
assert_xpath(xml, "/r/c | /r/a", 2);
}
#[test]
fn test_union_mixed_node_types() {
let xml = b"<r><a>text</a><b/></r>";
assert_xpath(xml, "//a | //a/text()", 2);
assert_xpath(xml, "//a | //b", 2);
}
#[test]
fn test_union_three_way() {
let xml = b"<r><a/><b/><c/></r>";
assert_xpath(xml, "/r/a | /r/b | /r/c", 3);
}
#[test]
fn test_self_axis_with_name() {
let xml = b"<r><a/><b/></r>";
assert_xpath(xml, "/r/a/self::a", 1);
assert_xpath(xml, "/r/a/self::b", 0);
assert_xpath(xml, "/r/a/self::*", 1);
assert_xpath(xml, "/r/a/self::node()", 1);
}
#[test]
fn test_self_axis_in_descendant_context() {
let xml = b"<r><a><b/></a><b/></r>";
assert_xpath(xml, "//b/self::b", 2);
assert_xpath(xml, "/r/*/self::a", 1);
}
#[test]
fn test_child_path_in_predicate() {
let xml = b"<r><a><x>1</x></a><a><y>2</y></a></r>";
assert_xpath(xml, "/r/a[x]", 1);
assert_xpath(xml, "/r/a[child::x]", 1);
assert_xpath(xml, "/r/a[y]", 1);
assert_xpath(xml, "/r/a[z]", 0);
}
#[test]
fn test_nested_path_in_predicate() {
let xml = b"<r><a><b><c/></b></a><a><b/></a></r>";
assert_xpath(xml, "/r/a[b/c]", 1);
}
#[test]
fn test_multiple_predicates() {
let xml = b"<r><a x='1' y='2'/><a x='1' y='3'/><a x='2' y='2'/></r>";
assert_xpath(xml, "/r/a[@x='1'][@y='2']", 1);
assert_xpath(xml, "/r/a[@x='1'][@y='3']", 1);
assert_xpath(xml, "/r/a[@x='2'][@y='2']", 1);
assert_xpath(xml, "/r/a[@x='1']", 2);
}
#[test]
fn test_multiple_predicates_with_position() {
let xml = b"<r><a x='1'>A</a><a x='2'>B</a><a x='1'>C</a><a x='1'>D</a></r>";
assert_xpath(xml, "/r/a[@x='1'][2]", 1);
assert_xpath_text(xml, "/r/a[@x='1'][2]", &["C"]);
assert_xpath(xml, "/r/a[2][@x='1']", 0);
assert_xpath(xml, "/r/a[2][@x='2']", 1);
assert_xpath_text(xml, "/r/a[2][@x='2']", &["B"]);
}
#[test]
fn test_bare_root_slash() {
let xml = b"<r><a/></r>";
assert_xpath(xml, "/", 1);
}
#[test]
fn test_double_slash_root() {
let xml = b"<r><a><b/></a></r>";
assert_xpath(xml, "//*", 3); }
#[test]
fn test_parent_from_child() {
let xml = b"<r><a><b/></a></r>";
assert_xpath(xml, "/r/a/b/..", 1);
}
#[test]
fn test_dot_self() {
let xml = b"<r><a/></r>";
assert_xpath(xml, "/r/a/.", 1);
}
#[test]
fn test_attribute_existence() {
let xml = b"<r><a x='1'/><a/><a x=''/></r>";
assert_xpath(xml, "/r/a[@x]", 2);
}
#[test]
fn test_attribute_wildcard() {
let xml = b"<r><a x='1' y='2'/><a/><a z='3'/></r>";
assert_xpath(xml, "/r/a[@*]", 2); }
#[test]
fn test_attribute_value_comparison() {
let xml = b"<r><a n='10'/><a n='2'/><a n='20'/></r>";
assert_xpath(xml, "/r/a[@n > 5]", 2); }
#[test]
fn test_or_operator() {
let xml = b"<r><a x='1'/><a x='2'/><a x='3'/></r>";
assert_xpath(xml, "/r/a[@x='1' or @x='3']", 2);
assert_xpath(xml, "/r/a[@x='1' or @x='2' or @x='3']", 3);
}
#[test]
fn test_and_operator() {
let xml = b"<r><a x='1' y='2'/><a x='1'/><a y='2'/></r>";
assert_xpath(xml, "/r/a[@x='1' and @y='2']", 1);
}
#[test]
fn test_or_and_precedence() {
let xml = b"<r><a x='1' y='1'/><a x='2' y='1'/><a x='1' y='2'/></r>";
assert_xpath(xml, "/r/a[@x='2' or @x='1' and @y='2']", 2);
}
#[test]
fn test_whitespace_in_expressions() {
let xml = b"<r><a x='1'/></r>";
assert_xpath(xml, "/r/a[ @x = '1' ]", 1);
assert_xpath(xml, "/r/a[ 1 ]", 1);
}
#[test]
fn test_count_function() {
let xml = b"<r><a><b/><b/><b/></a><a><b/></a></r>";
assert_xpath(xml, "/r/a[count(b)=3]", 1);
assert_xpath(xml, "/r/a[count(b)>0]", 2);
assert_xpath(xml, "/r/a[count(b)=0]", 0);
}
#[test]
fn test_name_function() {
let xml = b"<r><alpha/><beta/></r>";
assert_xpath(xml, "/r/*[starts-with(name(), 'a')]", 1);
assert_xpath(xml, "/r/*[name()='beta']", 1);
}
#[test]
fn test_deep_nesting() {
let xml = b"<a><b><c><d><e><f><g><h><i><j/></i></h></g></f></e></d></c></b></a>";
assert_xpath(xml, "//j", 1);
assert_xpath(xml, "/a/b/c/d/e/f/g/h/i/j", 1);
assert_xpath(xml, "/a/b/c/d/e/f/g/h/i/j/ancestor::*", 9);
}
#[test]
fn test_self_closing_root() {
let xml = b"<r/>";
assert_xpath(xml, "/r", 1);
assert_xpath(xml, "/r/*", 0);
assert_xpath(xml, "/r/text()", 0);
assert_xpath(xml, "//r", 1);
}
#[test]
fn test_text_only_element() {
let xml = b"<r>hello</r>";
assert_xpath(xml, "/r", 1);
assert_xpath(xml, "/r/text()", 1);
assert_xpath_text(xml, "/r/text()", &["hello"]);
assert_xpath(xml, "/r/*", 0); }
#[test]
fn test_mixed_content() {
let xml = b"<r>before<a/>middle<b/>after</r>";
assert_xpath(xml, "/r/text()", 3);
assert_xpath(xml, "/r/node()", 5); }
#[test]
fn test_descendant_vs_descendant_or_self() {
let xml = b"<r><a><b/></a></r>";
assert_xpath(xml, "/r/descendant::*", 2);
assert_xpath(xml, "/r/descendant-or-self::*", 3);
}
#[test]
fn test_string_to_number_comparison() {
let xml = b"<r><a v='10'/><a v='2'/><a v='abc'/></r>";
assert_xpath(xml, "/r/a[@v > 5]", 1);
assert_xpath(xml, "/r/a[@v = 10]", 1);
}
#[test]
fn test_not_function() {
let xml = b"<r><a x='1'/><a x='2'/><a/></r>";
assert_xpath(xml, "/r/a[not(@x='1')]", 2);
assert_xpath(xml, "/r/a[not(@x)]", 1);
assert_xpath(xml, "/r/a[not(true())]", 0);
assert_xpath(xml, "/r/a[not(false())]", 3);
}
#[test]
fn test_concat_function() {
let xml = b"<r><a f='hello' l='world'/></r>";
assert_xpath(xml, "/r/a[concat(@f, ' ', @l)='hello world']", 1);
}
#[test]
fn test_sum_function() {
let xml = b"<r><v>10</v><v>20</v><v>30</v></r>";
assert_xpath(xml, "/r[sum(v)=60]", 1);
assert_xpath(xml, "/r[sum(v)>50]", 1);
}
#[test]
fn test_math_functions() {
let r = simdxml::xpath::eval_standalone_expr("floor(2.7)").unwrap();
if let simdxml::xpath::StandaloneResult::Number(n) = r {
assert_eq!(n, 2.0);
}
let r = simdxml::xpath::eval_standalone_expr("ceiling(2.3)").unwrap();
if let simdxml::xpath::StandaloneResult::Number(n) = r {
assert_eq!(n, 3.0);
}
let r = simdxml::xpath::eval_standalone_expr("round(2.5)").unwrap();
if let simdxml::xpath::StandaloneResult::Number(n) = r {
assert_eq!(n, 3.0);
}
let r = simdxml::xpath::eval_standalone_expr("round(-0.5)").unwrap();
if let simdxml::xpath::StandaloneResult::Number(n) = r {
assert_eq!(n, 0.0, "XPath round(-0.5) should be 0 (round half to +inf)");
}
}
#[test]
fn test_union_document_order() {
let xml = b"<r><a/><b/><c/></r>";
let index = simdxml::parse(xml).unwrap();
let results = index.xpath("/r/c | /r/a").unwrap();
assert_eq!(results.len(), 2);
if let (simdxml::xpath::XPathNode::Element(i0), simdxml::xpath::XPathNode::Element(i1)) = (&results[0], &results[1]) {
assert!(
i0 < i1,
"Union results should be in document order: {} should be < {}",
i0,
i1
);
}
}
#[test]
fn test_attribute_axis_direct() {
let xml = b"<r><a x='1' y='2'/></r>";
assert_xpath(xml, "/r/a/@x", 1);
assert_xpath(xml, "/r/a/@*", 2);
assert_xpath(xml, "//a/@x", 1);
}
#[test]
fn test_id_function() {
let xml = b"<r><a id='foo'/><b id='bar'/></r>";
assert_xpath(xml, "id('foo')", 1);
assert_xpath(xml, "id('bar')", 1);
assert_xpath(xml, "id('baz')", 0); }
#[test]
fn test_patent_like_xpath() {
let xml = br#"<patent>
<claims>
<claim type="independent" num="1">A device for processing signals.</claim>
<claim type="dependent" num="2">The device of claim 1, further comprising a filter.</claim>
<claim type="independent" num="3">A method of signal processing.</claim>
</claims>
</patent>"#;
assert_xpath(xml, "//claim", 3);
assert_xpath(xml, "//claim[@type='independent']", 2);
assert_xpath(xml, "//claim[1]", 1);
assert_xpath(xml, "//claim[contains(., 'device')]", 2);
assert_xpath(xml, "//claim[contains(., 'Device')]", 0);
assert_xpath(xml, "//claim[@type='independent'][1]", 1);
assert_xpath(xml, "//claims[count(claim[@type='independent'])=2]", 1);
}
#[test]
fn test_sibling_traversal_pattern() {
let xml = b"<r><a/><b/><a/><c/><a/></r>";
assert_xpath(xml, "/r/a[1]/following-sibling::*", 4);
assert_xpath(xml, "/r/a[1]/following-sibling::a", 2);
assert_xpath(xml, "/r/a[position()=last()]/preceding-sibling::*", 4); }
#[test]
fn test_comment_nodes() {
let xml = b"<r><!-- a comment --><a/></r>";
let index = simdxml::parse(xml).unwrap();
let _ = index.xpath("//comment()"); }
#[test]
fn test_wildcard_patterns() {
let xml = b"<r><a><x/></a><b><y/></b></r>";
assert_xpath(xml, "/r/*/x", 1);
assert_xpath(xml, "/r/*/*", 2);
assert_xpath(xml, "//*/x", 1);
}
#[test]
fn test_node_test() {
let xml = b"<r><a/>text<b/></r>";
assert_xpath(xml, "/r/node()", 3); }
#[test]
fn test_chained_axes() {
let xml = b"<r><a><b><c/></b></a><d/></r>";
assert_xpath(xml, "/r/a/b/c/ancestor::*/following-sibling::*", 1);
}
#[test]
fn test_predicate_on_no_results() {
let xml = b"<r><a/></r>";
assert_xpath(xml, "/r/b[1]", 0);
assert_xpath(xml, "/r/b[@x='1']", 0);
assert_xpath(xml, "/r/b[contains(., 'x')]", 0);
}
#[test]
fn test_global_filter_expressions() {
let xml = b"<r><a>1</a><b>2</b><a>3</a><b>4</b></r>";
assert_xpath(xml, "(//a | //b)[1]", 1);
assert_xpath_text(xml, "(//a | //b)[1]", &["1"]);
assert_xpath(xml, "(//a | //b)[last()]", 1);
assert_xpath(xml, "(//a)[2]", 1);
assert_xpath_text(xml, "(//a)[2]", &["3"]);
}
#[test]
fn test_lang_function() {
let xml = br#"<r xml:lang="en"><a xml:lang="fr"/><b/></r>"#;
assert_xpath(xml, "/r/b[lang('en')]", 1);
assert_xpath(xml, "/r/a[lang('fr')]", 1);
assert_xpath(xml, "/r/a[lang('en')]", 0);
}
#[test]
fn test_type_conversion_functions() {
let xml = b"<r><a>42</a><a>0</a><a>abc</a></r>";
assert_xpath(xml, "/r/a[number(.) > 10]", 1);
assert_xpath(xml, "/r/a[boolean(.)]", 3);
assert_xpath(xml, "/r/a[string-length(.) = 2]", 1); }
#[test]
fn test_parser_edge_cases() {
let xml2 = b"<r><my-element/></r>";
assert_xpath(xml2, "//my-element", 1);
let xml3 = b"<r><my_element/></r>";
assert_xpath(xml3, "//my_element", 1);
let xml4 = b"<r><_a/></r>";
assert_xpath(xml4, "//_a", 1);
}
#[test]
fn test_double_quotes_in_xpath() {
let xml = b"<r><a x='hello'/></r>";
assert_xpath(xml, r#"/r/a[@x="hello"]"#, 1);
}