Clean up review findings
This commit is contained in:
@@ -1,5 +1,12 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.52.3 - 2026-08-09
|
||||||
|
|
||||||
|
- Closed SQLite connections after each database operation to avoid long-running file descriptor leaks.
|
||||||
|
- Removed obsolete legacy-project state migration code for clean Kaizoku deployments.
|
||||||
|
- Made failed queue-job reuse provider-aware so retries do not cross Anikoto, AniNeko, and AnimePahe selections.
|
||||||
|
- Added `openssl` to runtime dependency reporting and accepted comma-separated remote path roots.
|
||||||
|
|
||||||
## 0.52.2 - 2026-08-09
|
## 0.52.2 - 2026-08-09
|
||||||
|
|
||||||
- Updated Docker runtime packages to match the current Kaizoku provider downloader and include `openssl` explicitly.
|
- Updated Docker runtime packages to match the current Kaizoku provider downloader and include `openssl` explicitly.
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ Useful environment variables:
|
|||||||
- `KAIZOKU_JOB_STDOUT=true` to mirror job logs to container or terminal output.
|
- `KAIZOKU_JOB_STDOUT=true` to mirror job logs to container or terminal output.
|
||||||
- `KAIZOKU_STATE_ROOT=/path/to/state` to move SQLite state, config, thumbnails, sessions, and staging.
|
- `KAIZOKU_STATE_ROOT=/path/to/state` to move SQLite state, config, thumbnails, sessions, and staging.
|
||||||
- `KAIZOKU_DOWNLOAD_DIR=/downloads` for the default library output path.
|
- `KAIZOKU_DOWNLOAD_DIR=/downloads` for the default library output path.
|
||||||
- `KAIZOKU_REMOTE_PATH_ROOTS=/downloads,/media/anime` to limit remote filesystem browsing.
|
- `KAIZOKU_REMOTE_PATH_ROOTS=/downloads,/media/anime` to limit remote filesystem browsing; comma-separated and platform path separators are accepted.
|
||||||
- `KAIZOKU_MODE=sub`, `KAIZOKU_QUALITY=best`, and `KAIZOKU_DEBUG=1` for runtime defaults and diagnostics.
|
- `KAIZOKU_MODE=sub`, `KAIZOKU_QUALITY=best`, and `KAIZOKU_DEBUG=1` for runtime defaults and diagnostics.
|
||||||
|
|
||||||
## Docker
|
## Docker
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import urllib.error
|
|||||||
import urllib.request
|
import urllib.request
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from binascii import Error as BinasciiError
|
from binascii import Error as BinasciiError
|
||||||
|
from contextlib import contextmanager
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from http.server import ThreadingHTTPServer
|
from http.server import ThreadingHTTPServer
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -53,8 +54,8 @@ from app_support import (
|
|||||||
client_address_is_local,
|
client_address_is_local,
|
||||||
clear_remote_access_sessions,
|
clear_remote_access_sessions,
|
||||||
debug_log,
|
debug_log,
|
||||||
|
ensure_project_app_root,
|
||||||
load_json,
|
load_json,
|
||||||
migrate_legacy_state_storage,
|
|
||||||
normalize_season,
|
normalize_season,
|
||||||
normalize_config,
|
normalize_config,
|
||||||
normalize_episode_offset,
|
normalize_episode_offset,
|
||||||
@@ -1473,10 +1474,18 @@ class WatchlistStore:
|
|||||||
"media_type": "tv",
|
"media_type": "tv",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
def _connect(self):
|
def _connect(self):
|
||||||
conn = sqlite3.connect(STATE_DB_PATH, timeout=30)
|
conn = sqlite3.connect(STATE_DB_PATH, timeout=30)
|
||||||
conn.row_factory = sqlite3.Row
|
conn.row_factory = sqlite3.Row
|
||||||
return conn
|
try:
|
||||||
|
yield conn
|
||||||
|
conn.commit()
|
||||||
|
except Exception:
|
||||||
|
conn.rollback()
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
def _table_columns_conn(self, conn, table_name):
|
def _table_columns_conn(self, conn, table_name):
|
||||||
return [str(row["name"]) for row in conn.execute(f"PRAGMA table_info({table_name})").fetchall()]
|
return [str(row["name"]) for row in conn.execute(f"PRAGMA table_info({table_name})").fetchall()]
|
||||||
@@ -3158,7 +3167,7 @@ def runtime_state():
|
|||||||
|
|
||||||
|
|
||||||
def build_runtime(start_workers=None):
|
def build_runtime(start_workers=None):
|
||||||
migrate_legacy_state_storage()
|
ensure_project_app_root()
|
||||||
worker_state = background_workers_enabled() if start_workers is None else bool(start_workers) and background_workers_enabled()
|
worker_state = background_workers_enabled() if start_workers is None else bool(start_workers) and background_workers_enabled()
|
||||||
config = normalize_config(load_json(CONFIG_PATH, DEFAULT_CONFIG))
|
config = normalize_config(load_json(CONFIG_PATH, DEFAULT_CONFIG))
|
||||||
write_json(CONFIG_PATH, config)
|
write_json(CONFIG_PATH, config)
|
||||||
|
|||||||
+2
-85
@@ -358,14 +358,6 @@ def ensure_project_app_root():
|
|||||||
return PROJECT_APP_ROOT
|
return PROJECT_APP_ROOT
|
||||||
|
|
||||||
|
|
||||||
def legacy_state_root():
|
|
||||||
return Path(os.environ.get("XDG_STATE_HOME", Path.home() / ".local" / "state")) / APP_NAME
|
|
||||||
|
|
||||||
|
|
||||||
def legacy_config_root():
|
|
||||||
return Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / APP_NAME
|
|
||||||
|
|
||||||
|
|
||||||
def project_state_path(*parts):
|
def project_state_path(*parts):
|
||||||
return PROJECT_APP_ROOT.joinpath(*parts)
|
return PROJECT_APP_ROOT.joinpath(*parts)
|
||||||
|
|
||||||
@@ -374,53 +366,6 @@ def project_state_dir(*parts):
|
|||||||
return PROJECT_APP_ROOT.joinpath(*parts)
|
return PROJECT_APP_ROOT.joinpath(*parts)
|
||||||
|
|
||||||
|
|
||||||
def migrate_legacy_file(project_path, legacy_path, label):
|
|
||||||
if project_path.exists() or not legacy_path.exists():
|
|
||||||
return project_path
|
|
||||||
project_path.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
try:
|
|
||||||
shutil.move(str(legacy_path), str(project_path))
|
|
||||||
debug_log("state.migrated", label=label, from_path=legacy_path, to_path=project_path)
|
|
||||||
except OSError as exc:
|
|
||||||
debug_log("state.migrate_failed", label=label, from_path=legacy_path, to_path=project_path, error=exc)
|
|
||||||
return project_path
|
|
||||||
|
|
||||||
|
|
||||||
def migrate_legacy_dir(project_dir, legacy_dir, label):
|
|
||||||
if not legacy_dir.exists():
|
|
||||||
return project_dir
|
|
||||||
project_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
moved = 0
|
|
||||||
for child in legacy_dir.iterdir():
|
|
||||||
target = project_dir / child.name
|
|
||||||
if target.exists():
|
|
||||||
if child.is_dir() and target.is_dir():
|
|
||||||
migrate_legacy_dir(target, child, f"{label}/{child.name}")
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
shutil.move(str(child), str(target))
|
|
||||||
moved += 1
|
|
||||||
except OSError as exc:
|
|
||||||
debug_log("state.migrate_failed", label=label, from_path=child, to_path=target, error=exc)
|
|
||||||
try:
|
|
||||||
legacy_dir.rmdir()
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
if moved:
|
|
||||||
debug_log("state.migrated", label=label, from_path=legacy_dir, to_path=project_dir, entries=moved)
|
|
||||||
return project_dir
|
|
||||||
|
|
||||||
|
|
||||||
LEGACY_STATE_ROOT = legacy_state_root()
|
|
||||||
LEGACY_CONFIG_ROOT = legacy_config_root()
|
|
||||||
LEGACY_CONFIG_PATH = LEGACY_CONFIG_ROOT / "config.json"
|
|
||||||
LEGACY_QUEUE_PATH = LEGACY_STATE_ROOT / "queue.json"
|
|
||||||
LEGACY_QUEUE_DB_PATH = LEGACY_STATE_ROOT / "queue.sqlite3"
|
|
||||||
LEGACY_STAGING_ROOT = LEGACY_STATE_ROOT / "staging"
|
|
||||||
LEGACY_WATCHLIST_JSON_PATH = LEGACY_STATE_ROOT / "watchlist.json"
|
|
||||||
LEGACY_THUMBNAIL_ROOT = LEGACY_STATE_ROOT / "thumbnails"
|
|
||||||
LEGACY_ANIDB_TITLES_PATH = LEGACY_STATE_ROOT / "anidb-anime-titles.xml.gz"
|
|
||||||
|
|
||||||
CONFIG_PATH = project_state_path("config.json")
|
CONFIG_PATH = project_state_path("config.json")
|
||||||
QUEUE_PATH = project_state_path("queue.json")
|
QUEUE_PATH = project_state_path("queue.json")
|
||||||
STATE_DB_PATH = project_state_path("state.sqlite3")
|
STATE_DB_PATH = project_state_path("state.sqlite3")
|
||||||
@@ -439,17 +384,6 @@ ANIDB_TITLES_CACHE = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def migrate_legacy_state_storage():
|
|
||||||
ensure_project_app_root()
|
|
||||||
migrate_legacy_file(CONFIG_PATH, LEGACY_CONFIG_PATH, "config.json")
|
|
||||||
migrate_legacy_file(QUEUE_PATH, LEGACY_QUEUE_PATH, "queue.json")
|
|
||||||
migrate_legacy_file(STATE_DB_PATH, PROJECT_APP_ROOT / "queue.sqlite3", "queue.sqlite3")
|
|
||||||
migrate_legacy_file(STATE_DB_PATH, LEGACY_QUEUE_DB_PATH, "queue.sqlite3")
|
|
||||||
migrate_legacy_file(WATCHLIST_JSON_PATH, LEGACY_WATCHLIST_JSON_PATH, "watchlist.json")
|
|
||||||
migrate_legacy_file(ANIDB_TITLES_PATH, LEGACY_ANIDB_TITLES_PATH, "anidb-anime-titles.xml.gz")
|
|
||||||
migrate_legacy_dir(STAGING_ROOT, LEGACY_STAGING_ROOT, "staging")
|
|
||||||
migrate_legacy_dir(THUMBNAIL_ROOT, LEGACY_THUMBNAIL_ROOT, "thumbnails")
|
|
||||||
|
|
||||||
def load_json(path, fallback):
|
def load_json(path, fallback):
|
||||||
try:
|
try:
|
||||||
with path.open("r", encoding="utf-8") as handle:
|
with path.open("r", encoding="utf-8") as handle:
|
||||||
@@ -580,7 +514,7 @@ def _env_remote_path_roots():
|
|||||||
if not raw:
|
if not raw:
|
||||||
return []
|
return []
|
||||||
roots = []
|
roots = []
|
||||||
for part in raw.split(os.pathsep):
|
for part in re.split(rf"[{re.escape(os.pathsep)},]", raw):
|
||||||
text = str(part or "").strip()
|
text = str(part or "").strip()
|
||||||
if not text:
|
if not text:
|
||||||
continue
|
continue
|
||||||
@@ -660,20 +594,7 @@ def thumbnail_file_path(name):
|
|||||||
filename = Path(str(name or "")).name
|
filename = Path(str(name or "")).name
|
||||||
if not filename:
|
if not filename:
|
||||||
return None
|
return None
|
||||||
project_path = THUMBNAIL_ROOT / filename
|
return THUMBNAIL_ROOT / filename
|
||||||
if project_path.exists():
|
|
||||||
return project_path
|
|
||||||
legacy_path = LEGACY_THUMBNAIL_ROOT / filename
|
|
||||||
if legacy_path.exists():
|
|
||||||
THUMBNAIL_ROOT.mkdir(parents=True, exist_ok=True)
|
|
||||||
try:
|
|
||||||
shutil.move(str(legacy_path), str(project_path))
|
|
||||||
debug_log("thumbnail.cache.migrated", from_path=legacy_path, to_path=project_path)
|
|
||||||
return project_path
|
|
||||||
except OSError as exc:
|
|
||||||
debug_log("thumbnail.cache.migrate_failed", from_path=legacy_path, to_path=project_path, error=exc)
|
|
||||||
return legacy_path
|
|
||||||
return project_path
|
|
||||||
|
|
||||||
|
|
||||||
def _discord_file_part(boundary, field_name, attachment_name, content_type, payload):
|
def _discord_file_part(boundary, field_name, attachment_name, content_type, payload):
|
||||||
@@ -1017,10 +938,6 @@ def cli_executable_exists(command):
|
|||||||
return bool(shutil.which(text) or (Path(text).exists() and os.access(text, os.X_OK)))
|
return bool(shutil.which(text) or (Path(text).exists() and os.access(text, os.X_OK)))
|
||||||
|
|
||||||
|
|
||||||
def cli_executable_exists_any(*commands):
|
|
||||||
return any(cli_executable_exists(command) for command in commands)
|
|
||||||
|
|
||||||
|
|
||||||
def printable_command(command):
|
def printable_command(command):
|
||||||
return " ".join(sh_quote(part) for part in command)
|
return " ".join(sh_quote(part) for part in command)
|
||||||
|
|
||||||
|
|||||||
+1
-4
@@ -6,7 +6,6 @@ import json
|
|||||||
import mimetypes
|
import mimetypes
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
|
||||||
import traceback
|
import traceback
|
||||||
from base64 import b64decode
|
from base64 import b64decode
|
||||||
from binascii import Error as BinasciiError
|
from binascii import Error as BinasciiError
|
||||||
@@ -34,8 +33,6 @@ from app_support import (
|
|||||||
debug_log,
|
debug_log,
|
||||||
env_value,
|
env_value,
|
||||||
load_project_text_file,
|
load_project_text_file,
|
||||||
cli_executable_exists_any,
|
|
||||||
cli_executable_exists,
|
|
||||||
normalize_config,
|
normalize_config,
|
||||||
path_is_within_roots,
|
path_is_within_roots,
|
||||||
remote_access_allowed,
|
remote_access_allowed,
|
||||||
@@ -94,7 +91,7 @@ def build_handler_class(context):
|
|||||||
|
|
||||||
|
|
||||||
def dependency_status():
|
def dependency_status():
|
||||||
checks = ["node", "python3", "ffmpeg"]
|
checks = ["node", "python3", "ffmpeg", "openssl"]
|
||||||
result = {name: bool(shutil.which(name)) for name in checks}
|
result = {name: bool(shutil.which(name)) for name in checks}
|
||||||
result["provider-bridge"] = provider_bridge.BRIDGE.exists()
|
result["provider-bridge"] = provider_bridge.BRIDGE.exists()
|
||||||
return result
|
return result
|
||||||
|
|||||||
+29
-3
@@ -11,6 +11,7 @@ import subprocess
|
|||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import re
|
import re
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
from app_support import (
|
from app_support import (
|
||||||
DOWNLOAD_METHOD_CHOICES,
|
DOWNLOAD_METHOD_CHOICES,
|
||||||
@@ -85,10 +86,18 @@ class WatchlistRefreshJobs:
|
|||||||
if start_worker:
|
if start_worker:
|
||||||
self.ensure_worker()
|
self.ensure_worker()
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
def _connect(self):
|
def _connect(self):
|
||||||
conn = sqlite3.connect(STATE_DB_PATH, timeout=30)
|
conn = sqlite3.connect(STATE_DB_PATH, timeout=30)
|
||||||
conn.row_factory = sqlite3.Row
|
conn.row_factory = sqlite3.Row
|
||||||
return conn
|
try:
|
||||||
|
yield conn
|
||||||
|
conn.commit()
|
||||||
|
except Exception:
|
||||||
|
conn.rollback()
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
def ensure_worker(self):
|
def ensure_worker(self):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
@@ -553,10 +562,18 @@ class JellyfinSyncJobs:
|
|||||||
if start_worker:
|
if start_worker:
|
||||||
self.ensure_worker()
|
self.ensure_worker()
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
def _connect(self):
|
def _connect(self):
|
||||||
conn = sqlite3.connect(STATE_DB_PATH, timeout=30)
|
conn = sqlite3.connect(STATE_DB_PATH, timeout=30)
|
||||||
conn.row_factory = sqlite3.Row
|
conn.row_factory = sqlite3.Row
|
||||||
return conn
|
try:
|
||||||
|
yield conn
|
||||||
|
conn.commit()
|
||||||
|
except Exception:
|
||||||
|
conn.rollback()
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
def ensure_worker(self):
|
def ensure_worker(self):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
@@ -932,10 +949,18 @@ class DownloadQueue:
|
|||||||
if start_worker:
|
if start_worker:
|
||||||
self.ensure_worker()
|
self.ensure_worker()
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
def _connect(self):
|
def _connect(self):
|
||||||
conn = sqlite3.connect(STATE_DB_PATH, timeout=30)
|
conn = sqlite3.connect(STATE_DB_PATH, timeout=30)
|
||||||
conn.row_factory = sqlite3.Row
|
conn.row_factory = sqlite3.Row
|
||||||
return conn
|
try:
|
||||||
|
yield conn
|
||||||
|
conn.commit()
|
||||||
|
except Exception:
|
||||||
|
conn.rollback()
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
def ensure_worker(self):
|
def ensure_worker(self):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
@@ -1222,6 +1247,7 @@ class DownloadQueue:
|
|||||||
str(job.get("season") or "").strip(),
|
str(job.get("season") or "").strip(),
|
||||||
str(job.get("episode_offset") or "").strip(),
|
str(job.get("episode_offset") or "").strip(),
|
||||||
str(job.get("query") or "").strip(),
|
str(job.get("query") or "").strip(),
|
||||||
|
str(job.get("provider") or "").strip().lower(),
|
||||||
job.get("result_index"),
|
job.get("result_index"),
|
||||||
str(job.get("mode") or "").strip().lower(),
|
str(job.get("mode") or "").strip().lower(),
|
||||||
str(job.get("quality") or "").strip().lower(),
|
str(job.get("quality") or "").strip().lower(),
|
||||||
|
|||||||
+44
@@ -1097,6 +1097,40 @@ class DownloadQueueWorkerFailureTests(unittest.TestCase):
|
|||||||
self.assertEqual([command[0] for command in commands], ["python3"])
|
self.assertEqual([command[0] for command in commands], ["python3"])
|
||||||
self.assertIn("Download completed.", stored["log"])
|
self.assertIn("Download completed.", stored["log"])
|
||||||
|
|
||||||
|
def test_failed_job_reuse_keeps_providers_separate(self):
|
||||||
|
queue = APP.DownloadQueue(
|
||||||
|
lambda: {"mode": "sub", "quality": "best", "download_dir": "/tmp/example"},
|
||||||
|
start_worker=False,
|
||||||
|
)
|
||||||
|
with queue._connect() as conn:
|
||||||
|
conn.execute("DELETE FROM jobs")
|
||||||
|
first = queue.add(
|
||||||
|
{
|
||||||
|
"show_id": "queue-show",
|
||||||
|
"provider": "anikoto",
|
||||||
|
"query": "Queue Show",
|
||||||
|
"title": "Queue Show",
|
||||||
|
"season": "1",
|
||||||
|
"episodes": "1",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
first["status"] = "failed"
|
||||||
|
queue._save_job_locked(first)
|
||||||
|
|
||||||
|
second = queue.add(
|
||||||
|
{
|
||||||
|
"show_id": "queue-show",
|
||||||
|
"provider": "anineko",
|
||||||
|
"query": "Queue Show",
|
||||||
|
"title": "Queue Show",
|
||||||
|
"season": "1",
|
||||||
|
"episodes": "1",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertNotEqual(first["id"], second["id"])
|
||||||
|
self.assertEqual(second["provider"], "anineko")
|
||||||
|
|
||||||
|
|
||||||
class WatchlistRefreshAllTests(unittest.TestCase):
|
class WatchlistRefreshAllTests(unittest.TestCase):
|
||||||
def test_refresh_all_iterates_only_watching_and_planned_show_ids(self):
|
def test_refresh_all_iterates_only_watching_and_planned_show_ids(self):
|
||||||
@@ -2678,6 +2712,14 @@ class ConfigSnapshotTests(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(config["download_methods"], ["kaizoku"])
|
self.assertEqual(config["download_methods"], ["kaizoku"])
|
||||||
|
|
||||||
|
def test_remote_path_roots_accept_comma_and_platform_separators(self):
|
||||||
|
with tempfile.TemporaryDirectory() as first_root, tempfile.TemporaryDirectory() as second_root, tempfile.TemporaryDirectory() as third_root:
|
||||||
|
raw = f"{first_root},{second_root}{os.pathsep}{third_root}"
|
||||||
|
with mock.patch.dict(os.environ, {"KAIZOKU_REMOTE_PATH_ROOTS": raw}, clear=False):
|
||||||
|
roots = APP.app_support._env_remote_path_roots()
|
||||||
|
|
||||||
|
self.assertEqual(roots, [Path(first_root).resolve(), Path(second_root).resolve(), Path(third_root).resolve()])
|
||||||
|
|
||||||
def test_normalize_config_filters_discord_webhook_events(self):
|
def test_normalize_config_filters_discord_webhook_events(self):
|
||||||
config = APP.normalize_config(
|
config = APP.normalize_config(
|
||||||
{
|
{
|
||||||
@@ -3301,6 +3343,7 @@ class HandlerRouteTests(unittest.TestCase):
|
|||||||
self.assertIn("node", handler.json_payload)
|
self.assertIn("node", handler.json_payload)
|
||||||
self.assertIn("python3", handler.json_payload)
|
self.assertIn("python3", handler.json_payload)
|
||||||
self.assertIn("ffmpeg", handler.json_payload)
|
self.assertIn("ffmpeg", handler.json_payload)
|
||||||
|
self.assertIn("openssl", handler.json_payload)
|
||||||
self.assertIn("provider-bridge", handler.json_payload)
|
self.assertIn("provider-bridge", handler.json_payload)
|
||||||
|
|
||||||
def test_dependency_status_checks_provider_runtime(self):
|
def test_dependency_status_checks_provider_runtime(self):
|
||||||
@@ -3315,6 +3358,7 @@ class HandlerRouteTests(unittest.TestCase):
|
|||||||
self.assertTrue(status["node"])
|
self.assertTrue(status["node"])
|
||||||
self.assertTrue(status["python3"])
|
self.assertTrue(status["python3"])
|
||||||
self.assertTrue(status["ffmpeg"])
|
self.assertTrue(status["ffmpeg"])
|
||||||
|
self.assertTrue(status["openssl"])
|
||||||
self.assertTrue(status["provider-bridge"])
|
self.assertTrue(status["provider-bridge"])
|
||||||
|
|
||||||
def test_clear_failed_route_removes_failed_jobs(self):
|
def test_clear_failed_route_removes_failed_jobs(self):
|
||||||
|
|||||||
Reference in New Issue
Block a user