gedcomkit 0.1.11

A byte-preserving GEDCOM document model: decoding, parsing, readings, version conversion, plausibility checks, and the GEDZIP container, for GEDCOM 5.5 through 7.x.
Documentation
//! The requirement corpus, read end to end.
//!
//! Two private files are an optional local acceptance target: a GEDCOM export
//! and a GEDZIP holding the same document with its media. They live outside
//! this repository and are never copied into it, so this test reads them from
//! paths supplied by the environment and skips when it cannot:
//!
//! ```powershell
//! $env:GEDCOMKIT_CORPUS_GED  = "C:\path\to\private-corpus.ged"
//! $env:GEDCOMKIT_CORPUS_GEDZ = "C:\path\to\private-corpus.gedz"
//! cargo test -p gedcomkit --test requirement_corpus -- --nocapture
//! ```
//!
//! Skipping is deliberate and is not a pass. The release/test matrix is
//! maintained with the root release checklist, and a run that skipped must be
//! recorded as skipped.

#[cfg(feature = "gedzip")]
use gedcomkit::gedzip::Archive;
use gedcomkit::{Document, decode_gedcom};
use std::path::PathBuf;

fn corpus_path(variable: &str) -> Option<PathBuf> {
    let path = PathBuf::from(std::env::var_os(variable)?);
    path.is_file().then_some(path)
}

#[test]
fn the_requirement_gedcom_reads_and_writes_back_byte_for_byte() {
    let Some(path) = corpus_path("GEDCOMKIT_CORPUS_GED") else {
        eprintln!("skipped: set GEDCOMKIT_CORPUS_GED to the requirement corpus document");
        return;
    };

    let bytes = std::fs::read(&path).expect("read the corpus document");
    let (text, encoding) = decode_gedcom(&bytes).expect("decode the corpus document");
    let document = Document::parse(&text).expect("parse the corpus document");

    let individuals = document.records_of("INDI").count();
    let families = document.records_of("FAM").count();
    let sources = document.records_of("SOUR").count();
    let media = document.records_of("OBJE").count();
    eprintln!(
        "{}: {} lines, {individuals} individuals, {families} families, {sources} sources, \
         {media} media objects, {}",
        path.display(),
        document.line_count(),
        encoding.summary()
    );

    assert_eq!(
        document.to_text(),
        text,
        "the document did not write back byte for byte"
    );
    assert!(individuals > 0, "expected at least one individual");
    assert_eq!(document.version().as_deref(), Some("5.5.1"));

    let dangling = document.unresolved_pointers();
    assert!(
        dangling.is_empty(),
        "{} pointers do not resolve, first is {:?}",
        dangling.len(),
        dangling.first()
    );
}

#[cfg(feature = "gedzip")]
#[test]
fn the_requirement_archive_opens_and_its_document_reads_back_byte_for_byte() {
    let Some(path) = corpus_path("GEDCOMKIT_CORPUS_GEDZ") else {
        eprintln!("skipped: set GEDCOMKIT_CORPUS_GEDZ to the requirement corpus archive");
        return;
    };

    let bytes = std::fs::read(&path).expect("read the corpus archive");
    let archive = Archive::open(&bytes).expect("open the corpus archive");
    let name = archive
        .document_name()
        .expect("find the document")
        .to_owned();
    eprintln!(
        "{}: {} entries, document is {name}",
        path.display(),
        archive.entries().len()
    );

    let document_bytes = archive.read(&name).expect("read the document");
    let (text, _) = decode_gedcom(&document_bytes).expect("decode the document");
    let document = Document::parse(&text).expect("parse the document");
    assert_eq!(document.to_text(), text);

    // Every media entry must decompress and pass its checksum, which is what
    // proves the container reader rather than only the directory reader.
    let mut media_bytes = 0usize;
    for entry in archive.entries() {
        if entry.is_directory || entry.name == name {
            continue;
        }
        let content = archive
            .read_entry(entry)
            .unwrap_or_else(|failure| panic!("{}: {failure}", entry.name));
        media_bytes += content.len();
    }
    eprintln!("{media_bytes} bytes of media read and checksummed");
    assert!(media_bytes > 0, "the archive carried no media");
}

/// The checks, run against a real file, which is the only way to know whether
/// they say anything useful.
///
/// A check that fires on every third person is noise, and a check that never
/// fires is decoration. This reports what each one found so the thresholds can
/// be argued with, and asserts only the two things that must hold: it is fast,
/// and it does not accuse most of the file.
#[test]
fn the_checks_say_something_useful_about_the_requirement_corpus() {
    let Some(path) = corpus_path("GEDCOMKIT_CORPUS_GED") else {
        eprintln!("skipped: set GEDCOMKIT_CORPUS_GED to the requirement corpus document");
        return;
    };
    let bytes = std::fs::read(&path).expect("read the corpus document");
    let (text, _) = decode_gedcom(&bytes).expect("decode the corpus document");
    let document = Document::parse(&text).expect("parse the corpus document");
    let people = document.records_of("INDI").count();

    let started = std::time::Instant::now();
    let found = gedcomkit::validate::inspect(&document);
    let took = started.elapsed();

    let mut counted: std::collections::BTreeMap<&str, usize> = std::collections::BTreeMap::new();
    for finding in &found {
        *counted.entry(finding.check).or_default() += 1;
    }
    eprintln!(
        "checks: {} findings over {people} people in {took:?}",
        found.len()
    );
    for (check, count) in &counted {
        eprintln!("  {check:>28}: {count}");
    }
    // The wording of the checks that fire rarely, which are the ones somebody
    // will actually read. Sampling the list in order would show six copies of
    // whichever check fires most.
    let mut shown: std::collections::BTreeSet<&str> = std::collections::BTreeSet::new();
    for finding in &found {
        if finding.severity != gedcomkit::validate::Severity::Missing && shown.insert(finding.check)
        {
            eprintln!("  e.g. {} — {}", finding.record, finding.message);
        }
    }

    assert!(
        took.as_millis() < 2_000,
        "inspecting the file took {took:?}, which is on the path between a click and a report"
    );

    // Contradictions are the ones a person acts on. A real file has some; a
    // file where a fifth of the people contradict themselves means the check
    // is wrong, not the file.
    let contradictions = found
        .iter()
        .filter(|finding| finding.severity == gedcomkit::validate::Severity::Contradiction)
        .count();
    assert!(
        contradictions * 5 < people,
        "{contradictions} contradictions among {people} people is a broken check, not a broken file"
    );
}