Files
Barış Keser cc959dc16e Test paketi ekle: 47 test, düzeltilen her hata için regresyon
.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.
2026-08-03 10:12:31 +03:00

180 lines
6.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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"