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

@ -26,21 +26,17 @@ Runtime external tools (must be on PATH): `ffmpeg`/`ffprobe` (or `avconv`/`avpro
## Testing
There is **no test framework or test suite.** Verify changes two ways:
There is a **pytest suite in `tests/`** (run `python3 -m pytest`; it needs `pytest` and PyGObject/`gi`). Tests are headless — no display, no actual encoding — and import the in-tree package via `tests/conftest.py` (which prepends `src/` to the path and installs the gettext `_` builtin). CI runs them on Codeberg via `.forgejo/workflows/tests.yml`. Coverage focuses on the two things that are cheap to test and easy to break:
1. **Pure logic** (no GTK/display) — e.g. the split planner. Import and exercise directly:
```bash
cd src && python3 -c "from devedeng.project import plan_disc_split; ..."
```
`plan_disc_split`, `_movie_chapter_seconds`, and `_nearest_chapter_before` in `project.py` are deliberately module-level pure functions so they can be unit-tested without instantiating GTK.
1. **Pure logic** (no GTK/display) — the split planner and bitrate math: `plan_disc_split`, `_movie_chapter_seconds`, `_nearest_chapter_before`, and `compute_high_profile_bitrate` in `project.py` are deliberately module-level pure functions so they can be unit-tested without instantiating GTK. See `tests/test_split_planner.py`, `tests/test_high_profile_bitrate.py`.
2. **Generated ffmpeg commands** — build a converter and inspect `command_var` without running anything. The config singleton works headless:
2. **Generated commands** — build a converter and assert on `command_var` without running anything (`tests/test_ffmpeg_command.py`); detection is checked by feeding `ffprobe.process_json` canned JSON (`tests/test_ffprobe_detection.py`). The `FakeFileProject`/`FakeMovie` factories and the `ffmpeg_converter`/`arg_after` helpers live in `conftest.py`. The config singleton works headless:
```python
cfg = devedeng.configuration_data.configuration.get_config(); cfg.disc_type = "dvd"
conv = devedeng.ffmpeg.ffmpeg(); conv.convert_file(mock_file_movie, "/tmp/out.mpg", 0)
print(conv.command_var) # the exact ffmpeg argv
```
`convert_file` takes the `file_movie` object as `file_project` and reads `hardsub_index`/`hardsub_kind`, the encode profile from config, etc. For a full integration check, run a real short encode through the executor's GLib loop and watch a fake progress bar (GTK is installed but there is no Xvfb here, so the *windowed* UI can only be verified by actually launching the app on a display).
`convert_file` reads `hardsub_index`/`hardsub_kind` off the `file_project` and the encode profile from config. For a full integration check, run a real short encode through the executor's GLib loop and watch a fake progress bar; the *windowed* UI can only be verified by launching the app on a real display.
`contrib/dvd-safe-converter.sh` is the known-good baseline the **Compatibility** profile is modelled on — use it as a regression reference for the no-subtitle DVD encode (mpeg2video / 720x480 / 29.97 / AC3 192k / 48k).