119 lines
5.4 KiB
Python
119 lines
5.4 KiB
Python
"""Office content checks for installed Python runtime tests."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
from collections.abc import Iterator, Mapping, Sequence
|
|
from contextlib import contextmanager
|
|
from pathlib import Path
|
|
|
|
@contextmanager
|
|
def workspace(artifacts: Path | None = None, *, prefix: str = "dsh-office-render-") -> Iterator[Path]:
|
|
"""Retain only Office inputs, PNG batches, and errors before cleaning the temporary root."""
|
|
if artifacts is not None:
|
|
artifacts.mkdir(parents=True, exist_ok=False)
|
|
with tempfile.TemporaryDirectory(prefix=prefix) as temporary:
|
|
root = Path(temporary).resolve()
|
|
try:
|
|
yield root
|
|
except Exception as error:
|
|
if artifacts is not None:
|
|
(artifacts / "error.txt").write_text(f"{type(error).__name__}: {error}\n", encoding="utf-8")
|
|
raise
|
|
finally:
|
|
if artifacts is not None:
|
|
document = next(root.rglob("color-blocks.docx"), None)
|
|
if document is not None:
|
|
source = document.parent
|
|
for name in ("color-blocks.docx", "color-blocks.pptx", "color-blocks.xlsx"):
|
|
if (source / name).is_file():
|
|
shutil.copy2(source / name, artifacts / name)
|
|
for name in ("cli-docx", "cli-pptx", "cli-xlsx", "node-pptx"):
|
|
batch = source / name
|
|
if batch.is_dir():
|
|
destination = artifacts / name
|
|
destination.mkdir()
|
|
for file in batch.iterdir():
|
|
if file.is_file() and (file.suffix == ".png" or file.name == "manifest.json"):
|
|
shutil.copy2(file, destination / file.name)
|
|
|
|
|
|
def copy_documents(directory: Path) -> dict[str, Path]:
|
|
"""Copy the Kit's one-page color-block fixtures into a private test directory."""
|
|
documents = {}
|
|
for extension in ("docx", "pptx", "xlsx"):
|
|
source = Path(__file__).with_suffix("") / f"color-blocks.{extension}"
|
|
destination = directory / source.name
|
|
shutil.copy2(source, destination)
|
|
documents[extension] = destination
|
|
return documents
|
|
|
|
|
|
def check_image(path: Path) -> None:
|
|
"""Require visible colored blocks and text after compositing transparency onto white."""
|
|
from PIL import Image
|
|
|
|
with Image.open(path) as image:
|
|
rgba = image.convert("RGBA")
|
|
visible = Image.alpha_composite(Image.new("RGBA", rgba.size, "white"), rgba).convert("RGB")
|
|
red = blue = black = nonwhite = red_x = blue_x = 0
|
|
for index, (r, g, b) in enumerate(visible.get_flattened_data()):
|
|
x = index % visible.width
|
|
if r >= 160 and g <= 100 and b <= 100:
|
|
red += 1
|
|
red_x += x
|
|
if r <= 100 and g <= 160 and b >= 160:
|
|
blue += 1
|
|
blue_x += x
|
|
black += max(r, g, b) < 80
|
|
nonwhite += min(r, g, b) < 240
|
|
pixels = visible.width * visible.height
|
|
counts = {"red": red, "blue": blue, "black": black, "nonwhite": nonwhite, "pixels": pixels}
|
|
if red < pixels * 0.01 or blue < pixels * 0.01 or black < pixels * 0.0002 or nonwhite < pixels * 0.04:
|
|
raise AssertionError(f"Office image lacks visible color blocks or text: {path}: {counts}")
|
|
if red_x / red + visible.width * 0.1 >= blue_x / blue:
|
|
raise AssertionError(f"Office image has reversed or overlapping color blocks: {path}: {counts}")
|
|
|
|
|
|
def check_render_manifest(manifest: dict, expected_backend: str) -> Path:
|
|
"""Check direct-Office metadata and return its sole saved image path."""
|
|
assert manifest["backend"] == expected_backend, manifest
|
|
assert manifest["rasterEngine"] == "libreoffice", manifest
|
|
assert manifest["source"] == "saved", manifest
|
|
assert len(manifest["images"]) == 1, manifest
|
|
return Path(manifest["images"][0]["path"])
|
|
|
|
|
|
def check_render(manifest: dict, expected_backend: str) -> None:
|
|
"""Check direct-Office metadata and visible content in its sole output image."""
|
|
check_image(check_render_manifest(manifest, expected_backend))
|
|
|
|
|
|
def render_cli(
|
|
command: Sequence[str], documents: Mapping[str, Path], directory: Path,
|
|
expected_backend: str, environment: Mapping[str, str], *, check_content: bool = True,
|
|
) -> None:
|
|
"""Run the three document engines through the real CLI with isolated outputs."""
|
|
errors = []
|
|
for extension, document in documents.items():
|
|
selection = ["--sheet", "Render", "--range", "A1:B2"] if extension == "xlsx" else ["--pages", "1"]
|
|
try:
|
|
result = subprocess.run([
|
|
*command, "render", "--input", str(document), "--output-dir", str(directory / f"cli-{extension}"),
|
|
"--dpi", "72", "--timeout-ms", "120000", *selection,
|
|
], env=environment, capture_output=True, text=True, encoding="utf-8", timeout=480)
|
|
if result.returncode:
|
|
raise AssertionError(f"exit {result.returncode}: {result.stderr}")
|
|
manifest = json.loads(result.stdout)
|
|
if check_content:
|
|
check_render(manifest, expected_backend)
|
|
else:
|
|
check_render_manifest(manifest, expected_backend)
|
|
except Exception as error:
|
|
errors.append(f"{extension}: {type(error).__name__}: {error}")
|
|
if errors:
|
|
raise AssertionError("Office CLI content checks failed:\n" + "\n".join(errors))
|