Skip to content

Commit 5de2e4a

Browse files
committed
[tests] Expand VFR test coverage and rework CLI tests
Use Click's CliRunner rather than subprocesses for CLI tests. Add CSV, EDL, and expand OTIO tests for VFR and compare that the OpenCV and PyAV backends return equal results for both CFR and VFR video.
1 parent 701cdb7 commit 5de2e4a

File tree

4 files changed

+467
-222
lines changed

4 files changed

+467
-222
lines changed

scenedetect/_cli/commands.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,9 @@ def save_xml(
487487
logger.error(f"Unknown format: {format}")
488488

489489

490+
# TODO: We have to export framerate as a float for OTIO's current format. When OTIO supports
491+
# fractional timecodes, we should export the framerate as a rational number instead.
492+
# https://github.com/AcademySoftwareFoundation/OpenTimelineIO/issues/190
490493
def save_otio(
491494
context: CliContext,
492495
scenes: SceneList,

tests/helpers.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#
2+
# PySceneDetect: Python-Based Video Scene Detector
3+
# -------------------------------------------------------------------
4+
# [ Site: https://scenedetect.com ]
5+
# [ Docs: https://scenedetect.com/docs/ ]
6+
# [ Github: https://github.com/Breakthrough/PySceneDetect/ ]
7+
#
8+
# Copyright (C) 2014-2025 Brandon Castellano <http://www.bcastell.com>.
9+
# PySceneDetect is licensed under the BSD 3-Clause License; see the
10+
# included LICENSE file, or visit one of the above pages for details.
11+
#
12+
"""Shared test helpers."""
13+
14+
import typing as ty
15+
16+
from click.testing import CliRunner
17+
18+
from scenedetect._cli import scenedetect as _scenedetect_cli
19+
from scenedetect._cli.context import CliContext
20+
from scenedetect._cli.controller import run_scenedetect
21+
22+
23+
def invoke_cli(args: ty.List[str], catch_exceptions: bool = False) -> ty.Tuple[int, str]:
24+
"""Invoke the scenedetect CLI in-process using Click's CliRunner.
25+
26+
Replicates the two-step execution of ``__main__.py``:
27+
28+
1. ``scenedetect.main(obj=context)`` — parse args and register callbacks on ``CliContext``
29+
2. ``run_scenedetect(context)`` — execute detection and output commands
30+
31+
Returns ``(exit_code, output_text)``.
32+
"""
33+
context = CliContext()
34+
runner = CliRunner()
35+
result = runner.invoke(_scenedetect_cli, args, obj=context, catch_exceptions=catch_exceptions)
36+
if result.exit_code == 0:
37+
run_scenedetect(context)
38+
return result.exit_code, result.output

tests/test_cli.py

Lines changed: 83 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838

3939
import scenedetect
4040
from scenedetect.output import is_ffmpeg_available, is_mkvmerge_available
41+
from tests.helpers import invoke_cli
4142

4243
SCENEDETECT_CMD = sys.executable + " -m scenedetect"
4344

@@ -303,14 +304,22 @@ def test_cli_detector_with_stats(tmp_path, detector_command: str):
303304

304305
def test_cli_list_scenes(tmp_path: Path):
305306
"""Test `list-scenes` command."""
306-
# Regular invocation
307-
assert (
308-
invoke_scenedetect(
309-
"-i {VIDEO} time {TIME} {DETECTOR} list-scenes",
310-
output_dir=tmp_path,
311-
)
312-
== 0
307+
exit_code, _ = invoke_cli(
308+
[
309+
"-i",
310+
DEFAULT_VIDEO_PATH,
311+
"-o",
312+
str(tmp_path),
313+
"time",
314+
"-s",
315+
"2s",
316+
"-d",
317+
"4s",
318+
"detect-content",
319+
"list-scenes",
320+
]
313321
)
322+
assert exit_code == 0
314323
output_path = tmp_path.joinpath(f"{DEFAULT_VIDEO_NAME}-Scenes.csv")
315324
assert os.path.exists(output_path)
316325
EXPECTED_CSV_OUTPUT = """Timecode List:,00:00:03.754
@@ -742,13 +751,22 @@ def test_cli_load_scenes_round_trip():
742751

743752
def test_cli_save_edl(tmp_path: Path):
744753
"""Test `save-edl` command."""
745-
assert (
746-
invoke_scenedetect(
747-
"-i {VIDEO} time {TIME} {DETECTOR} save-edl",
748-
output_dir=tmp_path,
749-
)
750-
== 0
754+
exit_code, _ = invoke_cli(
755+
[
756+
"-i",
757+
DEFAULT_VIDEO_PATH,
758+
"-o",
759+
str(tmp_path),
760+
"time",
761+
"-s",
762+
"2s",
763+
"-d",
764+
"4s",
765+
"detect-content",
766+
"save-edl",
767+
]
751768
)
769+
assert exit_code == 0
752770
output_path = tmp_path.joinpath(f"{DEFAULT_VIDEO_NAME}.edl")
753771
assert os.path.exists(output_path)
754772
EXPECTED_EDL_OUTPUT = f"""* CREATED WITH PYSCENEDETECT {scenedetect.__version__}
@@ -763,13 +781,28 @@ def test_cli_save_edl(tmp_path: Path):
763781

764782
def test_cli_save_edl_with_params(tmp_path: Path):
765783
"""Test `save-edl` command but override the other options."""
766-
assert (
767-
invoke_scenedetect(
768-
"-i {VIDEO} time {TIME} {DETECTOR} save-edl -t title -r BX -f file_no_ext",
769-
output_dir=tmp_path,
770-
)
771-
== 0
784+
exit_code, _ = invoke_cli(
785+
[
786+
"-i",
787+
DEFAULT_VIDEO_PATH,
788+
"-o",
789+
str(tmp_path),
790+
"time",
791+
"-s",
792+
"2s",
793+
"-d",
794+
"4s",
795+
"detect-content",
796+
"save-edl",
797+
"-t",
798+
"title",
799+
"-r",
800+
"BX",
801+
"-f",
802+
"file_no_ext",
803+
]
772804
)
805+
assert exit_code == 0
773806
output_path = tmp_path.joinpath("file_no_ext")
774807
assert os.path.exists(output_path)
775808
EXPECTED_EDL_OUTPUT = f"""* CREATED WITH PYSCENEDETECT {scenedetect.__version__}
@@ -784,13 +817,22 @@ def test_cli_save_edl_with_params(tmp_path: Path):
784817

785818
def test_cli_save_otio(tmp_path: Path):
786819
"""Test `save-otio` command."""
787-
assert (
788-
invoke_scenedetect(
789-
"-i {VIDEO} time {TIME} {DETECTOR} save-otio",
790-
output_dir=tmp_path,
791-
)
792-
== 0
820+
exit_code, _ = invoke_cli(
821+
[
822+
"-i",
823+
DEFAULT_VIDEO_PATH,
824+
"-o",
825+
str(tmp_path),
826+
"time",
827+
"-s",
828+
"2s",
829+
"-d",
830+
"4s",
831+
"detect-content",
832+
"save-otio",
833+
]
793834
)
835+
assert exit_code == 0
794836
output_path = tmp_path.joinpath(f"{DEFAULT_VIDEO_NAME}.otio")
795837
assert os.path.exists(output_path)
796838
EXPECTED_OTIO_OUTPUT = """{
@@ -992,13 +1034,23 @@ def test_cli_save_otio(tmp_path: Path):
9921034

9931035
def test_cli_save_otio_no_audio(tmp_path: Path):
9941036
"""Test `save-otio` command without audio."""
995-
assert (
996-
invoke_scenedetect(
997-
"-i {VIDEO} time {TIME} {DETECTOR} save-otio --no-audio",
998-
output_dir=tmp_path,
999-
)
1000-
== 0
1037+
exit_code, _ = invoke_cli(
1038+
[
1039+
"-i",
1040+
DEFAULT_VIDEO_PATH,
1041+
"-o",
1042+
str(tmp_path),
1043+
"time",
1044+
"-s",
1045+
"2s",
1046+
"-d",
1047+
"4s",
1048+
"detect-content",
1049+
"save-otio",
1050+
"--no-audio",
1051+
]
10011052
)
1053+
assert exit_code == 0
10021054
output_path = tmp_path.joinpath(f"{DEFAULT_VIDEO_NAME}.otio")
10031055
assert os.path.exists(output_path)
10041056
EXPECTED_OTIO_OUTPUT = """{

0 commit comments

Comments
 (0)