WIP checkpoint: Address review feedback: Implement Godot dedicated server scene + player spawning
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
"""Tests for the lightweight glTF metadata extraction service."""
|
||||
|
||||
import json
|
||||
import struct
|
||||
import pytest
|
||||
|
||||
from app.services.gltf import (
|
||||
extract_gltf_metadata,
|
||||
looks_like_gltf,
|
||||
_parse_glb,
|
||||
_parse_gltf_json,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test data helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _make_gltf_json(
|
||||
meshes: int = 1,
|
||||
vertices_per_mesh: int = 24,
|
||||
has_animations: bool = False,
|
||||
images: list[dict] | None = None,
|
||||
) -> dict:
|
||||
"""Build a minimal glTF JSON structure."""
|
||||
accessors = []
|
||||
meshes_data = []
|
||||
for i in range(meshes):
|
||||
acc_index = len(accessors)
|
||||
accessors.append({
|
||||
"bufferView": 0,
|
||||
"componentType": 5126, # FLOAT
|
||||
"count": vertices_per_mesh,
|
||||
"type": "VEC3",
|
||||
"max": [1.0, 1.0, 1.0],
|
||||
"min": [-1.0, -1.0, -1.0],
|
||||
})
|
||||
meshes_data.append({
|
||||
"primitives": [{
|
||||
"attributes": {"POSITION": acc_index},
|
||||
}],
|
||||
})
|
||||
gltf = {
|
||||
"asset": {"version": "2.0"},
|
||||
"accessors": accessors,
|
||||
"meshes": meshes_data,
|
||||
}
|
||||
if has_animations:
|
||||
gltf["animations"] = [{"name": "anim1", "channels": [], "samplers": []}]
|
||||
if images:
|
||||
gltf["images"] = images
|
||||
return gltf
|
||||
|
||||
|
||||
def _make_glb(gltf_json: dict) -> bytes:
|
||||
"""Wrap *gltf_json* in a minimal GLB binary container."""
|
||||
json_bytes = json.dumps(gltf_json).encode("utf-8")
|
||||
json_length = len(json_bytes)
|
||||
# Pad JSON chunk to 4-byte boundary.
|
||||
json_padding = (4 - json_length % 4) % 4
|
||||
json_chunk = json_bytes + b"\x00" * json_padding
|
||||
json_chunk_type = 0x4E4F534A # "JSON"
|
||||
# No BIN chunk.
|
||||
bin_length = 0
|
||||
bin_chunk_type = 0x004E4942
|
||||
total_length = 12 + 8 + len(json_chunk) + 8 + bin_length
|
||||
header = struct.pack("<III", 0x46546C67, 2, total_length) # magic, version, length
|
||||
json_header = struct.pack("<II", len(json_chunk), json_chunk_type)
|
||||
if bin_length > 0:
|
||||
bin_header = struct.pack("<II", bin_length, bin_chunk_type)
|
||||
else:
|
||||
bin_header = b""
|
||||
return header + json_header + json_chunk + bin_header
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# looks_like_gltf
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestLooksLikeGltf:
|
||||
def test_glb_magic(self):
|
||||
assert looks_like_gltf(b"glTF\x00\x00\x00\x00") is True
|
||||
|
||||
def test_gltf_extension(self):
|
||||
assert looks_like_gltf(b'{"asset":{"version":"2.0"}}', "model.gltf") is True
|
||||
|
||||
def test_glb_extension(self):
|
||||
assert looks_like_gltf(b"some binary", "model.glb") is True
|
||||
|
||||
def test_json_with_asset_version(self):
|
||||
payload = json.dumps({"asset": {"version": "2.0"}}).encode()
|
||||
assert looks_like_gltf(payload) is True
|
||||
|
||||
def test_plain_json_no_asset(self):
|
||||
payload = json.dumps({"foo": "bar"}).encode()
|
||||
assert looks_like_gltf(payload) is False
|
||||
|
||||
def test_non_json_bytes(self):
|
||||
assert looks_like_gltf(b"\x89PNG\r\n\x1a\n") is False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _parse_glb
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestParseGlb:
|
||||
def test_parses_valid_glb(self):
|
||||
gltf_json = _make_gltf_json()
|
||||
glb = _make_glb(gltf_json)
|
||||
result = _parse_glb(glb)
|
||||
assert result["asset"]["version"] == "2.0"
|
||||
assert len(result["meshes"]) == 1
|
||||
|
||||
def test_rejects_too_short(self):
|
||||
with pytest.raises(ValueError, match="too short"):
|
||||
_parse_glb(b"glTF\x00\x00")
|
||||
|
||||
def test_rejects_invalid_magic(self):
|
||||
with pytest.raises(ValueError, match="invalid GLB magic"):
|
||||
_parse_glb(b"NOTglTF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _parse_gltf_json
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestParseGltfJson:
|
||||
def test_parses_valid_gltf(self):
|
||||
gltf_json = _make_gltf_json()
|
||||
result = _parse_gltf_json(json.dumps(gltf_json).encode())
|
||||
assert result["asset"]["version"] == "2.0"
|
||||
|
||||
def test_rejects_non_object(self):
|
||||
with pytest.raises(ValueError, match="root must be an object"):
|
||||
_parse_gltf_json(b"[1, 2, 3]")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# extract_gltf_metadata (GLB path)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestExtractGltfMetadataGlb:
|
||||
def test_basic_glb(self):
|
||||
gltf_json = _make_gltf_json(meshes=1, vertices_per_mesh=24)
|
||||
glb = _make_glb(gltf_json)
|
||||
meta = extract_gltf_metadata(glb)
|
||||
assert meta["mesh_count"] == 1
|
||||
assert meta["vertex_count"] == 24
|
||||
assert meta["has_animation"] is False
|
||||
assert meta["bounding_box_min"] == [-1.0, -1.0, -1.0]
|
||||
assert meta["bounding_box_max"] == [1.0, 1.0, 1.0]
|
||||
assert meta["texture_refs"] == []
|
||||
|
||||
def test_multiple_meshes(self):
|
||||
gltf_json = _make_gltf_json(meshes=3, vertices_per_mesh=12)
|
||||
glb = _make_glb(gltf_json)
|
||||
meta = extract_gltf_metadata(glb)
|
||||
assert meta["mesh_count"] == 3
|
||||
assert meta["vertex_count"] == 36
|
||||
|
||||
def test_with_animation(self):
|
||||
gltf_json = _make_gltf_json(has_animations=True)
|
||||
glb = _make_glb(gltf_json)
|
||||
meta = extract_gltf_metadata(glb)
|
||||
assert meta["has_animation"] is True
|
||||
|
||||
def test_with_external_texture(self):
|
||||
gltf_json = _make_gltf_json(images=[{"uri": "textures/rock.png"}])
|
||||
glb = _make_glb(gltf_json)
|
||||
meta = extract_gltf_metadata(glb)
|
||||
assert meta["texture_refs"] == ["textures/rock.png"]
|
||||
|
||||
def test_with_named_texture(self):
|
||||
gltf_json = _make_gltf_json(images=[{"name": "my_texture"}])
|
||||
glb = _make_glb(gltf_json)
|
||||
meta = extract_gltf_metadata(glb)
|
||||
assert meta["texture_refs"] == ["my_texture"]
|
||||
|
||||
def test_empty_meshes(self):
|
||||
gltf_json = _make_gltf_json(meshes=0)
|
||||
glb = _make_glb(gltf_json)
|
||||
meta = extract_gltf_metadata(glb)
|
||||
assert meta["mesh_count"] == 0
|
||||
assert meta["vertex_count"] == 0
|
||||
assert meta["bounding_box_min"] == [0.0, 0.0, 0.0]
|
||||
assert meta["bounding_box_max"] == [0.0, 0.0, 0.0]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# extract_gltf_metadata (plain .gltf JSON path)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestExtractGltfMetadataGltfJson:
|
||||
def test_plain_gltf(self):
|
||||
gltf_json = _make_gltf_json(meshes=2, vertices_per_mesh=8)
|
||||
raw = json.dumps(gltf_json).encode()
|
||||
meta = extract_gltf_metadata(raw)
|
||||
assert meta["mesh_count"] == 2
|
||||
assert meta["vertex_count"] == 16
|
||||
assert meta["has_animation"] is False
|
||||
|
||||
def test_plain_gltf_with_animation(self):
|
||||
gltf_json = _make_gltf_json(has_animations=True)
|
||||
raw = json.dumps(gltf_json).encode()
|
||||
meta = extract_gltf_metadata(raw)
|
||||
assert meta["has_animation"] is True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Error handling
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestExtractGltfMetadataErrors:
|
||||
def test_invalid_glb(self):
|
||||
with pytest.raises(ValueError):
|
||||
extract_gltf_metadata(b"not a glb")
|
||||
|
||||
def test_invalid_gltf_json(self):
|
||||
with pytest.raises(ValueError):
|
||||
_parse_gltf_json(b"[1, 2, 3]")
|
||||
Reference in New Issue
Block a user