Stress test that writes a document with N body paragraphs and verifies the count survives save+load. Parameterised across N = 100, 1000, 10000 so non-linear blow-ups in the authoring pipeline show up as case-to-case size or time divergence.
| Feature ID | Axis bindings | python-docx | docxjs |
|---|---|---|---|
docx/scale-paragraphs--n100 | size=n100 | — | — |
docx/scale-paragraphs--n1000 | size=n1000 | — | — |
docx/scale-paragraphs--n10000 | size=n10000 | — | — |
| Library | Pass | Fail | Pending |
|---|---|---|---|
python-docx | 0 | 0 | 3 |
docxjs | 0 | 0 | 3 |
size (3 values): n100, n1000, n10000scripts/gen_scale_paragraphs.py — runs with --arg_template --count {size.n} --out fixtures/docx/scale-paragraphs--{size.id}.docx
#!/usr/bin/env python3
"""Generate ``fixtures/docx/scale-paragraphs--<n>.docx``.
Parameterised stress fixture for the ``docx/scale-paragraphs`` family.
Writes a document with ``--count`` body paragraphs, each containing a
single run with a short deterministic text. Designed to expose
non-linear memory or time behaviour in the authoring pipeline.
The assertion against the resulting fixture counts ``<w:p>`` children
under ``<w:body>`` and compares to ``--count``.
Usage::
python scripts/gen_scale_paragraphs.py \
--count 100 \
--out fixtures/docx/scale-paragraphs--n100.docx
"""
from __future__ import annotations
import argparse
from pathlib import Path
from docx import Document
_REPO_ROOT = Path(__file__).resolve().parent.parent
def _write(count: int, out: Path) -> None:
doc = Document()
for i in range(count):
doc.add_paragraph(f"Paragraph {i}")
out.parent.mkdir(parents=True, exist_ok=True)
doc.save(out, reproducible=True)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--count", required=True, type=int, help="Number of body paragraphs.")
parser.add_argument("--out", required=True, help="Output path for the .docx file.")
args = parser.parse_args()
out_path = Path(args.out)
if not out_path.is_absolute():
out_path = _REPO_ROOT / out_path
_write(args.count, out_path)
print(out_path)
return 0
if __name__ == "__main__":
raise SystemExit(main())
{
"$schema": "../manifest.schema.json",
"id": "docx/scale-paragraphs",
"kind": "parameterised",
"title": "Scale: many paragraphs",
"format": "docx",
"category": "scale",
"summary": "Stress test that writes a document with N body paragraphs and verifies the count survives save+load. Parameterised across N = 100, 1000, 10000 so non-linear blow-ups in the authoring pipeline show up as case-to-case size or time divergence.",
"spec": {
"source": "ecma-376-5-part-1",
"clause": "17.2.2",
"element": "w:p",
"rnc_reference": "spec/ecma-376-5/part-1/rnc/WordprocessingML.rnc",
"xsd_reference": "spec/ecma-376-5/part-1/xsd/wml.xsd",
"notes": "Real-world documents routinely carry thousands of paragraphs. Minimal fixtures hide three classes of bugs: (a) per-paragraph O(N) scans inside the save path that compound to O(N^2) wall time, (b) id-generator wrap or collision past 65536, and (c) file-size explosions from per-paragraph rsid bookkeeping. This family exists to surface all three as visible case-to-case deltas in the recorded performance baseline and as hard failures when save+reopen round-trip loses any paragraph."
},
"fixtures": {
"machine": "docx/scale-paragraphs"
},
"generator": {
"python": "scripts/gen_scale_paragraphs.py",
"arg_template": "--count {size.n} --out fixtures/docx/scale-paragraphs--{size.id}.docx"
},
"parameters": {
"size": [
{
"id": "n100",
"n": "100"
},
{
"id": "n1000",
"n": "1000"
},
{
"id": "n10000",
"n": "10000"
}
]
},
"assertions_template": [
{
"id": "paragraph-count-{size.id}",
"part": "word/document.xml",
"namespaces": {
"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
},
"xpath": "string(count(/w:document/w:body/w:p))",
"must": "equal",
"value": "{size.n}",
"description": "The body must contain exactly N direct <w:p> children, matching the requested paragraph count."
},
{
"id": "first-paragraph-text-present-{size.id}",
"part": "word/document.xml",
"namespaces": {
"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
},
"xpath": "//w:t[normalize-space()='Paragraph 0']",
"must": "exist",
"description": "The first paragraph ('Paragraph 0') must be present, guarding against a truncation that loses the head of the stream."
}
],
"meta": {
"performance": {
"recorded_on": "2026-05-05",
"notes": "Save-time wall clock and on-disk size recorded on the authoring machine; not asserted, baseline only.",
"cases": [
{
"id": "n100",
"save_seconds": 0.23,
"file_bytes": 20716
},
{
"id": "n1000",
"save_seconds": 0.3,
"file_bytes": 35096
},
{
"id": "n10000",
"save_seconds": 1.48,
"file_bytes": 177410
}
]
}
}
}