Files
MoviePilot/tests/test_test_runner.py
T

96 lines
3.4 KiB
Python

"""后端单测统一 runner 的分片与 CI 调用合同。"""
from pathlib import Path
import pytest
from tests import run as test_runner
ROOT = Path(__file__).resolve().parents[1]
WORKFLOW = ROOT / ".github" / "workflows" / "test.yml"
def _test_files(count: int) -> list[Path]:
"""构造按字典序排列的测试文件路径。"""
return [Path(f"test_{index:03d}.py") for index in range(count)]
def test_split_test_files_uses_stable_contiguous_chunks() -> None:
"""文件分片必须稳定覆盖全集,且与既有 CI 的连续均分语义一致。"""
test_files = _test_files(10)
shards = test_runner.split_test_files(test_files, shard_count=4)
assert [len(shard) for shard in shards] == [3, 3, 3, 1]
assert [test_file for shard in shards for test_file in shard] == test_files
def test_main_defaults_to_four_parallel_shards(monkeypatch) -> None:
"""无 runner 参数时应并行执行四个独立 pytest 文件分片。"""
test_files = _test_files(10)
captured = {}
monkeypatch.setattr(test_runner, "collect_test_files", lambda: test_files)
def fake_run_parallel(shards, pytest_args):
captured["shards"] = shards
captured["pytest_args"] = pytest_args
return 0
monkeypatch.setattr(test_runner, "run_parallel_shards", fake_run_parallel)
assert test_runner.main(["-q", "--maxfail=1"]) == 0
assert [len(shard) for shard in captured["shards"]] == [3, 3, 3, 1]
assert captured["pytest_args"] == ["-q", "--maxfail=1"]
def test_main_runs_requested_ci_shard_in_current_process(monkeypatch) -> None:
"""CI 指定分片时只运行该分片,并继续透传 pytest 参数。"""
test_files = _test_files(10)
captured = {}
monkeypatch.setattr(test_runner, "collect_test_files", lambda: test_files)
def fake_run_pytest(paths, pytest_args):
captured["paths"] = paths
captured["pytest_args"] = pytest_args
return 0
monkeypatch.setattr(test_runner, "run_pytest", fake_run_pytest)
assert test_runner.main(["--shard", "2/4", "-q"]) == 0
assert captured["paths"] == test_files[3:6]
assert captured["pytest_args"] == ["-q"]
def test_main_serial_preserves_legacy_full_suite_entry(monkeypatch) -> None:
"""串行模式必须保留 tests 根目录加 pytest 参数透传的旧入口。"""
captured = {}
def fake_run_pytest(paths, pytest_args):
captured["paths"] = paths
captured["pytest_args"] = pytest_args
return 0
monkeypatch.setattr(test_runner, "run_pytest", fake_run_pytest)
assert test_runner.main(["--serial", "-q", "--maxfail=1"]) == 0
assert captured["paths"] == [test_runner.TESTS_DIR]
assert captured["pytest_args"] == ["-q", "--maxfail=1"]
@pytest.mark.parametrize("value", ["0/4", "5/4", "1/0", "invalid"])
def test_invalid_shard_values_are_rejected(value: str) -> None:
"""分片参数必须使用有效的一基 N/TOTAL 范围。"""
with pytest.raises(SystemExit, match="2"):
test_runner.parse_args(["--shard", value])
def test_workflow_uses_the_shared_runner_contract() -> None:
"""CI 不得另行维护 shell 分片算法,覆盖率必须显式使用串行模式。"""
workflow = WORKFLOW.read_text(encoding="utf-8")
assert 'python tests/run.py --shard "${{ matrix.shard }}"' in workflow
assert "python -m coverage run tests/run.py --serial" in workflow
assert "mapfile" not in workflow
assert "SHARD_INDEX" not in workflow