wikioxide 0.1.0

MediaWiki wikitext parser producing typed domain models (templates, links, categories, warnings) with template expansion — no HTML, no network
Documentation
//! Property tests: the parser is total and deterministic over arbitrary input.
#![cfg(feature = "presets")]

use proptest::prelude::*;
use wikioxide::{DocumentView, PageContext, SiteConfig, WikitextParser};

// `allow-expect-in-tests` only covers `#[test]` bodies, not integration-test helpers.
#[allow(clippy::expect_used)]
fn parser() -> WikitextParser {
    WikitextParser::builder()
        .site_config(SiteConfig::ruwiki().clone())
        .build()
        .expect("preset parser builds")
}

proptest! {
    #![proptest_config(ProptestConfig::with_cases(256))]

    /// P1 totality: any string parses without panicking, and markup-heavy alphabets
    /// stress the interesting paths.
    #[test]
    fn parse_never_panics(input in proptest::string::string_regex(
        "[\\{\\}\\[\\]\\|=<>!*#;:'&_ \\n\\-a-zа-яё/~\"]{0,400}"
    ).expect("valid regex")) {
        let doc = parser().parse(&input, &PageContext::default());
        // Touch every accessor so nothing lazy hides a panic.
        let _ = (
            doc.templates().len(),
            doc.links().len(),
            doc.categories().len(),
            doc.external_links().len(),
            doc.warnings().len(),
            doc.stats().tables(),
            doc.plain_text().len(),
            doc.redirect().is_some(),
        );
    }

    /// P1b totality over fully arbitrary unicode (control chars, surrogate-adjacent,
    /// multi-byte) — hygiene must tame everything.
    #[test]
    fn parse_never_panics_on_arbitrary_unicode(input in ".{0,200}") {
        let _ = parser().parse(&input, &PageContext::default());
    }

    /// P3 determinism: parsing the same input twice yields identical extractions.
    #[test]
    fn parse_is_deterministic(input in proptest::string::string_regex(
        "[\\{\\}\\[\\]\\|=' \\na-zа-я]{0,200}"
    ).expect("valid regex")) {
        let ctx = PageContext::default();
        let p = parser();
        let a = p.parse(&input, &ctx);
        let b = p.parse(&input, &ctx);
        prop_assert_eq!(a.templates(), b.templates());
        prop_assert_eq!(a.links(), b.links());
        prop_assert_eq!(a.categories(), b.categories());
        prop_assert_eq!(a.external_links(), b.external_links());
        prop_assert_eq!(a.warnings(), b.warnings());
        prop_assert_eq!(a.plain_text(), b.plain_text());
        prop_assert_eq!(a.stats(), b.stats());
    }
}

/// Instantiates the public generic API surface (`insert`/`extend`/`FromIterator`) in this
/// integration build so its coverage map carries real monomorphizations rather than the
/// "unused generic" placeholder records.
#[test]
fn public_generic_forms_instantiate_and_run() {
    use std::sync::Arc;
    use wikioxide::{ExistenceMap, ExistenceRecord, InMemoryTemplateStore, Title};

    let cfg = SiteConfig::ruwiki().clone();
    let t = |s: &str| Title::parse(s, &cfg).expect("valid title");

    let store = InMemoryTemplateStore::new();
    store.insert(t("Шаблон:Owned"), String::from("owned"));
    store.insert(t("Шаблон:Borrowed"), "borrowed");
    store.insert(t("Шаблон:Arced"), Arc::<str>::from("arced"));
    store.extend([(t("Шаблон:Bulk"), Some(Arc::<str>::from("bulk")))]);
    assert_eq!(store.len(), 4);

    let map: ExistenceMap = [(
        t("Шаблон:Fact"),
        ExistenceRecord {
            exists: true,
            hidden_category: None,
        },
    )]
    .into_iter()
    .collect();
    assert_eq!(map.len(), 1);
}

#[cfg(feature = "serde")]
#[test]
fn title_serialize_instantiates_in_integration_build() {
    use wikioxide::Title;
    let cfg = SiteConfig::ruwiki().clone();
    let title = Title::parse("Шаблон:Пример", &cfg).expect("valid title");
    let json = serde_json::to_string(&title).expect("serialize");
    assert!(json.contains("\"ns\":10"), "{json}");
}