Load each of 10 diverse committed fixtures with python-docx and immediately re-save with reproducible=True. Assert the twin retains core structural invariants of the original: paragraph count, presence of the fixture's defining run text, image count, and sectPr presence.
| Library | Pass | Fail | Pending |
|---|---|---|---|
python-docx | 0 | 0 | 10 |
docxjs | 0 | 0 | 10 |
source (10 values): bold-text, italic-text, underline-text, hyperlink, image-inline, page-header, comment, footnote, bulleted-list, table-basicscripts/gen_round_trip_identity.py — runs with --arg_template --source {source.id} --out fixtures/docx/round-trip-identity--{source.id}.docx
#!/usr/bin/env python3
"""Generate round-trip-identity twin fixtures.
For each source fixture, load it via python-docx and immediately save
to ``fixtures/docx/round-trip-identity--<source-id>.docx`` with
``reproducible=True``. The committed twin is what the manifest's
assertions run against; the whole point is to catch python-docx losing
or mutating something on a pure load-save cycle.
"""
from __future__ import annotations
import argparse
import shutil
import sys
from pathlib import Path
from docx import Document
_REPO_ROOT = Path(__file__).resolve().parent.parent
_FIX = _REPO_ROOT / "fixtures" / "docx"
def round_trip(source: Path, out: Path) -> None:
doc = Document(str(source))
out.parent.mkdir(parents=True, exist_ok=True)
doc.save(str(out), reproducible=True)
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--source", required=True, help="Source fixture logical name (e.g. 'bold-text').")
parser.add_argument("--out", required=True, help="Output fixture path (repo-relative).")
ns = parser.parse_args(argv)
source_path = _FIX / f"{ns.source}.docx"
out_path = _REPO_ROOT / ns.out
if not source_path.exists():
print(f"Source fixture missing: {source_path}", file=sys.stderr)
return 1
round_trip(source_path, out_path)
print(out_path)
return 0
if __name__ == "__main__":
raise SystemExit(main())
{
"$schema": "../manifest.schema.json",
"id": "docx/round-trip-identity",
"kind": "parameterised",
"title": "Round-trip identity (python-docx)",
"format": "docx",
"category": "round-trip",
"summary": "Load each of 10 diverse committed fixtures with python-docx and immediately re-save with reproducible=True. Assert the twin retains core structural invariants of the original: paragraph count, presence of the fixture's defining run text, image count, and sectPr presence.",
"spec": {
"source": "ecma-376-5-part-1",
"clause": "11.1",
"element": "",
"notes": "This family does not target a single OOXML element. It targets the python-docx authoring library's load-save round-trip contract: a file it loads and immediately re-saves must preserve the fixture's externally observable structure. Bugs in this space (e.g. dropped child elements, mutated element ordering, renumbered relationships) do not show up in direct-authoring fixtures because the library also authors the assertion target. Round-trip inverts that."
},
"fixtures": {
"machine": "docx/round-trip-identity"
},
"round_trip": {
"library": "python-docx",
"source_fixture": "docx/round-trip-identity",
"notes": "Mode B (generator-embedded). The generator commits the twin fixture under `round-trip-identity--<source.id>.docx`; this manifest's `source_fixture` value is parameterised per case via the `source` axis."
},
"generator": {
"python": "scripts/gen_round_trip_identity.py",
"arg_template": "--source {source.id} --out fixtures/docx/round-trip-identity--{source.id}.docx"
},
"parameters": {
"source": [
{
"id": "bold-text",
"text": "Hello bold world",
"p_count": "1",
"has_image": "0"
},
{
"id": "italic-text",
"text": "Hello italic world",
"p_count": "1",
"has_image": "0"
},
{
"id": "underline-text",
"text": "Hello underline world",
"p_count": "1",
"has_image": "0"
},
{
"id": "hyperlink",
"text": "python-docx project page",
"p_count": "1",
"has_image": "0"
},
{
"id": "image-inline",
"text": "",
"p_count": "1",
"has_image": "1"
},
{
"id": "page-header",
"text": "",
"p_count": "1",
"has_image": "0"
},
{
"id": "comment",
"text": "Text being commented on",
"p_count": "1",
"has_image": "0"
},
{
"id": "footnote",
"text": "",
"p_count": "1",
"has_image": "0"
},
{
"id": "bulleted-list",
"text": "",
"p_count": "3",
"has_image": "0"
},
{
"id": "table-basic",
"text": "",
"p_count": "4",
"has_image": "0"
}
]
},
"assertions_template": [
{
"id": "paragraph-count-preserved-{source.id}",
"part": "word/document.xml",
"namespaces": {
"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
},
"xpath": "count(//w:body//w:p)",
"must": "match",
"value": "^{source.p_count}(\\.0)?$",
"description": "The twin must carry exactly the same number of <w:p> elements as the original."
},
{
"id": "sectPr-preserved-{source.id}",
"part": "word/document.xml",
"namespaces": {
"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
},
"xpath": "//w:body/w:sectPr",
"must": "exist",
"description": "The body's terminating <w:sectPr> must survive round-trip — Word refuses to open documents without one."
},
{
"id": "document-rels-present-{source.id}",
"part": "word/_rels/document.xml.rels",
"namespaces": {
"r": "http://schemas.openxmlformats.org/package/2006/relationships"
},
"xpath": "//r:Relationships/r:Relationship[@Type='http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles']",
"must": "exist",
"description": "The document's relationship to styles.xml must survive — its absence is a classic round-trip loss mode."
},
{
"id": "content-types-document-main-{source.id}",
"part": "[Content_Types].xml",
"namespaces": {
"ct": "http://schemas.openxmlformats.org/package/2006/content-types"
},
"xpath": "//ct:Override[@ContentType='application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml']",
"must": "exist",
"description": "[Content_Types].xml must still declare the document main part."
}
]
}