build: harden controller v2 runtime
This commit is contained in:
+75
-4
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
import stat
|
||||
import subprocess
|
||||
import tempfile
|
||||
import threading
|
||||
@@ -24,14 +25,34 @@ class ImmediateExecutor:
|
||||
|
||||
|
||||
class FakeYtDlp:
|
||||
def __init__(self, media_base: Path, *, fail_download=False, no_media=False):
|
||||
def __init__(
|
||||
self,
|
||||
media_base: Path,
|
||||
*,
|
||||
fail_download=False,
|
||||
no_media=False,
|
||||
rewrite_cookie=False,
|
||||
):
|
||||
self.media_base = media_base
|
||||
self.fail_download = fail_download
|
||||
self.no_media = no_media
|
||||
self.rewrite_cookie = rewrite_cookie
|
||||
self.calls = []
|
||||
self.cookie_observations = []
|
||||
|
||||
def __call__(self, arguments, **kwargs):
|
||||
self.calls.append((list(arguments), dict(kwargs)))
|
||||
if "--cookies" in arguments:
|
||||
cookie_path = Path(arguments[arguments.index("--cookies") + 1])
|
||||
self.cookie_observations.append(
|
||||
(
|
||||
cookie_path,
|
||||
cookie_path.read_text(encoding="utf-8"),
|
||||
stat.S_IMODE(cookie_path.stat().st_mode),
|
||||
)
|
||||
)
|
||||
if self.rewrite_cookie:
|
||||
cookie_path.write_text("updated by yt-dlp", encoding="utf-8")
|
||||
if "--flat-playlist" in arguments:
|
||||
return subprocess.CompletedProcess(
|
||||
arguments,
|
||||
@@ -92,7 +113,8 @@ class DownloadManagerTests(unittest.TestCase):
|
||||
|
||||
def test_success_uses_configured_cookie_and_atomically_builds_playlist(self):
|
||||
events = []
|
||||
runner = FakeYtDlp(self.media)
|
||||
self.cookie.chmod(0o444)
|
||||
runner = FakeYtDlp(self.media, rewrite_cookie=True)
|
||||
manager = self.manager(runner, events)
|
||||
result = manager.enqueue("https://example.test/playlist", "Alice", "Service")
|
||||
self.assertEqual(result.status, "reserved")
|
||||
@@ -109,20 +131,41 @@ class DownloadManagerTests(unittest.TestCase):
|
||||
self.assertEqual(list(playlist.parent.glob(".playlist.*.tmp")), [])
|
||||
for arguments, _ in runner.calls:
|
||||
self.assertIn("--ignore-config", arguments)
|
||||
self.assertIn("--no-js-runtimes", arguments)
|
||||
self.assertEqual(arguments[arguments.index("--js-runtimes") + 1], "node")
|
||||
self.assertEqual(
|
||||
arguments[arguments.index("--cache-dir") + 1],
|
||||
"/tmp/yt-dlp-cache",
|
||||
)
|
||||
self.assertIn("--cookies", arguments)
|
||||
self.assertIn(str(self.cookie), arguments)
|
||||
self.assertNotIn(str(self.cookie), arguments)
|
||||
self.assertEqual(len(runner.cookie_observations), 2)
|
||||
working_paths = [item[0] for item in runner.cookie_observations]
|
||||
self.assertEqual(len(set(working_paths)), 2)
|
||||
self.assertTrue(
|
||||
all(item[1] == "cookie data" for item in runner.cookie_observations)
|
||||
)
|
||||
self.assertTrue(
|
||||
all(item[2] == 0o600 for item in runner.cookie_observations)
|
||||
)
|
||||
self.assertTrue(all(not path.exists() for path in working_paths))
|
||||
self.assertEqual(self.cookie.read_text(encoding="utf-8"), "cookie data")
|
||||
download_arguments = runner.calls[-1][0]
|
||||
self.assertIn("bv*[height<=720]+ba/b[height<=720]/b", download_arguments)
|
||||
|
||||
def test_worker_failure_is_contained_and_persisted(self):
|
||||
events = []
|
||||
manager = self.manager(FakeYtDlp(self.media, fail_download=True), events)
|
||||
runner = FakeYtDlp(self.media, fail_download=True)
|
||||
manager = self.manager(runner, events)
|
||||
result = manager.enqueue("https://example.test/failure", None, None)
|
||||
self.assertEqual(result.status, "reserved")
|
||||
slot = self.store.get_slot(1)
|
||||
self.assertEqual(slot.media_state, "error")
|
||||
self.assertIn("exit code 7", slot.last_error)
|
||||
self.assertEqual(events[-1].state, "error")
|
||||
self.assertTrue(
|
||||
all(not observation[0].exists() for observation in runner.cookie_observations)
|
||||
)
|
||||
|
||||
def test_missing_required_cookie_fails_job_without_escaping(self):
|
||||
self.cookie.unlink()
|
||||
@@ -196,6 +239,34 @@ class DownloadManagerTests(unittest.TestCase):
|
||||
self.assertFalse(slot.exists())
|
||||
self.assertEqual(self.store.get_slot(1).media_state, "empty")
|
||||
|
||||
def test_interrupted_deletion_recovery_defers_a_new_playback_guard(self):
|
||||
reservation = self.store.reserve_download(
|
||||
"https://example.test/protected-recovery",
|
||||
None,
|
||||
None,
|
||||
job_id="protected-recovery",
|
||||
)
|
||||
self.store.fail_download(
|
||||
"protected-recovery",
|
||||
1,
|
||||
reservation.generation,
|
||||
"failed",
|
||||
)
|
||||
self.store.begin_delete(1, reason="manual")
|
||||
slot = self.media / "slot1"
|
||||
slot.mkdir(parents=True)
|
||||
(slot / "still-playing.mp4").write_bytes(b"playing")
|
||||
self.store.protect_slot(1, "player_active", guard_id="fresh-player")
|
||||
manager = self.manager(FakeYtDlp(self.media))
|
||||
|
||||
self.assertEqual(manager.recover_pending_deletions(), [])
|
||||
self.assertTrue(slot.exists())
|
||||
self.assertEqual(self.store.get_slot(1).media_state, "deleting")
|
||||
|
||||
self.store.clear_playback_guards(reason="fresh terminal idle evidence")
|
||||
self.assertEqual(manager.recover_pending_deletions(), [1])
|
||||
self.assertFalse(slot.exists())
|
||||
|
||||
def test_shutdown_terminates_tracked_process_without_waiting_for_job_timeout(self):
|
||||
manager = DownloadManager(self.config, self.store)
|
||||
errors = []
|
||||
|
||||
Reference in New Issue
Block a user