devedeng-extended/tests/test_split_planner.py
Luna 691664095b 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>
2026-06-04 22:26:22 -07:00

115 lines
3.7 KiB
Python

"""Tests for the pure disc-split planner in project.py."""
from devedeng.project import (
plan_disc_split,
_movie_chapter_seconds,
_nearest_chapter_before,
)
# ~4.2 GB usable disc in kBytes (matches get_dvd_size()[0]*1000 with margin)
CAP = 4_200_000.0
# 4500k video + 192k audio = 4692 kbit/s -> /8 = 586.5 kByte/s
RATE = (4500 + 192) / 8.0
# seconds that fill one disc at RATE
SECS_PER_DISC = CAP / RATE
def names(plan):
return [[seg[0] for seg in disc] for disc in plan]
def test_single_short_movie_one_disc(make_movie):
m = make_movie(rate_kBps=RATE, length=3600)
plan = plan_disc_split([m], CAP)
assert len(plan) == 1
assert plan[0] == [(m, 0, 0)]
def test_two_movies_fit_one_disc(make_movie):
a = make_movie(rate_kBps=RATE, length=3000)
b = make_movie(rate_kBps=RATE, length=3000)
plan = plan_disc_split([a, b], CAP)
assert len(plan) == 1
assert names(plan) == [[a, b]]
def test_two_movies_overflow_to_two_discs(make_movie):
a = make_movie(rate_kBps=RATE, length=5000)
b = make_movie(rate_kBps=RATE, length=5000)
plan = plan_disc_split([a, b], CAP)
assert len(plan) == 2
assert plan[0] == [(a, 0, 0)]
assert plan[1] == [(b, 0, 0)]
def test_single_long_movie_splits_at_chapter(make_movie):
# 200 min movie, chapters every 5 min (300 s)
m = make_movie(rate_kBps=RATE, length=12000, chapters_step=5)
plan = plan_disc_split([m], CAP)
assert len(plan) >= 2
first = plan[0][0]
assert first[0] is m and first[1] == 0
cut = first[2]
# cut lands on a 300-s chapter boundary, just under one disc's capacity
assert cut % 300 == 0
assert SECS_PER_DISC - 300 < cut <= SECS_PER_DISC
# last segment runs to the end (duration 0)
assert plan[-1][0][2] == 0
def test_single_long_movie_no_chapters_even_cut(make_movie):
m = make_movie(rate_kBps=RATE, length=12000, divide=False)
plan = plan_disc_split([m], CAP)
assert len(plan) >= 2
# first cut is an exact time cut at capacity
assert abs(plan[0][0][2] - SECS_PER_DISC) < 1.0
def test_segments_cover_whole_movie(make_movie):
m = make_movie(rate_kBps=RATE, length=12000, divide=False)
plan = plan_disc_split([m], CAP)
# reconstruct covered span from (start, duration) segments
covered = 0.0
for disc in plan:
movie, start, duration = disc[0]
end = movie.original_length if duration == 0 else start + duration
assert start == covered # contiguous, no gaps/overlaps
covered = end
assert covered == m.original_length
def test_menu_reservation_on_first_disc(make_movie):
m = make_movie(rate_kBps=RATE, length=3000)
# reserve a chunk on disc 1; small movie still fits
plan = plan_disc_split([m], CAP, first_disc_reserved_kbytes=500_000)
assert len(plan) == 1
def test_empty_project_returns_one_empty_disc():
plan = plan_disc_split([], CAP)
assert plan == [[]]
def test_zero_rate_does_not_crash(make_movie):
m = make_movie(rate_kBps=0, length=3600)
plan = plan_disc_split([m], CAP)
assert len(plan) >= 1
# --- chapter helpers ---
def test_chapter_seconds_auto_and_manual(make_movie):
m = make_movie(length=1000, chapters_step=5, manual="2:30,9:00")
# auto: 300, 600, 900 (< length-4); manual: 150 (2:30), 540 (9:00)
assert _movie_chapter_seconds(m) == [150, 300, 540, 600, 900]
def test_chapter_seconds_disabled(make_movie):
m = make_movie(length=1000, divide=False)
assert _movie_chapter_seconds(m) == []
def test_nearest_chapter_before():
assert _nearest_chapter_before([300, 600, 900], 0, 700) == 600
assert _nearest_chapter_before([300, 600, 900], 0, 250) is None
assert _nearest_chapter_before([300, 600, 900], 600, 1000) == 900