- Removed unnecessary imports from various test files to improve readability and maintainability. - Added new settings for the Generated Feed display in the settingsFeed.js file. - Introduced a new configuration file for Pyright type checking.
189 lines
7.1 KiB
Python
189 lines
7.1 KiB
Python
"""
|
|
Tests for 3D model metadata extraction (extractors_3d.py).
|
|
|
|
Covers:
|
|
- GLB JSON chunk happy path (embedded workflow/prompt)
|
|
- GLB JSON chunk too large (skipped without crash)
|
|
- GLB with empty/no extras (no workflow found)
|
|
- Sidecar JSON fallback when GLB has no embedded data
|
|
- Malformed sidecar JSON (graceful no-op)
|
|
- Missing sidecar + valid GLB extras (only embedded data used)
|
|
- .gltf text file parsing
|
|
- Non-GLB formats fall through to sidecar only
|
|
"""
|
|
import json
|
|
import struct
|
|
from pathlib import Path
|
|
|
|
from mjr_am_backend.features.metadata.extractors_3d import (
|
|
_read_glb_json_chunk,
|
|
_read_sidecar_json,
|
|
extract_model3d_metadata,
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers to build minimal GLB binary blobs
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_GLB_MAGIC = 0x46546C67
|
|
_GLB_CHUNK_TYPE_JSON = 0x4E4F534A
|
|
_GLB_CHUNK_TYPE_BIN = 0x004E4942
|
|
|
|
|
|
def _make_glb(json_payload: dict | None, *, version: int = 2) -> bytes:
|
|
"""Build a minimal GLB binary container with an optional JSON chunk."""
|
|
if json_payload is None:
|
|
# No chunks — truncated file
|
|
return struct.pack("<III", _GLB_MAGIC, version, 12)
|
|
|
|
json_bytes = json.dumps(json_payload).encode("utf-8")
|
|
# Pad to 4-byte boundary with spaces per glTF spec
|
|
pad = (4 - len(json_bytes) % 4) % 4
|
|
json_bytes += b" " * pad
|
|
|
|
total = 12 + 8 + len(json_bytes)
|
|
header = struct.pack("<III", _GLB_MAGIC, version, total)
|
|
chunk_header = struct.pack("<II", len(json_bytes), _GLB_CHUNK_TYPE_JSON)
|
|
return header + chunk_header + json_bytes
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _read_glb_json_chunk
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_read_glb_json_chunk_happy_path(tmp_path: Path) -> None:
|
|
payload = {"asset": {"version": "2.0"}, "extras": {"comfyui_workflow": {"nodes": []}}}
|
|
glb = tmp_path / "test.glb"
|
|
glb.write_bytes(_make_glb(payload))
|
|
result = _read_glb_json_chunk(str(glb))
|
|
assert result is not None
|
|
assert result.get("extras", {}).get("comfyui_workflow") == {"nodes": []}
|
|
|
|
|
|
def test_read_glb_json_chunk_returns_none_for_non_glb(tmp_path: Path) -> None:
|
|
f = tmp_path / "notglb.glb"
|
|
f.write_bytes(b"not a glb file at all")
|
|
assert _read_glb_json_chunk(str(f)) is None
|
|
|
|
|
|
def test_read_glb_json_chunk_returns_none_for_truncated(tmp_path: Path) -> None:
|
|
f = tmp_path / "trunc.glb"
|
|
f.write_bytes(_make_glb(None)) # header only, no chunk
|
|
assert _read_glb_json_chunk(str(f)) is None
|
|
|
|
|
|
def test_read_glb_json_chunk_skips_oversized_chunk(tmp_path: Path, monkeypatch) -> None:
|
|
import mjr_am_backend.features.metadata.extractors_3d as m
|
|
monkeypatch.setattr(m, "_MAX_GLB_JSON_CHUNK_BYTES", 4) # tiny limit
|
|
payload = {"asset": {"version": "2.0"}}
|
|
glb = tmp_path / "big.glb"
|
|
glb.write_bytes(_make_glb(payload))
|
|
assert _read_glb_json_chunk(str(glb)) is None
|
|
|
|
|
|
def test_read_glb_json_chunk_returns_none_for_missing_file() -> None:
|
|
assert _read_glb_json_chunk("/nonexistent/path/model.glb") is None
|
|
|
|
|
|
def test_read_glb_json_chunk_returns_none_for_old_version(tmp_path: Path) -> None:
|
|
payload = {"asset": {"version": "1.0"}}
|
|
glb = tmp_path / "v1.glb"
|
|
glb.write_bytes(_make_glb(payload, version=1))
|
|
assert _read_glb_json_chunk(str(glb)) is None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _read_sidecar_json
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_read_sidecar_json_happy_path(tmp_path: Path) -> None:
|
|
model = tmp_path / "mesh.obj"
|
|
model.write_bytes(b"# obj")
|
|
sidecar = tmp_path / "mesh.obj.json"
|
|
sidecar.write_text(json.dumps({"workflow": {"nodes": []}}), encoding="utf-8")
|
|
result = _read_sidecar_json(str(model))
|
|
assert result is not None
|
|
assert "workflow" in result
|
|
|
|
|
|
def test_read_sidecar_json_returns_none_when_missing(tmp_path: Path) -> None:
|
|
model = tmp_path / "mesh.obj"
|
|
model.write_bytes(b"# obj")
|
|
assert _read_sidecar_json(str(model)) is None
|
|
|
|
|
|
def test_read_sidecar_json_returns_none_for_malformed_json(tmp_path: Path) -> None:
|
|
model = tmp_path / "mesh.ply"
|
|
model.write_bytes(b"ply")
|
|
sidecar = tmp_path / "mesh.ply.json"
|
|
sidecar.write_text("{ this is not valid json", encoding="utf-8")
|
|
assert _read_sidecar_json(str(model)) is None
|
|
|
|
|
|
def test_read_sidecar_json_returns_none_for_non_dict(tmp_path: Path) -> None:
|
|
model = tmp_path / "mesh.stl"
|
|
model.write_bytes(b"stl")
|
|
sidecar = tmp_path / "mesh.stl.json"
|
|
sidecar.write_text(json.dumps([1, 2, 3]), encoding="utf-8")
|
|
assert _read_sidecar_json(str(model)) is None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# extract_model3d_metadata — integration
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_extract_model3d_metadata_glb_with_embedded_workflow(tmp_path: Path) -> None:
|
|
workflow = {"nodes": [{"id": 1, "type": "KSampler"}], "links": []}
|
|
payload = {"asset": {"version": "2.0"}, "extras": {"comfyui_workflow": workflow}}
|
|
glb = tmp_path / "mesh.glb"
|
|
glb.write_bytes(_make_glb(payload))
|
|
result = extract_model3d_metadata(str(glb))
|
|
assert result.ok
|
|
assert result.data["quality"] == "full"
|
|
assert result.data["workflow"] == workflow
|
|
|
|
|
|
def test_extract_model3d_metadata_glb_no_extras_falls_to_sidecar(tmp_path: Path) -> None:
|
|
payload = {"asset": {"version": "2.0"}}
|
|
glb = tmp_path / "mesh.glb"
|
|
glb.write_bytes(_make_glb(payload))
|
|
workflow = {"nodes": [{"id": 1, "type": "CLIPTextEncode"}], "links": []}
|
|
sidecar = tmp_path / "mesh.glb.json"
|
|
sidecar.write_text(json.dumps({"workflow": workflow}), encoding="utf-8")
|
|
result = extract_model3d_metadata(str(glb))
|
|
assert result.ok
|
|
assert result.data["quality"] == "full"
|
|
assert result.data["workflow"] == workflow
|
|
|
|
|
|
def test_extract_model3d_metadata_glb_no_extras_no_sidecar_returns_none_quality(tmp_path: Path) -> None:
|
|
payload = {"asset": {"version": "2.0"}}
|
|
glb = tmp_path / "empty.glb"
|
|
glb.write_bytes(_make_glb(payload))
|
|
result = extract_model3d_metadata(str(glb))
|
|
assert result.ok
|
|
assert result.data["quality"] == "none"
|
|
assert result.data["workflow"] is None
|
|
assert result.data["prompt"] is None
|
|
|
|
|
|
def test_extract_model3d_metadata_obj_sidecar_fallback(tmp_path: Path) -> None:
|
|
obj = tmp_path / "mesh.obj"
|
|
obj.write_bytes(b"# obj file")
|
|
workflow = {"nodes": [{"id": 2, "type": "VAEDecode"}], "links": []}
|
|
sidecar = tmp_path / "mesh.obj.json"
|
|
sidecar.write_text(json.dumps({"workflow": workflow}), encoding="utf-8")
|
|
result = extract_model3d_metadata(str(obj))
|
|
assert result.ok
|
|
assert result.data["quality"] == "full"
|
|
|
|
|
|
def test_extract_model3d_metadata_malformed_sidecar_returns_none_quality(tmp_path: Path) -> None:
|
|
obj = tmp_path / "bad.obj"
|
|
obj.write_bytes(b"# obj")
|
|
sidecar = tmp_path / "bad.obj.json"
|
|
sidecar.write_text("!!invalid json!!", encoding="utf-8")
|
|
result = extract_model3d_metadata(str(obj))
|
|
assert result.ok
|
|
assert result.data["quality"] == "none"
|