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>
115 lines
3.7 KiB
Python
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
|