#![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(),
),
])
}
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")
}
#[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");
}
#[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"
);
}
}