#![cfg(feature = "presets")]
use std::fs;
use std::path::PathBuf;
use serde_json::Value;
use wikioxide::{DocumentView, PageContext, ParsedDocument, SiteConfig, WikitextParser};
fn fixtures_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/checklist")
}
#[test]
fn checklist_fixtures_match_expectations() {
let mut names: Vec<String> = fs::read_dir(fixtures_dir())
.expect("fixtures/checklist must exist")
.filter_map(|e| {
let name = e.ok()?.file_name().into_string().ok()?;
name.strip_suffix(".wikitext").map(str::to_owned)
})
.collect();
names.sort();
assert!(
names.len() >= 30,
"expected ≥30 fixtures, found {}",
names.len()
);
let mut failures = Vec::new();
for name in &names {
let wikitext = fs::read_to_string(fixtures_dir().join(format!("{name}.wikitext")))
.expect("fixture wikitext readable");
let expected: Value = serde_json::from_str(
&fs::read_to_string(fixtures_dir().join(format!("{name}.expected.json")))
.expect("expected.json readable"),
)
.expect("expected.json valid");
let config = match expected["config"].as_str() {
Some("ruwiki") => SiteConfig::ruwiki(),
_ => SiteConfig::enwiki(),
};
let parser = WikitextParser::builder()
.site_config(config.clone())
.build()
.expect("parser builds");
let doc = parser.parse(&wikitext, &PageContext::default());
check_fixture(name, &doc, &expected, &mut failures);
}
assert!(
failures.is_empty(),
"{} checklist mismatches:\n{}",
failures.len(),
failures.join("\n")
);
}
fn check_fixture(name: &str, doc: &ParsedDocument, expected: &Value, failures: &mut Vec<String>) {
let mut fail = |key: &str, detail: String| {
failures.push(format!("[{name}] {key}: {detail}"));
};
if let Some(links) = expected["links"].as_array() {
let got: Vec<(String, Option<String>, Option<String>)> = doc
.links()
.iter()
.map(|l| {
(
l.target().text(),
l.fragment().map(str::to_owned),
l.anchor_text().map(str::to_owned),
)
})
.collect();
let want: Vec<(String, Option<String>, Option<String>)> = links
.iter()
.map(|l| {
(
l["text"].as_str().unwrap_or_default().to_owned(),
l["fragment"].as_str().map(str::to_owned),
l["anchor"].as_str().map(str::to_owned),
)
})
.collect();
if got != want {
fail("links", format!("want {want:?}, got {got:?}"));
}
}
if let Some(cats) = expected["categories"].as_array() {
let got: Vec<(String, String)> = doc
.categories()
.iter()
.map(|c| (c.category().text(), c.sortkey().to_owned()))
.collect();
let want: Vec<(String, String)> = cats
.iter()
.map(|c| {
(
c["text"].as_str().unwrap_or_default().to_owned(),
c["sortkey"].as_str().unwrap_or_default().to_owned(),
)
})
.collect();
if got != want {
fail("categories", format!("want {want:?}, got {got:?}"));
}
}
if let Some(inter) = expected["interlanguage"].as_array() {
let got: Vec<(String, String)> = doc
.interlanguage_links()
.iter()
.map(|l| (l.prefix().to_owned(), l.target().to_owned()))
.collect();
let want: Vec<(String, String)> = inter
.iter()
.map(|l| {
(
l["prefix"].as_str().unwrap_or_default().to_owned(),
l["target"].as_str().unwrap_or_default().to_owned(),
)
})
.collect();
if got != want {
fail("interlanguage", format!("want {want:?}, got {got:?}"));
}
}
if let Some(ext) = expected["external_links"].as_array() {
let got: Vec<&str> = doc.external_links().iter().map(|e| e.url()).collect();
let want: Vec<&str> = ext.iter().filter_map(Value::as_str).collect();
if got != want {
fail("external_links", format!("want {want:?}, got {got:?}"));
}
}
if let Some(images) = expected["images"].as_u64() {
let got = u64::from(doc.stats().images());
if got != images {
fail("images", format!("want {images}, got {got}"));
}
}
if let Some(templates) = expected["templates"].as_array() {
if doc.templates().len() != templates.len() {
fail(
"templates.len",
format!(
"want {}, got {} ({:?})",
templates.len(),
doc.templates().len(),
doc.templates()
.iter()
.map(|t| t.title().text())
.collect::<Vec<_>>()
),
);
} else {
for (i, want) in templates.iter().enumerate() {
let got = &doc.templates()[i];
if let Some(title) = want["title"].as_str() {
if got.title().text() != title {
fail(
&format!("templates[{i}].title"),
format!("want {title:?}, got {:?}", got.title().text()),
);
}
}
if let Some(raw) = want["name_raw"].as_str() {
if got.name_raw() != raw {
fail(
&format!("templates[{i}].name_raw"),
format!("want {raw:?}, got {:?}", got.name_raw()),
);
}
}
if let Some(params) = want["params"].as_object() {
let got_params: Vec<(&str, &str)> = got
.params()
.iter()
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect();
let mut want_params: Vec<(&str, &str)> = params
.iter()
.map(|(k, v)| (k.as_str(), v.as_str().unwrap_or_default()))
.collect();
want_params.sort();
if got_params != want_params {
fail(
&format!("templates[{i}].params"),
format!("want {want_params:?}, got {got_params:?}"),
);
}
}
}
}
}
if let Some(modules) = expected["modules"].as_array() {
let got: Vec<(String, String)> = doc
.modules()
.iter()
.map(|m| (m.module().text(), m.function().to_owned()))
.collect();
let want: Vec<(String, String)> = modules
.iter()
.map(|m| {
(
m["module"].as_str().unwrap_or_default().to_owned(),
m["function"].as_str().unwrap_or_default().to_owned(),
)
})
.collect();
if got != want {
fail("modules", format!("want {want:?}, got {got:?}"));
}
}
match &expected["redirect"] {
Value::Null => {
if expected.get("redirect").is_some() && doc.redirect().is_some() {
fail("redirect", format!("want none, got {:?}", doc.redirect()));
}
}
Value::Object(want) => match doc.redirect() {
None => fail("redirect", "want some, got none".to_owned()),
Some(got) => {
if Some(got.target().text().as_str()) != want["target"].as_str() {
fail(
"redirect.target",
format!("want {:?}, got {:?}", want["target"], got.target().text()),
);
}
if got.fragment() != want["fragment"].as_str() {
fail(
"redirect.fragment",
format!("want {:?}, got {:?}", want["fragment"], got.fragment()),
);
}
}
},
_ => {}
}
if let Some(stats) = expected["stats"].as_object() {
for (key, want) in stats {
let want = want.as_u64().unwrap_or_default();
let got = u64::from(match key.as_str() {
"tables" => doc.stats().tables(),
"table_rows" => doc.stats().table_rows(),
"table_cells" => doc.stats().table_cells(),
"headings" => doc.stats().headings(),
"refs" => doc.stats().refs(),
"list_items" => doc.stats().list_items(),
"images" => doc.stats().images(),
"internal_links" => doc.stats().internal_links(),
"external_links" => doc.stats().external_links(),
"parser_errors" => doc.stats().parser_errors(),
other => {
fail("stats", format!("unknown stats key {other:?}"));
continue;
}
});
if got != want {
fail(&format!("stats.{key}"), format!("want {want}, got {got}"));
}
}
}
if let Some(subs) = expected["warnings_contains"].as_array() {
let rendered: Vec<String> = doc.warnings().iter().map(ToString::to_string).collect();
for sub in subs.iter().filter_map(Value::as_str) {
if !rendered.iter().any(|w| w.contains(sub)) {
fail(
"warnings_contains",
format!("no warning contains {sub:?}; warnings: {rendered:?}"),
);
}
}
}
if let Some(subs) = expected["plaintext_contains"].as_array() {
for sub in subs.iter().filter_map(Value::as_str) {
if !doc.plain_text().contains(sub) {
fail(
"plaintext_contains",
format!("plaintext lacks {sub:?}; got: {:?}", doc.plain_text()),
);
}
}
}
}