Add pytest suite and CI (roadmap A1)

37 headless tests covering the logic that is cheap to break:
- split planner: plan_disc_split, chapter helpers (whole-fit, multi-disc
  partition, single-movie chapter/even-cut split, contiguous coverage)
- High-profile fill-disc bitrate math (disc count + bounds + capacity)
- ffmpeg command generation: profiles, text/image hardsub, 2-pass, nice,
  progress-to-file, even pad/crop + positive offsets, -ss seek, and the
  AC3 448k audio-clamp regression
- ffprobe subtitle detection: text/image classification, tags/disposition

tests/conftest.py makes the in-tree package importable and installs the
gettext _ builtin so modules run headless. Forgejo Actions workflow runs
the suite on push/PR. Update CLAUDE.md's testing section accordingly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Luna 2026-06-04 22:26:22 -07:00
parent b5a6cfe25f
commit 691664095b
9 changed files with 559 additions and 8 deletions

View file

@ -0,0 +1,82 @@
"""Tests for embedded subtitle detection in ffprobe.process_json."""
import json
import devedeng.ffprobe
def _probe(streams):
p = devedeng.ffprobe.ffprobe()
data = json.dumps({"streams": streams})
err = p.process_json("/movies/input.mkv", data)
return p, err
def _video(index=0, duration="120.0"):
return {"index": index, "codec_type": "video", "codec_name": "h264",
"width": 1920, "height": 1080, "avg_frame_rate": "24/1",
"duration": duration}
def _audio(index=1):
return {"index": index, "codec_type": "audio", "codec_name": "aac",
"sample_rate": "48000"}
def _sub(index, codec, language="eng", title="", default=0, forced=0):
return {"index": index, "codec_type": "subtitle", "codec_name": codec,
"tags": {"language": language, "title": title},
"disposition": {"default": default, "forced": forced}}
def test_no_subtitles_empty_list():
p, err = _probe([_video(), _audio()])
assert err is False
assert p.subtitle_tracks == []
def test_text_subtitle_classified_text():
p, _ = _probe([_video(), _audio(), _sub(2, "subrip", "eng", default=1)])
assert len(p.subtitle_tracks) == 1
t = p.subtitle_tracks[0]
assert t["index"] == 2
assert t["codec"] == "subrip"
assert t["language"] == "eng"
assert t["kind"] == "text"
assert t["default"] is True
assert t["forced"] is False
def test_image_subtitles_classified_image():
for codec in ("hdmv_pgs_subtitle", "dvd_subtitle", "dvdsub", "pgssub", "xsub"):
p, _ = _probe([_video(), _audio(), _sub(2, codec)])
assert p.subtitle_tracks[0]["kind"] == "image", codec
def test_various_text_codecs_classified_text():
for codec in ("subrip", "ass", "ssa", "mov_text", "webvtt"):
p, _ = _probe([_video(), _audio(), _sub(2, codec)])
assert p.subtitle_tracks[0]["kind"] == "text", codec
def test_multiple_subtitle_tracks_in_order():
p, _ = _probe([
_video(), _audio(),
_sub(2, "subrip", "eng", default=1),
_sub(3, "hdmv_pgs_subtitle", "fre", forced=1),
_sub(4, "ass", "jpn", title="Signs"),
])
assert [t["index"] for t in p.subtitle_tracks] == [2, 3, 4]
assert [t["kind"] for t in p.subtitle_tracks] == ["text", "image", "text"]
assert p.subtitle_tracks[1]["forced"] is True
assert p.subtitle_tracks[2]["title"] == "Signs"
def test_missing_tags_and_disposition_defaults():
# a subtitle stream with no tags/disposition must not crash
p, _ = _probe([_video(), _audio(),
{"index": 2, "codec_type": "subtitle",
"codec_name": "subrip"}])
t = p.subtitle_tracks[0]
assert t["language"] == ""
assert t["default"] is False
assert t["forced"] is False