use std::collections::BTreeMap;
use structured_email_address::{Config, EmailAddress, Strictness};
const SUITE: &str = include_str!("isemail_tests.xml");
const ACCEPT_MAX_RANK: u8 = 4;
const TARGET_CATEGORIES: &[&str] = &[
"ISEMAIL_VALID_CATEGORY",
"ISEMAIL_RFC5321",
"ISEMAIL_RFC5322",
];
const KNOWN_DIVERGENCES: &[(u32, &str)] = &[];
fn rank(category: &str) -> u8 {
match category {
"ISEMAIL_VALID_CATEGORY" => 0,
"ISEMAIL_DNSWARN" => 1,
"ISEMAIL_RFC5321" => 2,
"ISEMAIL_CFWS" => 3,
"ISEMAIL_DEPREC" => 4,
"ISEMAIL_RFC5322" => 5,
"ISEMAIL_ERR" => 6,
other => panic!("unknown isEmail category: {other}"),
}
}
fn decode_controls(s: &str) -> String {
s.chars()
.map(|c| {
let cp = c as u32;
match cp {
0x2400..=0x241F => char::from_u32(cp - 0x2400).unwrap_or(c),
0x2420 => ' ',
0x2421 => '\u{7f}',
_ => c,
}
})
.collect()
}
fn child_text<'a>(node: &roxmltree::Node<'a, '_>, tag: &str) -> &'a str {
node.children()
.find(|n| n.has_tag_name(tag))
.and_then(|n| n.text())
.unwrap_or("")
}
#[test]
fn isemail_conformance() {
let cfg = Config::builder()
.strictness(Strictness::Lax)
.allow_domain_literal()
.allow_single_label_domain()
.build();
let doc = roxmltree::Document::parse(SUITE)
.unwrap_or_else(|e| panic!("isemail_tests.xml must parse: {e}"));
let mut per_cat: BTreeMap<&str, (u32, u32)> = BTreeMap::new();
let mut divergences: Vec<(u32, String, String, bool, Option<String>)> = Vec::new();
for node in doc.descendants().filter(|n| n.has_tag_name("test")) {
let id: u32 = node
.attribute("id")
.unwrap_or_else(|| panic!("test must have id"))
.parse()
.unwrap_or_else(|e| panic!("id must be numeric: {e}"));
let address = decode_controls(child_text(&node, "address"));
let category = child_text(&node, "category");
assert!(!category.is_empty(), "#{id} missing category");
let expect_accept = rank(category) <= ACCEPT_MAX_RANK;
let result = EmailAddress::parse_with(&address, &cfg);
let got_accept = result.is_ok();
let entry = per_cat.entry(category).or_default();
entry.1 += 1;
if got_accept == expect_accept {
entry.0 += 1;
} else {
let err = result.err().map(|e| e.to_string());
divergences.push((id, address, category.to_string(), expect_accept, err));
}
}
eprintln!("\nisEmail conformance (Lax-permissive config):");
for (cat, (pass, total)) in &per_cat {
eprintln!(" {cat:<24} {pass:>3}/{total:<3}");
}
let mut unexpected = Vec::new();
if !divergences.is_empty() {
eprintln!("\ndivergences:");
}
for (id, addr, cat, expect_accept, err) in &divergences {
let reason = KNOWN_DIVERGENCES
.iter()
.find(|(kid, _)| kid == id)
.map(|(_, r)| *r);
eprintln!(
" #{id} [{cat}] expected={} got={} addr={addr:?}{} :: {}",
if *expect_accept { "accept" } else { "reject" },
if err.is_some() { "reject" } else { "accept" },
err.as_deref()
.map(|e| format!(" ({e})"))
.unwrap_or_default(),
reason.unwrap_or("UNEXPECTED — regression"),
);
if reason.is_none() {
unexpected.push(*id);
}
}
assert!(
unexpected.is_empty(),
"unexpected conformance divergences (not in KNOWN_DIVERGENCES): {unexpected:?}"
);
let (mut pass, mut total) = (0u32, 0u32);
for cat in TARGET_CATEGORIES {
let (p, t) = per_cat.get(*cat).copied().unwrap_or((0, 0));
pass += p;
total += t;
}
let rate = f64::from(pass) / f64::from(total);
eprintln!("\ntarget categories: {pass}/{total} = {:.1}%", rate * 100.0);
assert!(
rate >= 0.98,
"target pass rate {:.1}% < 98% ({pass}/{total})",
rate * 100.0
);
}