use anyhow::Result;
use proptest::prelude::*;
use vexy_vsvg::Plugin;
use vexy_vsvg::{parse_svg, stringify};
fn svg_tag_names() -> impl Strategy<Value = &'static str> {
prop_oneof![
Just("svg"),
Just("g"),
Just("rect"),
Just("circle"),
Just("ellipse"),
Just("line"),
Just("path"),
Just("polygon"),
Just("polyline"),
Just("text"),
Just("defs"),
Just("use"),
Just("image"),
Just("clipPath"),
Just("mask"),
Just("pattern"),
Just("marker"),
Just("linearGradient"),
Just("radialGradient"),
Just("stop"),
Just("filter"),
Just("feGaussianBlur"),
Just("feColorMatrix"),
Just("feOffset"),
Just("feMorphology"),
Just("feFlood"),
Just("feComposite"),
Just("feMerge"),
Just("feMergeNode"),
Just("feImage"),
Just("feTurbulence"),
Just("feDisplacementMap"),
Just("feConvolveMatrix"),
Just("feDiffuseLighting"),
Just("feSpecularLighting"),
Just("feDistantLight"),
Just("fePointLight"),
Just("feSpotLight"),
Just("style"),
Just("title"),
Just("desc"),
Just("metadata"),
]
}
fn attr_names() -> impl Strategy<Value = &'static str> {
prop_oneof![
Just("id"),
Just("class"),
Just("style"),
Just("fill"),
Just("stroke"),
Just("stroke-width"),
Just("stroke-dasharray"),
Just("stroke-linecap"),
Just("stroke-linejoin"),
Just("opacity"),
Just("fill-opacity"),
Just("stroke-opacity"),
Just("transform"),
Just("x"),
Just("y"),
Just("width"),
Just("height"),
Just("cx"),
Just("cy"),
Just("r"),
Just("rx"),
Just("ry"),
Just("x1"),
Just("y1"),
Just("x2"),
Just("y2"),
Just("d"),
Just("points"),
Just("href"),
Just("xlink:href"),
Just("viewBox"),
Just("preserveAspectRatio"),
Just("xmlns"),
Just("version"),
Just("baseProfile"),
]
}
fn attr_values() -> impl Strategy<Value = String> {
prop_oneof![
Just("none".to_string()),
Just("currentColor".to_string()),
Just("#ff0000".to_string()),
Just("rgb(255, 0, 0)".to_string()),
Just("100".to_string()),
Just("100px".to_string()),
Just("50%".to_string()),
Just("translate(10, 20)".to_string()),
Just("scale(1.5)".to_string()),
Just("rotate(45)".to_string()),
]
}
fn simple_element() -> impl Strategy<Value = String> {
(
svg_tag_names(),
proptest::collection::vec((attr_names(), attr_values()), 0..5),
)
.prop_map(|(tag, attrs)| {
if attrs.is_empty() {
format!("<{}/>", tag)
} else {
let attr_str = attrs
.iter()
.map(|(name, value)| format!("{}=\"{}\"", name, value))
.collect::<Vec<_>>()
.join(" ");
format!("<{} {}/>", tag, attr_str)
}
})
}
pub fn simple_svg_document() -> impl Strategy<Value = String> {
proptest::collection::vec(simple_element(), 1..10).prop_map(|elements| {
format!(
"<svg xmlns=\"http://www.w3.org/2000/svg\">{}</svg>",
elements.join("")
)
})
}
pub fn prop_plugin_no_crash<P: Plugin + Clone>(plugin: P) -> impl Strategy<Value = ()> {
simple_svg_document().prop_map(move |svg| {
let plugin_clone = plugin.clone();
if let Ok(mut doc) = parse_svg(&svg) {
let _ = plugin_clone.apply(&mut doc);
}
})
}
pub fn prop_plugin_output_valid<P: Plugin + Clone>(plugin: P) -> impl Strategy<Value = ()> {
simple_svg_document().prop_map(move |svg| {
let plugin_clone = plugin.clone();
if let Ok(mut doc) = parse_svg(&svg) {
if plugin_clone.apply(&mut doc).is_ok() {
if let Ok(output) = stringify(&doc) {
assert!(parse_svg(&output).is_ok(), "Plugin output is not valid SVG");
}
}
}
})
}
pub fn prop_plugin_idempotent<P: Plugin + Clone>(plugin: P) -> impl Strategy<Value = ()> {
simple_svg_document().prop_map(move |svg| {
let plugin1 = plugin.clone();
let plugin2 = plugin.clone();
if let Ok(mut doc1) = parse_svg(&svg) {
if plugin1.apply(&mut doc1).is_ok() {
if let Ok(output1) = stringify(&doc1) {
if let Ok(mut doc2) = parse_svg(&output1) {
if plugin2.apply(&mut doc2).is_ok() {
if let Ok(output2) = stringify(&doc2) {
assert_eq!(output1, output2, "Plugin is not idempotent");
}
}
}
}
}
}
})
}
pub fn test_optimization_invariants<P: Plugin + Clone>(plugin: P, _test_cases: u32) -> Result<()> {
let mut runner = proptest::test_runner::TestRunner::default();
runner.run(&prop_plugin_no_crash(plugin.clone()), |_| Ok(()))?;
runner.run(&prop_plugin_output_valid(plugin.clone()), |_| Ok(()))?;
runner.run(&prop_plugin_idempotent(plugin.clone()), |_| Ok(()))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::plugins::RemoveCommentsPlugin;
use proptest::strategy::ValueTree;
#[test]
fn test_simple_svg_generation() {
let strategy = simple_svg_document();
let mut runner = proptest::test_runner::TestRunner::default();
for _ in 0..10 {
let svg = strategy.new_tree(&mut runner).unwrap().current();
assert!(svg.starts_with("<svg"));
assert!(svg.ends_with("</svg>"));
assert!(parse_svg(&svg).is_ok());
}
}
#[test]
fn test_remove_comments_invariants() {
let plugin = RemoveCommentsPlugin::new();
test_optimization_invariants(plugin, 100).unwrap();
}
}