pptx/round-trip-identity--slide-notes

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 verdicts: python-pptx: — pptxjs: —

Metadata

Feature idpptx/round-trip-identity--slide-notes
Formatpptx
Categoryround-trip
Familypptx/round-trip-identity
Axis valuescase=slide-notes
Spececma-376-5-part-1 § 19 p:sld

XPath assertions

ID / partPredicateXPathpython-pptxpptxjs
slide-1-present-after-round-trip
ppt/slides/slide1.xml
exist
After the round trip, slide1 must still parse and carry a <p:sld> root.
//p:sld
presentation-lists-slide
ppt/presentation.xml
exist
The sldIdLst must still register at least one slide after the round trip.
//p:sldIdLst/p:sldId[1]
expected-text-preserved
ppt/slides/slide1.xml
exist
A representative text run from the original fixture must survive the round-trip unchanged.
//a:t[normalize-space()='Slide with notes']

Render assertions

No render assertions declared.

Generator source

scripts/gen_round_trip_identity_pptx.py

#!/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()

Manifest (expanded)

{
  "$schema": "../manifest.schema.json",
  "id": "pptx/round-trip-identity--slide-notes",
  "kind": "literal",
  "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--slide-notes"
  },
  "round_trip": {
    "library": "python-pptx",
    "source_fixture": "pptx/slide-notes",
    "notes": "source_fixture is templated — the runner expands pptx/slide-notes per case so each loads its own committed fixture."
  },
  "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"
  },
  "_expansion": {
    "parent_id": "pptx/round-trip-identity",
    "bindings": {
      "case": "slide-notes"
    }
  },
  "assertions": [
    {
      "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()='Slide with notes']",
      "must": "exist",
      "description": "A representative text run from the original fixture must survive the round-trip unchanged."
    }
  ]
}

Fixture

Fixture file not found on disk.

Reference preview

No rendered reference is available for this case.

Spec 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.