Files
musicdl-catalog-sync-suite/catalog-sync/tests/catalogsync/test_ops_db.py
T

355 lines
13 KiB
Python

import tempfile
import unittest
from pathlib import Path
class OpsDatabaseSchemaTests(unittest.TestCase):
def test_initialize_database_creates_operations_tables(self):
from musicdl.catalogsync.db import initialize_database
expected_tables = {
"job_runs",
"job_stages",
"job_items",
"job_workers",
"job_commands",
"job_events",
"job_logs",
"config_revisions",
}
with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir:
db_path = Path(tmpdir) / "catalogsync.db"
conn = initialize_database(db_path)
conn.close()
verify_conn = initialize_database(db_path)
table_rows = verify_conn.execute(
"SELECT name FROM sqlite_master WHERE type = 'table'"
).fetchall()
verify_conn.close()
tables = {row["name"] for row in table_rows}
self.assertTrue(expected_tables.issubset(tables))
def test_initialize_database_operations_core_columns_match_baseline(self):
from musicdl.catalogsync.db import initialize_database
expected_job_runs = {
"id",
"job_type",
"status",
"priority",
"requested_by",
"config_snapshot_json",
"sources",
"download_sources",
"playlist_scope_json",
"created_at",
"started_at",
"ended_at",
"last_error",
"resume_token",
}
expected_job_stages = {
"id",
"job_run_id",
"stage_type",
"status",
"seq_no",
"total_items",
"pending_items",
"running_items",
"success_items",
"failed_items",
"skipped_items",
"started_at",
"ended_at",
"last_error",
}
expected_job_items = {
"id",
"job_stage_id",
"item_type",
"item_key",
"playlist_pool_id",
"playlist_id",
"song_id",
"file_location_id",
"status",
"attempt_count",
"max_attempts",
"worker_id",
"started_at",
"ended_at",
"last_error",
"last_error_code",
"payload_json",
}
with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir:
db_path = Path(tmpdir) / "catalogsync.db"
conn = initialize_database(db_path)
job_runs_cols = {
row["name"] for row in conn.execute("PRAGMA table_info(job_runs)").fetchall()
}
job_stages_cols = {
row["name"] for row in conn.execute("PRAGMA table_info(job_stages)").fetchall()
}
job_items_cols = {
row["name"] for row in conn.execute("PRAGMA table_info(job_items)").fetchall()
}
conn.close()
self.assertTrue(expected_job_runs.issubset(job_runs_cols))
self.assertTrue(expected_job_stages.issubset(job_stages_cols))
self.assertTrue(expected_job_items.issubset(job_items_cols))
def test_initialize_database_config_revisions_source_type_has_default(self):
from musicdl.catalogsync.db import initialize_database
with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir:
db_path = Path(tmpdir) / "catalogsync.db"
conn = initialize_database(db_path)
columns = conn.execute("PRAGMA table_info(config_revisions)").fetchall()
conn.close()
source_type_col = next(row for row in columns if row["name"] == "source_type")
self.assertEqual("'env_file'", source_type_col["dflt_value"])
class OpsRepositoryTests(unittest.TestCase):
def test_create_job_requires_config_snapshot(self):
from musicdl.catalogsync.db import initialize_database
from musicdl.catalogsync.ops.repository import OpsRepository
with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir:
db_path = Path(tmpdir) / "catalogsync.db"
initialize_database(db_path).close()
repo = OpsRepository(db_path)
with self.assertRaises(ValueError):
repo.create_job(job_type="catalog_sync", config_snapshot=None)
def test_create_and_read_job_stage_item_with_default_status(self):
from musicdl.catalogsync.db import initialize_database
from musicdl.catalogsync.ops.models import ItemStatus, JobStatus, StageStatus
from musicdl.catalogsync.ops.repository import OpsRepository
with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir:
db_path = Path(tmpdir) / "catalogsync.db"
initialize_database(db_path).close()
repo = OpsRepository(db_path)
job_id = repo.create_job(
job_type="catalog_sync",
config_snapshot={
"sources": ["qq", "netease"],
"download": {"enabled": True},
},
sources=["qq", "netease"],
download_sources=["qq"],
)
stage_id = repo.create_stage(job_run_id=job_id, stage_type="collect", seq_no=1)
item_id = repo.create_item(
job_stage_id=stage_id,
item_type="song_sync",
item_key="qq:playlist:123",
song_id=12345,
payload={"platform": "qq"},
)
job = repo.get_job(job_id)
stage = repo.get_stage(stage_id)
item = repo.get_item(item_id)
self.assertIsNotNone(job)
self.assertEqual(JobStatus.QUEUED, job.status)
self.assertEqual(100, job.priority)
self.assertEqual({"sources": ["qq", "netease"], "download": {"enabled": True}}, job.config_snapshot)
self.assertEqual(["qq", "netease"], job.sources)
self.assertEqual(["qq"], job.download_sources)
self.assertIsNotNone(stage)
self.assertEqual(StageStatus.PENDING, stage.status)
self.assertEqual(job_id, stage.job_run_id)
self.assertEqual(1, stage.total_items)
self.assertEqual(1, stage.pending_items)
self.assertEqual(0, stage.running_items)
self.assertEqual(0, stage.success_items)
self.assertEqual(1, stage.seq_no)
self.assertIsNotNone(item)
self.assertEqual(ItemStatus.PENDING, item.status)
self.assertEqual(stage_id, item.job_stage_id)
self.assertEqual(0, item.attempt_count)
self.assertEqual(3, item.max_attempts)
self.assertEqual("song_sync", item.item_type)
self.assertEqual(12345, item.song_id)
self.assertEqual("qq", item.payload["platform"])
def test_create_job_allows_empty_config_snapshot_and_reads_back(self):
from musicdl.catalogsync.db import initialize_database
from musicdl.catalogsync.ops.repository import OpsRepository
with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir:
db_path = Path(tmpdir) / "catalogsync.db"
initialize_database(db_path).close()
repo = OpsRepository(db_path)
job_id = repo.create_job(
job_type="catalog_sync",
config_snapshot={},
sources=["qq", "netease"],
download_sources=["qq"],
)
job = repo.get_job(job_id)
self.assertEqual({}, job.config_snapshot)
self.assertEqual(["qq", "netease"], job.sources)
self.assertEqual(["qq"], job.download_sources)
def test_create_item_with_failed_status_updates_stage_aggregate_counts(self):
from musicdl.catalogsync.db import initialize_database
from musicdl.catalogsync.ops.models import ItemStatus
from musicdl.catalogsync.ops.repository import OpsRepository
with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir:
db_path = Path(tmpdir) / "catalogsync.db"
initialize_database(db_path).close()
repo = OpsRepository(db_path)
job_id = repo.create_job(job_type="catalog_sync", config_snapshot={})
stage_id = repo.create_stage(job_run_id=job_id, stage_type="collect", seq_no=1)
repo.create_item(
job_stage_id=stage_id,
item_type="song_sync",
item_key="qq:song:failed:1",
status=ItemStatus.FAILED,
)
stage = repo.get_stage(stage_id)
self.assertEqual(1, stage.total_items)
self.assertEqual(0, stage.pending_items)
self.assertEqual(1, stage.failed_items)
def test_list_job_playlist_progress_aggregates_download_states_per_playlist(self):
from musicdl.catalogsync.db import initialize_database
from musicdl.catalogsync.models import CatalogSong, PlaylistCandidate
from musicdl.catalogsync.ops.models import ItemStatus
from musicdl.catalogsync.ops.repository import OpsRepository
from musicdl.catalogsync.repository import CatalogRepository
with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir:
root = Path(tmpdir)
db_path = root / "catalogsync.db"
library_root = root / "library"
initialize_database(db_path, default_library_root=library_root).close()
catalog_repo = CatalogRepository(db_path)
ops_repo = OpsRepository(db_path)
playlist_id = catalog_repo.upsert_playlist(
PlaylistCandidate(
platform="qq",
pool_kind="manual_file",
remote_id="job-progress-1",
name="Job Progress Playlist",
url="https://example.invalid/qq/job-progress-1",
)
)
pool_id = catalog_repo.upsert_playlist_pool(
platform="qq",
pool_kind="manual_file",
external_id="manual_file:job-progress-1",
name="Manual Pool",
url="https://example.invalid/pool/job-progress-1",
)
catalog_repo.link_pool_playlist(pool_id, playlist_id)
downloaded_song_id = catalog_repo.upsert_song(
CatalogSong(
platform="qq",
remote_song_id="job-song-1",
name="Downloaded Song",
singers="Singer A",
ext="mp3",
file_size_bytes=128,
quality_label="standard",
)
)
running_song_id = catalog_repo.upsert_song(
CatalogSong(
platform="qq",
remote_song_id="job-song-2",
name="Running Song",
singers="Singer A",
ext="mp3",
file_size_bytes=128,
quality_label="standard",
)
)
failed_song_id = catalog_repo.upsert_song(
CatalogSong(
platform="qq",
remote_song_id="job-song-3",
name="Failed Song",
singers="Singer A",
ext="mp3",
file_size_bytes=128,
quality_label="standard",
)
)
catalog_repo.link_playlist_song(playlist_id, downloaded_song_id, 1)
catalog_repo.link_playlist_song(playlist_id, running_song_id, 2)
catalog_repo.link_playlist_song(playlist_id, failed_song_id, 3)
backend_id = catalog_repo.get_default_backend_id()
catalog_repo.record_local_file(
song_id=downloaded_song_id,
backend_id=backend_id,
relative_path="qq/Singer A/job-song-1.mp3",
file_size_bytes=128,
ext="mp3",
quality_label="standard",
)
job_id = ops_repo.create_job(
job_type="download_only",
config_snapshot={},
playlist_scope={"playlist_ids": [playlist_id]},
)
stage_id = ops_repo.create_stage(job_run_id=job_id, stage_type="download", seq_no=1)
ops_repo.create_item(
job_stage_id=stage_id,
item_type="song_download",
item_key=f"song:{running_song_id}",
song_id=running_song_id,
status=ItemStatus.RUNNING,
)
ops_repo.create_item(
job_stage_id=stage_id,
item_type="song_download",
item_key=f"song:{failed_song_id}",
song_id=failed_song_id,
status=ItemStatus.FAILED,
)
rows = ops_repo.list_job_playlist_progress(job_id)
self.assertEqual(1, len(rows))
row = rows[0]
self.assertEqual(playlist_id, row["playlist_id"])
self.assertEqual("Job Progress Playlist", row["playlist_name"])
self.assertEqual(3, row["total_songs"])
self.assertEqual(1, row["downloaded_songs"])
self.assertEqual(1, row["running_songs"])
self.assertEqual(0, row["pending_songs"])
self.assertEqual(1, row["failed_songs"])
self.assertEqual(0, row["skipped_songs"])
self.assertEqual(33, row["progress_percent"])
if __name__ == "__main__":
unittest.main()