Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added tests/__init__.py
Empty file.
132 changes: 132 additions & 0 deletions tests/test_core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
"""Unit tests for the `dkdc_draw.core` PyO3 surface.

Covers the canonical flow — build / save / load / export — plus error paths.
Skips `run_cli` since it's an argv-driven CLI harness; structural correctness
is already covered by the Rust integration tests.
"""

from __future__ import annotations

import json
from pathlib import Path

import pytest

import dkdc_draw
from dkdc_draw import core


def _parse(doc_json: str) -> dict:
return json.loads(doc_json)


def test_top_level_reexports_wrapper_api() -> None:
# `dkdc_draw.__init__` re-exports the PyO3 functions so `from dkdc_draw
# import new_document` works. Smoke-test that the wrapper layer is intact.
for name in (
"new_document",
"load_document",
"save_document",
"export_svg",
"export_png",
"run_cli",
"run",
"main",
):
assert hasattr(dkdc_draw, name), f"dkdc_draw missing {name}"


def test_new_document_returns_valid_json() -> None:
doc = _parse(core.new_document("my drawing"))
assert doc["name"] == "my drawing"
assert doc["elements"] == []
assert doc["version"] == 1
assert isinstance(doc["id"], str) and doc["id"]


def test_save_load_roundtrip_empty(tmp_path: Path) -> None:
original = core.new_document("roundtrip")
path = tmp_path / "empty.draw.json"

core.save_document(original, str(path))
loaded = core.load_document(str(path))

assert _parse(original) == _parse(loaded)


def test_save_load_roundtrip_with_element(tmp_path: Path) -> None:
doc = _parse(core.new_document("with element"))
doc["elements"].append(
{
"type": "Rectangle",
"id": "r1",
"x": 10.0,
"y": 20.0,
"width": 100.0,
"height": 60.0,
}
)
path = tmp_path / "rect.draw.json"

core.save_document(json.dumps(doc), str(path))
loaded = _parse(core.load_document(str(path)))

assert loaded["elements"][0]["id"] == "r1"
assert loaded["elements"][0]["type"] == "Rectangle"
assert loaded["elements"][0]["width"] == 100.0


def test_export_svg_well_formed() -> None:
doc = core.new_document("svg")
svg = core.export_svg(doc)
assert svg.startswith("<svg")
assert svg.rstrip().endswith("</svg>")


def test_export_png_has_magic_bytes() -> None:
doc = core.new_document("png")
png = core.export_png(doc)
assert png[:4] == b"\x89PNG"


def test_export_png_default_scale_is_2() -> None:
doc = core.new_document("scale")
# Default (scale=2.0) should be larger than scale=1.0 for the same doc.
default_png = core.export_png(doc)
small_png = core.export_png(doc, 1.0)
# An empty doc renders to a small image either way, but 2x should still
# produce a file at least as large as 1x in practice.
assert len(default_png) >= len(small_png)


def test_export_png_custom_scale() -> None:
doc = core.new_document("scale custom")
big = core.export_png(doc, 3.0)
assert big[:4] == b"\x89PNG"


# ── Error paths ──────────────────────────────────────────────────────


def test_load_nonexistent_raises(tmp_path: Path) -> None:
with pytest.raises(RuntimeError):
core.load_document(str(tmp_path / "does-not-exist.draw.json"))


def test_save_invalid_json_raises(tmp_path: Path) -> None:
with pytest.raises(RuntimeError):
core.save_document("not valid json", str(tmp_path / "x.draw.json"))


def test_export_svg_invalid_json_raises() -> None:
with pytest.raises(RuntimeError):
core.export_svg("")
with pytest.raises(RuntimeError):
core.export_svg("not valid json")
with pytest.raises(RuntimeError):
core.export_svg("{}") # valid json, missing Document fields


def test_export_png_invalid_json_raises() -> None:
with pytest.raises(RuntimeError):
core.export_png("{ not json")
61 changes: 0 additions & 61 deletions tests/test_draw.py

This file was deleted.

56 changes: 56 additions & 0 deletions tests/test_gallery_roundtrip.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""Round-trip every gallery drawing through the Python bindings.

The committed `examples/gallery/*.draw.json` files act as fixtures — they
cover every element type, fill pattern, and stroke style in the core API.
If a backwards-incompatible change lands in the serde layer or the export
pipeline, this surfaces immediately against real documents.
"""

from __future__ import annotations

from pathlib import Path

import pytest

from dkdc_draw import core

GALLERY = Path(__file__).resolve().parents[1] / "examples" / "gallery"
SLUGS = ["flowchart", "sticky", "wireframe", "sketch", "patterns"]


@pytest.fixture(scope="module")
def gallery_json() -> dict[str, str]:
missing = [s for s in SLUGS if not (GALLERY / f"{s}.draw.json").exists()]
if missing:
pytest.skip(f"gallery fixtures missing: {missing}")
return {s: (GALLERY / f"{s}.draw.json").read_text() for s in SLUGS}


@pytest.mark.parametrize("slug", SLUGS)
def test_gallery_load_roundtrip(
tmp_path: Path, slug: str, gallery_json: dict[str, str]
) -> None:
src = GALLERY / f"{slug}.draw.json"
loaded = core.load_document(str(src))

dst = tmp_path / f"{slug}.draw.json"
core.save_document(loaded, str(dst))
reloaded = core.load_document(str(dst))

# Re-save-then-reload is a byte-stable fixed point.
assert loaded == reloaded


@pytest.mark.parametrize("slug", SLUGS)
def test_gallery_export_svg(slug: str, gallery_json: dict[str, str]) -> None:
svg = core.export_svg(gallery_json[slug])
assert svg.startswith("<svg")
assert svg.rstrip().endswith("</svg>")


@pytest.mark.parametrize("slug", SLUGS)
def test_gallery_export_png(slug: str, gallery_json: dict[str, str]) -> None:
png = core.export_png(gallery_json[slug])
assert png[:4] == b"\x89PNG"
# Gallery drawings have content, so PNG shouldn't be trivially small.
assert len(png) > 500
Loading