#![cfg(feature = "testkit")]
use std::fs;
use std::path::Path;
use html_to_markdown_rs::{ConversionOptions, TierStrategy, convert};
const FIXTURES_ROOT: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../tools/benchmark-harness/fixtures");
const FIXTURE_PATHS: &[&str] = &[
"mdream/nuxt-example.html",
"mdream/vuejs-docs.html",
"mdream/wikipedia-small.html",
"mdream/mdn-array.html",
"mdream/react-learn.html",
"mdream/github-markdown-complete.html",
"real-world/wikipedia/small_html.html",
"real-world/wikipedia/lists_timeline.html",
"real-world/wikipedia/tables_countries.html",
"real-world/wikipedia/medium_python.html",
"real-world/wikipedia/large_rust.html",
"real-world/issues/gh-190/mitrade.html",
"real-world/issues/gh-190/flex2025.html",
"real-world/issues/gh-190/insight.html",
"real-world/issues/gh-190/plusblog.html",
"real-world/issues/gh-190/rbloggers.html",
"real-world/issues/gh-190/ozonekorea.html",
"real-world/issues/gh-190/firsteigen.html",
"real-world/issues/gh-190/sjsu.html",
"real-world/issues/gh-190/kimbrain.html",
"real-world/issues/gh-121-hacker-news.html",
"real-world/issues/gh-127-issue.html",
"synthetic/optional_li.html",
"synthetic/table_no_tbody.html",
"synthetic/bare_fragment.html",
"synthetic/unescaped_lt.html",
"synthetic/unclosed_p.html",
"synthetic/unclosed_at_eof.html",
"synthetic/cdata_in_svg.html",
];
fn tier2_output(html: &str) -> Option<String> {
let opts = ConversionOptions {
tier_strategy: TierStrategy::Tier2,
extract_metadata: false,
..ConversionOptions::default()
};
let html_owned = html.to_owned();
let result = std::panic::catch_unwind(move || convert(&html_owned, Some(opts)));
match result {
Ok(Ok(r)) => r.content,
_ => None,
}
}
fn force_tier1_output(html: &str) -> Option<String> {
let opts = ConversionOptions {
tier_strategy: TierStrategy::Tier1,
extract_metadata: false,
..ConversionOptions::default()
};
let html_owned = html.to_owned();
let result = std::panic::catch_unwind(move || convert(&html_owned, Some(opts)));
match result {
Ok(Ok(r)) => r.content,
_ => None,
}
}
const KNOWN_TIER2_QUIRK_FIXTURES: &[&str] = &["mdream/nuxt-example.html"];
#[test]
fn tier1_byte_equality_against_all_fixtures() {
let fixtures_root = Path::new(FIXTURES_ROOT);
let mut total = 0usize;
let mut skipped = 0usize;
let mut failures: Vec<String> = Vec::new();
for rel_path in FIXTURE_PATHS {
total += 1;
if KNOWN_TIER2_QUIRK_FIXTURES.contains(rel_path) {
eprintln!("SKIP {rel_path}: known Tier-2 trailing-whitespace quirk");
skipped += 1;
continue;
}
let full_path = fixtures_root.join(rel_path);
let Ok(html) = fs::read_to_string(&full_path) else {
eprintln!("SKIP {rel_path}: cannot read");
skipped += 1;
continue;
};
let Some(t2) = tier2_output(&html) else {
eprintln!("SKIP {rel_path}: Tier-2 panicked or failed");
skipped += 1;
continue;
};
let Some(t1) = force_tier1_output(&html) else {
eprintln!("SKIP {rel_path}: Tier1 panicked");
skipped += 1;
continue;
};
if t1 != t2 {
let diff = first_diff(&t2, &t1);
let msg = format!(
"DIVERGE {rel_path}:\n Tier-2: {:?}\n Tier-1: {:?}\n diff: {diff}",
&t2[..t2.len().min(200)],
&t1[..t1.len().min(200)],
);
eprintln!("{msg}");
failures.push(msg);
}
}
eprintln!(
"\ntier1_byte_equality: total={total} skipped={skipped} failures={}",
failures.len()
);
let nfail = failures.len();
assert!(
failures.is_empty(),
"{nfail} fixture(s) diverged from Tier-2:\n{}",
failures.join("\n\n")
);
}
#[test]
fn tier1_bail_survey() {
let fixtures_root = Path::new(FIXTURES_ROOT);
let mut handled = 0usize;
let mut bailed = 0usize;
let mut skipped = 0usize;
for rel_path in FIXTURE_PATHS {
let full_path = fixtures_root.join(rel_path);
let Ok(html) = fs::read_to_string(&full_path) else {
skipped += 1;
continue;
};
let Some(t2) = tier2_output(&html) else {
skipped += 1;
continue;
};
let Some(t1) = force_tier1_output(&html) else {
skipped += 1;
continue;
};
if t1 == t2 {
handled += 1;
} else {
eprintln!("DIVERGE {rel_path}");
bailed += 1;
}
}
eprintln!("\ntier1_bail_survey: equal={handled} diverged={bailed} skipped={skipped}");
}
fn first_diff(expected: &str, actual: &str) -> String {
let exp: Vec<&str> = expected.lines().collect();
let act: Vec<&str> = actual.lines().collect();
for (i, (e, a)) in exp.iter().zip(act.iter()).enumerate() {
if e != a {
return format!("line {}: expected {:?} actual {:?}", i + 1, e, a);
}
}
format!("line count differs: expected {} actual {}", exp.len(), act.len())
}