146 lines
4.2 KiB
Python
146 lines
4.2 KiB
Python
"""Shared fixture construction and package-record helpers."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import hashlib
|
|
from copy import deepcopy
|
|
from typing import Any, Callable
|
|
|
|
from simplecadapi.scene import (
|
|
canonical_json_bytes,
|
|
validate_scene_package,
|
|
)
|
|
|
|
|
|
JsonObject = dict[str, Any]
|
|
ScenePackage = tuple[JsonObject, JsonObject, dict[str, bytes]]
|
|
Validator = Callable[[Any], Any]
|
|
|
|
IDENTITY = {
|
|
"origin": [0, 0, 0],
|
|
"x_axis": [1, 0, 0],
|
|
"y_axis": [0, 1, 0],
|
|
"z_axis": [0, 0, 1],
|
|
}
|
|
|
|
|
|
def b64(value: bytes) -> str:
|
|
return base64.b64encode(value).decode("ascii")
|
|
|
|
|
|
def sha256_hex(value: bytes) -> str:
|
|
return hashlib.sha256(value).hexdigest()
|
|
|
|
|
|
def content_hash(value: bytes) -> str:
|
|
return "sha256:" + sha256_hex(value)
|
|
|
|
|
|
def first_issue(report: Any) -> dict[str, str] | None:
|
|
issue = report.first_error
|
|
return None if issue is None else {"code": issue.code, "path": issue.path}
|
|
|
|
|
|
def report_case(name: str, payload: bytes, report: Any) -> dict[str, object]:
|
|
return {
|
|
"expected": first_issue(report),
|
|
"name": name,
|
|
"payload_base64": b64(payload),
|
|
"valid": report.valid,
|
|
}
|
|
|
|
|
|
def transform(origin: list[float] | None = None) -> JsonObject:
|
|
value = deepcopy(IDENTITY)
|
|
if origin is not None:
|
|
value["origin"] = origin
|
|
return value
|
|
|
|
|
|
def validation_case(
|
|
name: str, value: Any, validator: Validator
|
|
) -> dict[str, object]:
|
|
payload = value if isinstance(value, bytes) else canonical_json_bytes(value)
|
|
return report_case(name, payload, validator(payload))
|
|
|
|
|
|
def replace_entity_sidecar(
|
|
scene: JsonObject,
|
|
entity: JsonObject,
|
|
blobs: dict[str, bytes],
|
|
) -> ScenePackage:
|
|
entity_bytes = canonical_json_bytes(entity)
|
|
entity_hash = content_hash(entity_bytes)
|
|
entity_uri = f"entities/sha256-{entity_hash.removeprefix('sha256:')}.json"
|
|
entity_record = scene["entity_assets"][0]
|
|
old_uri = entity_record["uri"]
|
|
entity_record.update(
|
|
{
|
|
"byte_length": len(entity_bytes),
|
|
"content_hash": entity_hash,
|
|
"entity_asset_id": entity_hash,
|
|
"uri": entity_uri,
|
|
}
|
|
)
|
|
for definition in scene["definitions"]:
|
|
if definition["kind"] in {"part", "shape"}:
|
|
definition["entity_asset_id"] = entity_hash
|
|
updated_blobs = {uri: payload for uri, payload in blobs.items() if uri != old_uri}
|
|
updated_blobs[entity_uri] = entity_bytes
|
|
return scene, entity, updated_blobs
|
|
|
|
|
|
def set_product_material_appearance(
|
|
scene: JsonObject, material_id: str = "fixture_material"
|
|
) -> None:
|
|
appearance = scene["appearances"][0]
|
|
appearance["name"] = "Fixture material"
|
|
appearance["source"] = {
|
|
"kind": "product_material",
|
|
"material_id": material_id,
|
|
"root_id": "root",
|
|
}
|
|
draft = dict(appearance)
|
|
draft.pop("appearance_id")
|
|
appearance_id = "appearance/evaluated/" + sha256_hex(canonical_json_bytes(draft))
|
|
appearance["appearance_id"] = appearance_id
|
|
for definition in scene["definitions"]:
|
|
if definition["kind"] in {"part", "shape"}:
|
|
definition["appearance_id"] = appearance_id
|
|
|
|
|
|
def inline_package_case(
|
|
name: str,
|
|
scene: JsonObject,
|
|
blobs: dict[str, bytes],
|
|
blob_pool: dict[str, str],
|
|
) -> dict[str, object]:
|
|
scene_bytes = canonical_json_bytes(scene)
|
|
report = validate_scene_package(scene_bytes, blobs)
|
|
for uri, payload in blobs.items():
|
|
encoded = b64(payload)
|
|
previous = blob_pool.setdefault(uri, encoded)
|
|
if previous != encoded:
|
|
raise AssertionError(f"one fixture URI resolved to different bytes: {uri}")
|
|
return {
|
|
"blob_uris": sorted(blobs),
|
|
"expected": first_issue(report),
|
|
"manifest_base64": b64(scene_bytes),
|
|
"name": name,
|
|
"valid": report.valid,
|
|
}
|
|
|
|
|
|
def scene_shape_case(
|
|
name: str,
|
|
scene: JsonObject,
|
|
blobs: dict[str, bytes],
|
|
expected: dict[str, object],
|
|
blob_pool: dict[str, str],
|
|
) -> dict[str, object]:
|
|
package = inline_package_case(name, scene, blobs, blob_pool)
|
|
if not package["valid"]:
|
|
raise AssertionError(f"invalid scene shape fixture {name}: {package['expected']}")
|
|
return {**package, "expected_shape": expected}
|