use ahash::HashSet;
use proptest::prelude::*;
use crate::fixed;
const NAMESPACES: &[&str] = &["", "http://example.com/x", "http://example.com/y"];
const PREFIXES: &[&str] = &["", "x", "y"];
const ELEMENT_NAMES: &[&str] = &["a", "b", "c", "d", "e"];
const ATTRIBUTE_NAMES: &[&str] = &["q", "r", "s"];
const PI_NAMES: &[&str] = &["pi1", "pi2", "pi3", "pi4", "pi5"];
const XML_STRING: &str = "[\u{000a}\u{0009}][\u{0020}-\u{D7FF}][\u{E000}-\u{FFFD}]*";
const XML_STRING_WITHOUT_WHITESPACE: &str = "[\u{0020}-\u{D7FF}][\u{E000}-\u{FFFD}]*";
fn arb_attribute() -> impl Strategy<Value = (fixed::Name, String)> {
(
prop::sample::select(ATTRIBUTE_NAMES),
prop::sample::select(NAMESPACES),
XML_STRING_WITHOUT_WHITESPACE,
)
.prop_map(|(name, namespace, value)| {
(
fixed::Name {
localname: name.to_string(),
namespace: namespace.to_string(),
},
value,
)
})
}
fn arb_prefix() -> impl Strategy<Value = fixed::Prefix> {
(
prop::sample::select(PREFIXES),
prop::sample::select(NAMESPACES),
)
.prop_map(|(prefix, namespace)| fixed::Prefix {
name: prefix.to_string(),
namespace: namespace.to_string(),
})
}
fn arb_comment() -> impl Strategy<Value = String> {
XML_STRING.prop_filter("comment", |s| !s.contains('-'))
}
fn arb_processing_instruction() -> impl Strategy<Value = (String, Option<String>)> {
(
prop::sample::select(PI_NAMES),
prop::option::of(
XML_STRING_WITHOUT_WHITESPACE.prop_filter("non-empty string", |s| !s.is_empty()),
),
)
.prop_map(|(target, data)| (target.to_string(), data))
}
fn arb_fixed_content() -> impl Strategy<Value = fixed::Content> {
let leaf = prop_oneof![
XML_STRING.prop_map(fixed::Content::Text),
arb_comment().prop_map(fixed::Content::Comment),
arb_processing_instruction().prop_map(|(target, content)| {
let processing_instruction = fixed::ProcessingInstruction { target, content };
fixed::Content::ProcessingInstruction(processing_instruction)
}),
];
leaf.prop_recursive(
8, 256, 10, |inner| {
(
prop::sample::select(ELEMENT_NAMES),
prop::sample::select(NAMESPACES),
prop::collection::vec(inner, 0..10),
prop::collection::vec(arb_attribute(), 0..4),
prop::collection::vec(arb_prefix(), 0..4),
)
.prop_map(|(name, namespace, children, attributes, prefixes)| {
fixed::Content::Element(fixed::Element {
name: fixed::Name {
namespace: namespace.to_string(),
localname: name.to_string(),
},
attributes: unduplicate_attributes(attributes.as_slice()),
prefixes: unduplicate_prefixes(prefixes.as_slice()),
children,
})
})
},
)
}
prop_compose! {
fn arb_fixed_element()(name in prop::sample::select(ELEMENT_NAMES),
namespace in prop::sample::select(NAMESPACES),
children in arb_fixed_content(),
attributes in prop::collection::vec(arb_attribute(), 0..4),
prefixes in prop::collection::vec(arb_prefix(), 0..4)) -> fixed::Element {
fixed::Element {
name: fixed::Name {
namespace: namespace.to_string(),
localname: name.to_string(),
},
attributes: unduplicate_attributes(attributes.as_slice()),
prefixes: unduplicate_prefixes(prefixes.as_slice()),
children: vec![children],
}
}
}
fn unduplicate_attributes(attributes: &[(fixed::Name, String)]) -> Vec<(fixed::Name, String)> {
let mut seen = HashSet::default();
attributes
.iter()
.filter(|(name, _)| seen.insert(name.clone()))
.cloned()
.collect()
}
fn unduplicate_prefixes(prefixes: &[fixed::Prefix]) -> Vec<fixed::Prefix> {
let mut seen = HashSet::default();
prefixes
.iter()
.filter(|prefix| seen.insert(*prefix))
.cloned()
.collect()
}
pub fn arb_xml_document() -> impl Strategy<Value = fixed::Document> {
arb_xml_document_with_config(Config {
comments_and_pi_outside_document_element: true,
})
}
#[derive(Default)]
pub struct Config {
comments_and_pi_outside_document_element: bool,
}
pub fn arb_xml_document_with_config(config: Config) -> BoxedStrategy<fixed::Document> {
if config.comments_and_pi_outside_document_element {
let before = prop::collection::vec(
prop_oneof![
arb_comment().prop_map(fixed::DocumentContent::Comment),
arb_processing_instruction().prop_map(|(target, content)| {
let processing_instruction = fixed::ProcessingInstruction { target, content };
fixed::DocumentContent::ProcessingInstruction(processing_instruction)
}),
],
0..10,
);
let after = before.clone();
(before, arb_fixed_element(), after)
.prop_map(|(before, document_element, after)| fixed::Document {
before,
document_element,
after,
})
.boxed()
} else {
arb_fixed_element()
.prop_map(|document_element| fixed::Document {
before: vec![],
document_element,
after: vec![],
})
.boxed()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::xotdata::Xot;
proptest! {
#[test]
fn test_arb_xml_can_serialize_parse(fixed_root in arb_xml_document()) {
let mut xot = Xot::new();
let node = fixed_root.xotify(&mut xot);
xot.create_missing_prefixes(node).unwrap();
let serialized = xot.to_string(node).unwrap();
let parsed = xot.parse(&serialized);
prop_assert!(parsed.is_ok(), "Cannot parse: {} {} {:?}", serialized, parsed.err().unwrap(), serialized);
}
}
proptest! {
#[test]
fn test_arb_xml_can_serialize_parse2(fixed_root in arb_xml_document_with_config(Config::default())) {
let mut xot = Xot::new();
let node = fixed_root.xotify(&mut xot);
xot.create_missing_prefixes(node).unwrap();
let serialized = xot.to_string(node).unwrap();
let parsed = xot.parse(&serialized);
prop_assert!(parsed.is_ok(), "Cannot parse: {} {} {:?}", serialized, parsed.err().unwrap(), serialized);
}
}
}