Files
cadSet/SimpleCADAPI/tools/scene_contract_fixtures/common.py

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}