docspec-docx-reader 1.21.6

DOCX to DocSpec event stream reader
Documentation
//! Memory-overhead invariant tests for DOCX streaming.
//!
//! These tests verify that `DocxReader::from_path` adds bounded overhead regardless
//! of `document.xml` size. They are marked `#[ignore]` because they are slow and
//! should not run in normal CI.
//!
//! The tests spawn a prebuilt release example, so build it first:
//!
//! ```text
//! cargo build --release --example memtest_child -p docspec-docx-reader
//! cargo test -p docspec-docx-reader --test memory -- --ignored --nocapture
//! ```
#![allow(
    clippy::arithmetic_side_effects,
    clippy::expect_used,
    clippy::indexing_slicing,
    clippy::integer_division,
    clippy::print_stderr,
    clippy::tests_outside_test_module,
    clippy::unwrap_used
)]

#[cfg(target_os = "linux")]
mod linux_only {
    use std::io::Write as _;

    fn synth_large_docx(doc_xml_size_mb: usize) -> Vec<u8> {
        let target_bytes = doc_xml_size_mb * 1024 * 1024;

        let header = r#"<?xml version="1.0" encoding="UTF-8"?>
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body>"#;

        let para = "<w:p><w:r><w:t>test paragraph content for memory measurement</w:t></w:r></w:p>";
        let footer = "</w:body></w:document>";

        let para_bytes = para.len();
        let overhead = header.len() + footer.len();
        let n_paras = (target_bytes.saturating_sub(overhead)) / para_bytes + 1;

        let mut doc_xml = String::with_capacity(target_bytes + 1024);
        doc_xml.push_str(header);
        for _ in 0..n_paras {
            doc_xml.push_str(para);
        }
        doc_xml.push_str(footer);

        let rels_xml = r#"<?xml version="1.0"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/></Relationships>"#;

        docspec_test_utils::synth_docx_with_entries(&[
            (
                "_rels/.rels",
                zip::CompressionMethod::Stored,
                rels_xml.as_bytes(),
            ),
            (
                "word/document.xml",
                zip::CompressionMethod::Stored,
                doc_xml.as_bytes(),
            ),
        ])
    }

    /// Cargo's target directory, always absolute.
    ///
    /// `CARGO_TARGET_TMPDIR` is set at compile time for integration tests to
    /// `<target-dir>/tmp`, already resolved by Cargo. Reading `CARGO_TARGET_DIR`
    /// at runtime instead would break on relative values: Cargo resolves those
    /// against its invocation directory, while the test process runs from the
    /// package root.
    fn target_dir() -> std::path::PathBuf {
        std::path::Path::new(env!("CARGO_TARGET_TMPDIR"))
            .parent()
            .expect("CARGO_TARGET_TMPDIR is <target-dir>/tmp and always has a parent")
            .to_path_buf()
    }

    fn child_bin_path() -> std::path::PathBuf {
        target_dir().join("release/examples/memtest_child")
    }

    fn run_child_and_get_peak_rss(docx_path: &std::path::Path) -> u64 {
        let bin = child_bin_path();
        assert!(
            bin.exists(),
            "memtest_child binary not found at {}. Run: cargo build --release --example memtest_child -p docspec-docx-reader",
            bin.display()
        );

        let output = std::process::Command::new(&bin)
            .arg(docx_path)
            .output()
            .expect("failed to spawn memtest_child");

        let stderr = String::from_utf8_lossy(&output.stderr);
        assert!(output.status.success(), "memtest_child failed:\n{stderr}");

        stderr
            .lines()
            .find_map(|line| {
                let rest = line.strip_prefix("PEAK_RSS_KB=")?;
                rest.trim().parse::<u64>().ok()
            })
            .expect("PEAK_RSS_KB line must be present in child stderr")
    }

    /// Verify that streaming via `from_path` adds bounded overhead (< 80 MB peak RSS)
    /// even when processing a 50 MB `word/document.xml`.
    ///
    /// Proof: if buffering were still in effect, the child would need 50 MB just for
    /// the `document.xml` buffer, pushing it over the 80 MB threshold.
    ///
    /// Prerequisite: `cargo build --release --example memtest_child -p docspec-docx-reader`.
    #[test]
    #[ignore = "slow memory test, run manually after building memtest_child"]
    fn streaming_doc_xml_bounded_overhead() {
        let bytes = synth_large_docx(50);
        let mut tmp = tempfile::NamedTempFile::new().expect("tempfile");
        tmp.write_all(&bytes).expect("write docx");
        tmp.flush().expect("flush");
        drop(bytes);

        let rss_kib = run_child_and_get_peak_rss(tmp.path());
        let peak_mebibytes = rss_kib / 1024;
        eprintln!("50 MB doc.xml: child peak RSS = {peak_mebibytes} MB ({rss_kib} kB)");

        assert!(
            rss_kib < 80_000,
            "Peak RSS {peak_mebibytes} MB exceeds 80 MB budget for 50 MB document.xml (streaming leak!)"
        );
        eprintln!("Memory budget OK: {peak_mebibytes} MB < 80 MB");
    }

    /// Verify that memory usage stays flat as `document.xml` grows from 50 MB to 200 MB.
    /// Both tests hitting the same threshold (~10-20 MB) proves O(1) behavior.
    ///
    /// Prerequisite: `cargo build --release --example memtest_child -p docspec-docx-reader`.
    #[test]
    #[ignore = "slow memory test, run manually after building memtest_child"]
    fn streaming_doc_xml_scales_to_huge_docs() {
        let bytes = synth_large_docx(200);
        let mut tmp = tempfile::NamedTempFile::new().expect("tempfile");
        tmp.write_all(&bytes).expect("write docx");
        tmp.flush().expect("flush");
        drop(bytes);

        let rss_kib = run_child_and_get_peak_rss(tmp.path());
        let peak_mebibytes = rss_kib / 1024;
        eprintln!("200 MB doc.xml: child peak RSS = {peak_mebibytes} MB ({rss_kib} kB)");

        assert!(
            rss_kib < 80_000,
            "Peak RSS {peak_mebibytes} MB exceeds 80 MB budget for 200 MB document.xml (memory scales with input = not O(1)!)"
        );
        eprintln!(
            "Memory budget OK: {peak_mebibytes} MB < 80 MB — memory is O(1) regardless of document size"
        );
    }
} // mod linux_only