Files
MoviePilot/tests/test_benchmark_metainfo_rust.py

224 lines
8.3 KiB
Python

from contextlib import contextmanager
from types import SimpleNamespace
from unittest.mock import Mock
import pytest
from scripts import benchmark_metainfo_rust as benchmark
def test_build_inputs_separates_video_and_music_domains():
"""影视与音乐输入应独立扩展,并按 repeat 稳定重复。"""
video_once = benchmark.build_video_inputs(1)
music_once = benchmark.build_music_inputs(1)
assert benchmark.build_video_inputs(2) == video_once * 2
assert benchmark.build_music_inputs(2) == music_once * 2
assert {kind for kind, _value, _subtitle in music_once} == {
"music_query",
"title",
"path",
}
assert all(not value.lower().endswith(tuple(benchmark.metainfo_module.settings.RMT_AUDIOEXT))
for kind, value, _subtitle in music_once if kind == "music_query")
def test_parse_input_uses_public_production_entries(monkeypatch):
"""输入分发应调用 MetaInfo、MetaInfoPath 和 MetaMusic.parse_query 公开入口。"""
title_result = object()
path_result = object()
music_result = object()
title_parser = Mock(return_value=title_result)
path_parser = Mock(return_value=path_result)
music_parser = Mock(return_value=music_result)
monkeypatch.setattr(benchmark, "MetaInfo", title_parser)
monkeypatch.setattr(benchmark, "MetaInfoPath", path_parser)
monkeypatch.setattr(benchmark.MetaMusic, "parse_query", music_parser)
assert benchmark.parse_input(("title", "Movie 2026", "subtitle")) is title_result
assert benchmark.parse_input(("path", "/media/Movie 2026/movie.mkv", None)) is path_result
assert benchmark.parse_input(("music_query", "Artist - Track", None)) is music_result
title_parser.assert_called_once_with(
title="Movie 2026",
subtitle="subtitle",
custom_words=["#"],
)
path_parser.assert_called_once_with(benchmark.Path("/media/Movie 2026/movie.mkv"))
music_parser.assert_called_once_with("Artist - Track")
def test_selected_meta_parser_disables_and_restores_all_fast_paths(monkeypatch):
"""Python 对照上下文应屏蔽影视和音乐 Rust 入口,并完整恢复原函数。"""
rust_accel = benchmark.metainfo_module.rust_accel
parser_names = (
"parse_metainfo",
"parse_metainfo_path",
"find_metainfo",
"parse_metamusic",
)
originals = {}
for name in parser_names:
parser = Mock(name=name)
monkeypatch.setattr(rust_accel, name, parser, raising=False)
originals[name] = parser
with benchmark.selected_meta_parser(use_rust=False):
for name in parser_names:
assert getattr(rust_accel, name)("sample") is None
for name, parser in originals.items():
assert getattr(rust_accel, name) is parser
def test_measure_switches_once_and_warms_up_outside_samples(monkeypatch):
"""一次测量只应切换一次解析器,并额外执行一轮不计时预热。"""
context_calls = []
parse_calls = []
@contextmanager
def fake_selected_meta_parser(use_rust: bool):
"""记录测试中的解析器上下文进入次数。"""
context_calls.append(use_rust)
yield
def fake_parse_all(inputs):
"""记录测试中的每轮解析调用。"""
parse_calls.append(inputs)
return [object()] * len(inputs)
monkeypatch.setattr(benchmark, "selected_meta_parser", fake_selected_meta_parser)
monkeypatch.setattr(benchmark, "parse_all", fake_parse_all)
elapsed, parsed_count = benchmark.measure(
[("title", "Movie", None)],
use_rust=False,
loops=2,
repeats=3,
)
assert context_calls == [False]
assert len(parse_calls) == 7
assert parsed_count == 1
assert elapsed >= 0
def test_assert_projected_results_equal_reports_first_field_difference():
"""等价校验失败时应报告首个输入及稳定字段差异。"""
inputs = [("music_query", "Artist - Track", None)]
rust_result = SimpleNamespace(title="Track", artists=["Artist"])
python_result = SimpleNamespace(title="Other", artists=["Artist"])
with pytest.raises(AssertionError) as error:
benchmark.assert_projected_results_equal(
inputs,
[rust_result],
[python_result],
lambda result: {
"title": result.title,
"artists": list(result.artists),
},
)
message = str(error.value)
assert "Artist - Track" in message
assert "title" in message
assert "Track" in message
assert "Other" in message
def test_video_projection_ignores_python_parser_internal_state():
"""影视等价投影不应纳入 Python 解析器的临时私有字段。"""
rust_result = benchmark.MetaInfo("Marty Supreme 2025 2160p WEB-DL")
python_result = benchmark.MetaInfo("Marty Supreme 2025 2160p WEB-DL")
rust_result._index = 1
python_result._index = 99
rust_result._effect = []
python_result._effect = ["temporary"]
assert benchmark.project_video_result(rust_result) == benchmark.project_video_result(
python_result
)
def test_validate_rust_runtime_rejects_disabled_and_old_extensions(monkeypatch):
"""运行前检查应拒绝关闭的 Rust 和缺少音乐入口的旧扩展。"""
rust_accel = benchmark.metainfo_module.rust_accel
monkeypatch.setattr(rust_accel, "is_enabled", Mock(return_value=False))
with pytest.raises(RuntimeError, match="未启用"):
benchmark.validate_rust_runtime()
monkeypatch.setattr(rust_accel, "is_enabled", Mock(return_value=True))
monkeypatch.setattr(rust_accel, "parse_metamusic", Mock(return_value={}), raising=False)
monkeypatch.setattr(rust_accel, "_moviepilot_rust", SimpleNamespace())
with pytest.raises(RuntimeError, match="版本过旧"):
benchmark.validate_rust_runtime()
def test_validate_rust_runtime_requires_successful_music_probe(monkeypatch):
"""音乐 Rust 入口存在但实际回退 Python 时也必须拒绝执行基准。"""
rust_accel = benchmark.metainfo_module.rust_accel
extension = SimpleNamespace(parse_metamusic_fast=Mock())
monkeypatch.setattr(rust_accel, "is_enabled", Mock(return_value=True))
monkeypatch.setattr(rust_accel, "_moviepilot_rust", extension)
monkeypatch.setattr(rust_accel, "parse_metamusic", Mock(return_value=None), raising=False)
with pytest.raises(RuntimeError, match="探针"):
benchmark.validate_rust_runtime()
def test_main_outputs_independent_video_and_music_metrics(monkeypatch, capsys):
"""主程序应分别输出影视和音乐等价状态、耗时及性能提升。"""
monkeypatch.setattr(benchmark, "validate_rust_runtime", Mock())
monkeypatch.setattr(benchmark, "build_video_inputs", Mock(return_value=[("title", "V", None)]))
monkeypatch.setattr(
benchmark,
"build_music_inputs",
Mock(return_value=[("music_query", "M", None), ("title", "M.flac", None)]),
)
results = [
{
"rust_ms": 1.0,
"python_ms": 2.0,
"rust_count": 1,
"python_count": 1,
"speedup": 2.0,
},
{
"rust_ms": 2.0,
"python_ms": 6.0,
"rust_count": 2,
"python_count": 2,
"speedup": 3.0,
},
]
monkeypatch.setattr(benchmark, "benchmark_suite", Mock(side_effect=results))
monkeypatch.setattr(
benchmark.sys,
"argv",
["benchmark_metainfo_rust.py", "--repeat-inputs", "1", "--loops", "1", "--repeats", "1"],
)
assert benchmark.main() == 0
output = capsys.readouterr().out
assert "video_equivalent=true" in output
assert "video_speedup=2.00x" in output
assert "music_equivalent=true" in output
assert "music_speedup=3.00x" in output
assert "video_rust_us_per_item=1000.000" in output
assert "music_rust_us_per_item=1000.000" in output
def test_main_reports_runtime_failure_with_nonzero_exit(monkeypatch, capsys):
"""Rust 未就绪时主程序应明确报错并返回非零状态。"""
monkeypatch.setattr(
benchmark,
"validate_rust_runtime",
Mock(side_effect=RuntimeError("old extension")),
)
monkeypatch.setattr(benchmark.sys, "argv", ["benchmark_metainfo_rust.py"])
assert benchmark.main() == 2
assert "benchmark_error=old extension" in capsys.readouterr().err