Load an existing committed fixture through python-pptx, save it to a temp path, then re-assert the primary structural markers are still present. Catches load/save drops — e.g. a parser forgetting to re-emit a slide element, a rel being lost, a run collapsing. Covers eight fixtures chosen to span slide-count, shape types, media, charts, notes, and bullet / numbered lists.
| Library | Pass | Fail | Pending |
|---|---|---|---|
python-pptx | 0 | 0 | 8 |
pptxjs | 0 | 0 | 8 |
case (8 values): bullet-list, bullet-numbered, multiple-slides, slide-with-subtitle, slide-notes, textbox-plain, shape-oval, shape-rectanglescripts/gen_round_trip_identity_pptx.py — runs with --arg_template --source fixtures/{case.source}.pptx --out fixtures/pptx/round-trip-identity--{case.id}.pptx
#!/usr/bin/env python3
"""Generate a pptx round-trip fixture by loading a source via python-pptx and
saving the result to the named output path. Invoked per expanded case by the
corpus generator driver.
Usage::
python scripts/gen_round_trip_identity_pptx.py \
--source fixtures/pptx/bullet-list.pptx \
--out fixtures/pptx/round-trip-identity--bullet-list.pptx
The output is what python-pptx produces after load+save. The associated
feature manifest's assertions then run against this artefact to verify
structural preservation.
"""
from __future__ import annotations
import argparse
import zipfile
from pathlib import Path
from pptx import Presentation # type: ignore[import-not-found]
def _rezip_with_epoch_timestamps(path: Path) -> None:
"""Rewrite ``path`` so every member carries the (1980, 1, 1, 0, 0, 0)
timestamp the corpus mandates for reproducible fixtures.
"""
tmp = path.with_suffix(path.suffix + ".tmp")
with zipfile.ZipFile(path, "r") as src, zipfile.ZipFile(
tmp, "w", zipfile.ZIP_DEFLATED
) as dst:
for info in src.infolist():
data = src.read(info.filename)
info.date_time = (1980, 1, 1, 0, 0, 0)
dst.writestr(info, data)
tmp.replace(path)
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--source", required=True)
ap.add_argument("--out", required=True)
args = ap.parse_args()
out = Path(args.out)
out.parent.mkdir(parents=True, exist_ok=True)
Presentation(args.source).save(str(out))
_rezip_with_epoch_timestamps(out)
if __name__ == "__main__":
main()
{
"$schema": "../manifest.schema.json",
"id": "pptx/round-trip-identity",
"kind": "parameterised",
"title": "Round-trip preserves core content (pptx)",
"format": "pptx",
"category": "round-trip",
"summary": "Load an existing committed fixture through python-pptx, save it to a temp path, then re-assert the primary structural markers are still present. Catches load/save drops — e.g. a parser forgetting to re-emit a slide element, a rel being lost, a run collapsing. Covers eight fixtures chosen to span slide-count, shape types, media, charts, notes, and bullet / numbered lists.",
"spec": {
"source": "ecma-376-5-part-1",
"clause": "19",
"element": "p:sld",
"xsd_reference": "spec/ecma-376-5/part-1/xsd/pml.xsd",
"notes": "No schema element is unique to round-trip — we borrow the pml.xsd reference because p:sld is the primary artefact inspected. The round_trip block's library is python-pptx; source_fixture varies by parameter axis."
},
"fixtures": {
"machine": "pptx/round-trip-identity"
},
"round_trip": {
"library": "python-pptx",
"source_fixture": "{case.source}",
"notes": "source_fixture is templated — the runner expands {case.source} per case so each loads its own committed fixture."
},
"parameters": {
"case": [
{
"id": "bullet-list",
"source": "pptx/bullet-list",
"slides": "1",
"text": "Item one"
},
{
"id": "bullet-numbered",
"source": "pptx/bullet-numbered",
"slides": "1",
"text": "One"
},
{
"id": "multiple-slides",
"source": "pptx/multiple-slides",
"slides": "3",
"text": "Slide one title"
},
{
"id": "slide-with-subtitle",
"source": "pptx/slide-with-subtitle",
"slides": "1",
"text": "Title with subtitle"
},
{
"id": "slide-notes",
"source": "pptx/slide-notes",
"slides": "1",
"text": "Slide with notes"
},
{
"id": "textbox-plain",
"source": "pptx/textbox-plain",
"slides": "1",
"text": "Plain text box"
},
{
"id": "shape-oval",
"source": "pptx/shape-oval",
"slides": "1",
"text": "Oval shape"
},
{
"id": "shape-rectangle",
"source": "pptx/shape-rectangle",
"slides": "1",
"text": "Rectangle shape"
}
]
},
"generator": {
"python": "scripts/gen_round_trip_identity_pptx.py",
"arg_template": "--source fixtures/{case.source}.pptx --out fixtures/pptx/round-trip-identity--{case.id}.pptx"
},
"assertions_template": [
{
"id": "slide-1-present-after-round-trip",
"part": "ppt/slides/slide1.xml",
"namespaces": {
"p": "http://schemas.openxmlformats.org/presentationml/2006/main",
"a": "http://schemas.openxmlformats.org/drawingml/2006/main"
},
"xpath": "//p:sld",
"must": "exist",
"description": "After the round trip, slide1 must still parse and carry a <p:sld> root."
},
{
"id": "presentation-lists-slide",
"part": "ppt/presentation.xml",
"namespaces": {
"p": "http://schemas.openxmlformats.org/presentationml/2006/main",
"r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
},
"xpath": "//p:sldIdLst/p:sldId[1]",
"must": "exist",
"description": "The sldIdLst must still register at least one slide after the round trip."
},
{
"id": "expected-text-preserved",
"part": "ppt/slides/slide1.xml",
"namespaces": {
"a": "http://schemas.openxmlformats.org/drawingml/2006/main"
},
"xpath": "//a:t[normalize-space()='{case.text}']",
"must": "exist",
"description": "A representative text run from the original fixture must survive the round-trip unchanged."
}
]
}