mirror of
https://github.com/barkeser2002/VMD-Motion-Optimizer.git
synced 2026-09-25 01:10:14 +03:00
.gitignore *.vmd dosyalarını engellediği için fixture'lar depoya konamıyor; tests/conftest.py spesifikasyona uygun VMD baytlarını çalışma anında üretiyor. Kapsam: kayıpsız gidiş-dönüş, interpolasyon eğrilerinin kare bazında korunması, kamera/gölge bloklarının hayatta kalması, VMD 1.0 başlığı, bozuk dosyanın hızlı reddi, çıktı==girdi reddi, atomik yazma, cp932 çevriyazısı, _moving_average uzunluğu, depth eğilim çıkarma, ground IK dışlama ve geri alma, morph sabit aralık sonu, normalize edilmemiş quaternionlar, RDP özyineleme derinliği, iptal ve GUI duman testleri.
180 lines
6.5 KiB
Python
180 lines
6.5 KiB
Python
# VMD Motion Optimizer by Barış Keser (barkeser2002)
|
||
# License: GNU General Public License v3.0 (GPL-3.0)
|
||
|
||
"""VMD okuma/yazma ve veri kaybı testleri."""
|
||
|
||
import struct
|
||
|
||
import pytest
|
||
|
||
from conftest import SAMPLE_INTERP, build_vmd, simple_bone
|
||
import optimize_vmd as ov
|
||
|
||
|
||
def test_roundtrip_preserves_keys_and_interp(vmd_path, out_path):
|
||
"""Her kanalın tek anahtarı varsa hiçbiri düşmemeli ve eğriler korunmalı."""
|
||
bones = [simple_bone(name=f"bone{i}", frame=i, y=float(i)) for i in range(10)]
|
||
morphs = [(f"morph{i}", i, i / 10.0) for i in range(10)]
|
||
src = vmd_path(build_vmd(bones=bones, morphs=morphs))
|
||
|
||
ov.optimize_vmd(src, out_path, pos_eps=0.0, rot_eps_deg=0.0, morph_eps=0.0)
|
||
|
||
m = ov.read_vmd(out_path)
|
||
assert m is not None
|
||
assert len(m.bones) == len(bones)
|
||
assert len(m.morphs) == len(morphs)
|
||
# Interpolasyon eğrileri korunmalı (v1.0.4 hepsini sıfırlıyordu)
|
||
assert all(b.interp == SAMPLE_INTERP for b in m.bones)
|
||
|
||
|
||
def test_interp_follows_its_own_frame(vmd_path, out_path):
|
||
"""Hayatta kalan her kare KENDİ eğrisini taşımalı (karışmamalı)."""
|
||
bones = [
|
||
("センター", f, (0.0, float(f % 2), 0.0), (0.0, 0.0, 0.0, 1.0), bytes([f]) * 64)
|
||
for f in range(10)
|
||
]
|
||
src = vmd_path(build_vmd(bones=bones))
|
||
|
||
ov.optimize_vmd(src, out_path, pos_eps=0.0, rot_eps_deg=0.0)
|
||
|
||
m = ov.read_vmd(out_path)
|
||
assert len(m.bones) > 2, "zikzak kanal iki kareye çökmemeli"
|
||
for b in m.bones:
|
||
assert b.interp == bytes([b.frame]) * 64
|
||
|
||
|
||
def test_flatten_interp_writes_mmd_default(vmd_path, out_path):
|
||
bones = [simple_bone(frame=f, y=float(f)) for f in range(5)]
|
||
src = vmd_path(build_vmd(bones=bones))
|
||
|
||
ov.optimize_vmd(src, out_path, pos_eps=0.0, rot_eps_deg=0.0, flatten_interp=True)
|
||
|
||
m = ov.read_vmd(out_path)
|
||
assert all(b.interp == ov.MMD_DEFAULT_INTERP for b in m.bones)
|
||
assert not any(b.interp == b'\x00' * 64 for b in m.bones)
|
||
|
||
|
||
def test_camera_block_survives(vmd_path, out_path):
|
||
"""Kamera VMD'si 62 baytlık kütüğe indirgenmemeli."""
|
||
data = build_vmd(model_name="カメラ・照明", bones=[], morphs=[], camera_frames=3)
|
||
src = vmd_path(data)
|
||
|
||
ov.optimize_vmd(src, out_path)
|
||
|
||
written = open(out_path, 'rb').read()
|
||
assert len(written) > 62
|
||
m = ov.read_vmd(out_path)
|
||
assert m is not None
|
||
# Kuyruk bloğu bayt bayt korunmalı
|
||
assert struct.unpack('<I', m.trailing[:4])[0] == 3
|
||
assert len(m.trailing) == 4 + 3 * 61 + 4 + 4 + 4
|
||
|
||
|
||
def test_ik_and_shadow_blocks_survive(vmd_path, out_path):
|
||
data = build_vmd(bones=[simple_bone()], shadow_frames=2)
|
||
src = vmd_path(data)
|
||
ov.optimize_vmd(src, out_path)
|
||
m = ov.read_vmd(out_path)
|
||
# camera=0, light=0, shadow=2, ik=0
|
||
assert m.trailing == struct.pack('<II', 0, 0) + struct.pack('<I', 2) + bytes(18) + struct.pack('<I', 0)
|
||
|
||
|
||
def test_short_trailing_file_is_accepted(vmd_path, out_path):
|
||
"""Yalnızca kamera+ışık sayacıyla biten eski dosyalar okunabilmeli."""
|
||
data = build_vmd(bones=[simple_bone()], trailing_blocks=2)
|
||
m = ov.read_vmd(vmd_path(data))
|
||
assert m is not None
|
||
assert len(m.bones) == 1
|
||
|
||
|
||
def test_vmd_v1_header_offsets(vmd_path):
|
||
"""v1 model adı alanı 10 bayt; v2 ofsetleriyle okunursa her şey kayar."""
|
||
data = build_vmd(model_name="V1Model", bones=[simple_bone(frame=7)], version=1)
|
||
m = ov.read_vmd(vmd_path(data))
|
||
assert m is not None
|
||
assert m.version == 1
|
||
assert m.model_name == "V1Model"
|
||
assert len(m.bones) == 1
|
||
assert m.bones[0].frame == 7
|
||
|
||
|
||
def test_corrupt_file_fails_fast(vmd_path):
|
||
"""Bozuk dosya saatlerce taranmadan reddedilmeli."""
|
||
data = bytearray(build_vmd(bones=[simple_bone(frame=f) for f in range(20)]))
|
||
struct.pack_into('<I', data, 50, 999999) # kemik sayısını boz
|
||
del data[-4:] # kuyruğu da kes
|
||
assert ov.read_vmd(vmd_path(bytes(data))) is None
|
||
|
||
|
||
def test_invalid_signature_returns_none(vmd_path):
|
||
assert ov.read_vmd(vmd_path(b"NOT A VMD FILE" + bytes(200))) is None
|
||
|
||
|
||
def test_refuses_to_overwrite_input(vmd_path):
|
||
src = vmd_path(build_vmd(bones=[simple_bone()]))
|
||
with pytest.raises(ValueError, match="girdi ile aynı"):
|
||
ov.optimize_vmd(src, src)
|
||
|
||
|
||
def test_force_allows_overwriting_input(vmd_path):
|
||
src = vmd_path(build_vmd(bones=[simple_bone(frame=f) for f in range(4)]))
|
||
ov.optimize_vmd(src, src, force_overwrite=True)
|
||
assert ov.read_vmd(src) is not None
|
||
|
||
|
||
def test_creates_missing_output_directory(vmd_path, tmp_path):
|
||
src = vmd_path(build_vmd(bones=[simple_bone()]))
|
||
out = str(tmp_path / "yeni" / "klasor" / "out.vmd")
|
||
ov.optimize_vmd(src, out)
|
||
assert ov.read_vmd(out) is not None
|
||
|
||
|
||
def test_write_is_atomic_on_failure(tmp_path):
|
||
"""Yazma sırasında hata olursa hedefteki dosya bozulmamalı."""
|
||
dest = tmp_path / "keep.vmd"
|
||
dest.write_bytes(b"ORIGINAL CONTENT")
|
||
|
||
bad = ov.Motion(model_name="x", bones=[], morphs=[])
|
||
bad.bones = [ov.BoneFrame(name="b", frame=0, pos=None, quat=None)] # yazarken patlar
|
||
|
||
with pytest.raises(Exception):
|
||
ov.write_vmd(str(dest), bad)
|
||
assert dest.read_bytes() == b"ORIGINAL CONTENT"
|
||
assert not list(tmp_path.glob("*.part"))
|
||
|
||
|
||
def test_header_fix_repairs_missing_padding(vmd_path, out_path):
|
||
"""İmzadan hemen sonra ad başlıyorsa (dolgu yok) düzeltilmeli."""
|
||
good = build_vmd(model_name="Padded", bones=[simple_bone(frame=3)])
|
||
broken = good[:25] + good[30:] # 5 baytlık dolguyu sil
|
||
src = vmd_path(broken, "broken.vmd")
|
||
|
||
ov.optimize_vmd(src, out_path)
|
||
m = ov.read_vmd(out_path)
|
||
assert m is not None
|
||
assert m.bones[0].frame == 3
|
||
|
||
|
||
def test_header_fix_leaves_valid_file_alone(vmd_path):
|
||
good = build_vmd(model_name="Padded", bones=[simple_bone()])
|
||
assert ov._maybe_fix_vmd_header(vmd_path(good)) is None
|
||
|
||
|
||
def test_header_fix_cleans_up_temp_file(vmd_path, out_path, tmp_path):
|
||
good = build_vmd(model_name="Padded", bones=[simple_bone()])
|
||
broken = good[:25] + good[30:]
|
||
import glob
|
||
import tempfile
|
||
before = set(glob.glob(tempfile.gettempdir() + "/*.fixed.vmd"))
|
||
ov.optimize_vmd(vmd_path(broken, "b.vmd"), out_path)
|
||
after = set(glob.glob(tempfile.gettempdir() + "/*.fixed.vmd"))
|
||
assert after == before
|
||
|
||
|
||
def test_model_name_transliterated_not_dropped(vmd_path, out_path):
|
||
"""cp932'de olmayan harfler silinmemeli: 'Barış' -> 'Baris', 'Bar' değil."""
|
||
src = vmd_path(build_vmd(model_name="XR Animator", bones=[simple_bone()]))
|
||
ov.optimize_vmd(src, out_path, replace_xr_with="Barış Keser")
|
||
m = ov.read_vmd(out_path)
|
||
assert m.model_name == "Baris Keser"
|