Files
importarr/tests/test_queue_controls.py
T

328 lines
13 KiB
Python

from importarr.config import Settings
from importarr.state import State
def configure_main(tmp_path, monkeypatch):
monkeypatch.setenv("IMPORTARR_STATE_PATH", str(tmp_path / "initial.db"))
import importarr.main as main
download = tmp_path / "downloads"
movies = tmp_path / "movies"
tv = tmp_path / "tv"
monkeypatch.setattr(main, "settings", Settings(download_root=download, movies_root=movies, tv_root=tv, state_path=tmp_path / "state.db"))
monkeypatch.setattr(main, "state", State(tmp_path / "state.db"))
return main, download, movies, tv
def test_pause_prevents_manual_queue_sync(tmp_path, monkeypatch):
main, download, _movies, _tv = configure_main(tmp_path, monkeypatch)
batch = download / "Release"
batch.mkdir(parents=True)
(batch / "Movie.mkv").write_bytes(b"movie")
main.state.add_manual_batch(batch)
main.state.set_app_state("queue_mode", "paused")
main.sync_manual_queue()
assert main.state.list_queue_items() == []
def test_start_reenables_manual_queue_sync(tmp_path, monkeypatch):
main, download, _movies, _tv = configure_main(tmp_path, monkeypatch)
batch = download / "Release"
batch.mkdir(parents=True)
(batch / "Movie.mkv").write_bytes(b"movie")
main.state.add_manual_batch(batch)
main.state.set_app_state("queue_mode", "paused")
main.sync_manual_queue()
main.state.set_app_state("queue_mode", "running")
main.sync_manual_queue()
assert len(main.state.list_queue_items()) == 1
def test_default_control_commands_target_manual_import_service(monkeypatch):
monkeypatch.delenv("IMPORTARR_START_COMMAND", raising=False)
monkeypatch.delenv("IMPORTARR_STOP_COMMAND", raising=False)
monkeypatch.delenv("IMPORTARR_RESTART_COMMAND", raising=False)
settings = Settings.from_env()
assert settings.start_command == ["systemctl", "start", "manual-media-import.service"]
assert settings.stop_command == ["systemctl", "stop", "manual-media-import.service"]
assert settings.restart_command == ["systemctl", "restart", "manual-media-import.service"]
def test_start_control_starts_manual_import_service(tmp_path, monkeypatch):
main, _download, _movies, _tv = configure_main(tmp_path, monkeypatch)
main.settings.start_command = ["systemctl", "start", "manual-media-import.service"]
calls = []
def fake_run(command, **kwargs):
calls.append(command)
return main.subprocess.CompletedProcess(command, 0, stdout="started", stderr="")
monkeypatch.setattr(main.subprocess, "run", fake_run)
result = main.start_queue()
assert result["control"]["queue_mode"] == "running"
assert result["command_result"]["command"] == ["systemctl", "start", "manual-media-import.service"]
assert calls == [["systemctl", "start", "manual-media-import.service"]]
def test_stop_control_stops_manual_import_service(tmp_path, monkeypatch):
main, _download, _movies, _tv = configure_main(tmp_path, monkeypatch)
main.settings.stop_command = ["systemctl", "stop", "manual-media-import.service"]
calls = []
def fake_run(command, **kwargs):
calls.append(command)
return main.subprocess.CompletedProcess(command, 0, stdout="stopped", stderr="")
monkeypatch.setattr(main.subprocess, "run", fake_run)
result = main.stop_queue()
assert result["control"]["queue_mode"] == "stopped"
assert result["control"]["cancel_requested"] is True
assert result["command_result"]["command"] == ["systemctl", "stop", "manual-media-import.service"]
assert calls == [["systemctl", "stop", "manual-media-import.service"]]
def test_queue_jobs_include_groups_and_manual_context(tmp_path, monkeypatch):
main, download, _movies, _tv = configure_main(tmp_path, monkeypatch)
batch = download / "Release" / "Season 1"
batch.mkdir(parents=True)
(batch / "Episode.mkv").write_bytes(b"episode")
main.state.add_manual_batch(batch.parent)
main.sync_manual_queue()
jobs = main.queue_jobs()
assert jobs[0]["group"] == "manual_batch"
assert jobs[0]["relative_path"] == "Release/Season 1/Episode.mkv"
assert jobs[0]["can_run_now"] is True
assert jobs[0]["state"] == "ready"
def test_queue_item_retry_ignore_and_remove_actions(tmp_path, monkeypatch):
main, _download, _movies, _tv = configure_main(tmp_path, monkeypatch)
row = main.state.upsert_queue_item(source_type="sab", source_id="job-1", name="Release", state="failed", reason="ImportError")
retried = main.queue_item_action(row["id"], main.QueueItemActionRequest(action="retry"))
assert retried["item"]["state"] == "ready"
assert retried["item"]["reason"] == "retry requested"
ignored = main.queue_item_action(row["id"], main.QueueItemActionRequest(action="ignore"))
assert ignored["item"]["state"] == "skipped"
removed = main.queue_item_action(row["id"], main.QueueItemActionRequest(action="remove"))
assert removed == {"status": "removed", "id": row["id"]}
assert main.state.get_queue_item(row["id"]) is None
def test_queue_item_run_now_imports_only_selected_item(tmp_path, monkeypatch):
main, download, movies, _tv = configure_main(tmp_path, monkeypatch)
batch = download / "Release"
batch.mkdir(parents=True)
selected = batch / "Selected.mkv"
waiting = batch / "Waiting.mkv"
selected.write_bytes(b"selected")
waiting.write_bytes(b"waiting")
selected_row = main.state.upsert_queue_item(source_type="manual", source_id=str(selected), source_path=selected, name=selected.name, state="ready")
main.state.upsert_queue_item(source_type="manual", source_id=str(waiting), source_path=waiting, name=waiting.name, state="ready")
result = main.queue_item_action(selected_row["id"], main.QueueItemActionRequest(action="run-now"))
assert result["status"] == "imported"
assert result["imported"] == 1
assert (movies / "Selected.mkv").read_bytes() == b"selected"
assert waiting.exists()
rows = {row["name"]: row for row in main.state.list_queue_items(active_only=False)}
assert rows["Selected.mkv"]["state"] == "imported"
assert rows["Waiting.mkv"]["state"] == "ready"
def test_current_job_status_includes_progress(tmp_path, monkeypatch):
main, _download, _movies, _tv = configure_main(tmp_path, monkeypatch)
monkeypatch.setattr(main.time, "time", lambda: 110.0)
main.set_current_job("Movie.mkv", bytes_copied=50, total_bytes=200, started_at=100.0)
current = main.control_status()["current"]
assert current["file"] == "Movie.mkv"
assert current["bytes_copied"] == 50
assert current["total_bytes"] == 200
assert current["percent"] == 25
assert current["elapsed_seconds"] == 10
def test_cancel_current_stops_before_next_manual_item(tmp_path, monkeypatch):
main, download, movies, tv = configure_main(tmp_path, monkeypatch)
batch = download / "Release"
batch.mkdir(parents=True)
(batch / "A.mkv").write_bytes(b"a")
(batch / "B.mkv").write_bytes(b"b")
main.state.add_manual_batch(batch)
main.sync_manual_queue()
main.state.set_app_state("cancel_requested", "true")
assert main._import_manual_batches(main.Importer(movies, tv)) == 0
assert len(main.state.list_queue_items()) == 2
def test_cancel_current_stops_active_copy(tmp_path, monkeypatch):
main, download, movies, tv = configure_main(tmp_path, monkeypatch)
batch = download / "Release"
batch.mkdir(parents=True)
source = batch / "A.mkv"
source.write_bytes(b"a" * (1024 * 1024 + 1))
main.state.add_manual_batch(batch)
main.sync_manual_queue()
calls = 0
def cancel_during_copy() -> bool:
nonlocal calls
calls += 1
return calls > 1
monkeypatch.setattr(main, "consume_cancel_request", cancel_during_copy)
assert main._import_manual_batches(main.Importer(movies, tv)) == 0
assert source.exists()
assert not any(movies.glob("*.partial"))
assert main.state.list_queue_items(active_only=False)[0]["state"] == "skipped"
def test_status_includes_queue_counts(tmp_path, monkeypatch):
main, _download, _movies, _tv = configure_main(tmp_path, monkeypatch)
main.state.upsert_queue_item(source_type="manual", source_id="a", name="A.mkv", state="ready")
main.state.upsert_queue_item(source_type="manual", source_id="b", name="B.mkv", state="failed")
status = main.status()
assert status["queue_total"] == 2
assert status["queue_counts"]["ready"] == 1
assert status["queue_counts"]["failed"] == 1
def test_worker_claimed_failure_retries_item(tmp_path, monkeypatch):
main, download, movies, tv = configure_main(tmp_path, monkeypatch)
batch = download / "Release"
batch.mkdir(parents=True)
source = batch / "A.mkv"
source.write_bytes(b"a")
row = main.state.upsert_queue_item(source_type="manual", source_id=str(source), source_path=source, name=source.name, state="ready")
claimed = main.state.claim_next_queue_item(main.WORKER_ID)
class BrokenImporter:
def import_file(self, *args, **kwargs):
raise RuntimeError("boom")
imported = main._import_queue_item(claimed, BrokenImporter(), from_worker=True)
updated = main.state.get_queue_item(row["id"])
assert imported == 0
assert updated["state"] == "retrying"
assert updated["attempt_count"] == 1
def test_control_update_runs_configured_command(tmp_path, monkeypatch):
main, _download, _movies, _tv = configure_main(tmp_path, monkeypatch)
main.settings.update_command = ["upgrade", "now"]
monkeypatch.setattr(main, "check_update_available", lambda: {"status": "update_available", "current_version": "0.1.0", "latest_version": "0.2.0", "update_available": True, "release_url": None})
calls = []
def fake_run(command, **kwargs):
calls.append((command, kwargs))
return main.subprocess.CompletedProcess(command, 0, stdout="updated", stderr="")
monkeypatch.setattr(main.subprocess, "run", fake_run)
result = main.update_service()
assert result["status"] == "update_available"
assert result["command_result"]["status"] == "ok"
assert result["command_result"]["command"] == ["upgrade", "now"]
assert result["command_result"]["stdout"] == "updated"
assert calls[0][0] == ["upgrade", "now"]
assert calls[0][1].get("shell") is not True
def test_control_update_skips_command_when_current(tmp_path, monkeypatch):
main, _download, _movies, _tv = configure_main(tmp_path, monkeypatch)
main.settings.update_command = ["upgrade", "now"]
monkeypatch.setattr(main, "check_update_available", lambda: {"status": "current", "current_version": "0.2.0", "latest_version": "v0.2.0", "update_available": False, "release_url": None})
def fake_run(command, **kwargs):
raise AssertionError("update command should not run without a newer release")
monkeypatch.setattr(main.subprocess, "run", fake_run)
result = main.update_service()
assert result["status"] == "current"
assert result["command"] == ["upgrade", "now"]
assert result["update_available"] is False
def test_update_check_compares_latest_release(tmp_path, monkeypatch):
main, _download, _movies, _tv = configure_main(tmp_path, monkeypatch)
monkeypatch.setenv("IMPORTARR_VERSION", "0.1.0")
class FakeResponse:
def raise_for_status(self):
return None
def json(self):
return {"tag_name": "v0.2.0", "html_url": "https://example.test/releases/v0.2.0"}
class FakeClient:
def __init__(self, timeout):
self.timeout = timeout
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return False
def get(self, url, headers):
assert url == main.settings.update_release_url
assert headers["Accept"] == "application/json"
return FakeResponse()
monkeypatch.setattr(main.httpx, "Client", FakeClient)
result = main.check_update_available()
assert result["status"] == "update_available"
assert result["current_version"] == "0.1.0"
assert result["latest_version"] == "v0.2.0"
assert result["update_available"] is True
def test_control_restart_reports_command_failure(tmp_path, monkeypatch):
main, _download, _movies, _tv = configure_main(tmp_path, monkeypatch)
main.settings.restart_command = ["restart"]
def fake_run(command, **kwargs):
return main.subprocess.CompletedProcess(command, 1, stdout="", stderr="failed")
monkeypatch.setattr(main.subprocess, "run", fake_run)
try:
main.restart_service()
except main.HTTPException as exc:
assert exc.status_code == 500
assert exc.detail["status"] == "failed"
assert exc.detail["stderr"] == "failed"
else:
raise AssertionError("expected HTTPException")