/
/
/
1"""Tests that library sync batches database writes into few commits."""
2
3from __future__ import annotations
4
5from typing import TYPE_CHECKING
6from unittest.mock import MagicMock
7
8import pytest
9from music_assistant_models.enums import ProviderType
10
11from music_assistant.constants import CONF_LOG_LEVEL, DB_TABLE_ARTISTS, DB_TABLE_TRACKS
12from music_assistant.controllers.music import MusicController
13from music_assistant.providers.test import (
14 CONF_KEY_NUM_ALBUMS,
15 CONF_KEY_NUM_ARTISTS,
16 CONF_KEY_NUM_TRACKS,
17)
18from music_assistant.providers.test import TestProvider as FakeMusicProvider
19
20if TYPE_CHECKING:
21 from collections.abc import AsyncGenerator
22
23 from music_assistant.mass import MusicAssistant
24
25NUM_ARTISTS = 2
26NUM_ALBUMS = 2
27NUM_TRACKS = 3 # per album
28TOTAL_TRACKS = NUM_ARTISTS * NUM_ALBUMS * NUM_TRACKS
29
30
31@pytest.fixture
32async def music(mass_minimal: MusicAssistant) -> AsyncGenerator[MusicController]:
33 """Return a music controller with initialized database on the minimal mass instance."""
34 controller = MusicController(mass_minimal)
35 mass_minimal.music = controller
36 await controller._setup_database()
37 yield controller
38 if controller._database:
39 await controller._database.close()
40
41
42@pytest.fixture
43def provider(mass_minimal: MusicAssistant) -> FakeMusicProvider:
44 """Return a fake music provider with a small deterministic library."""
45 manifest = MagicMock()
46 manifest.type = ProviderType.MUSIC
47 manifest.domain = "test"
48 config = MagicMock()
49 config.instance_id = "test--1"
50 config.domain = "test"
51 values = {
52 CONF_KEY_NUM_ARTISTS: NUM_ARTISTS,
53 CONF_KEY_NUM_ALBUMS: NUM_ALBUMS,
54 CONF_KEY_NUM_TRACKS: NUM_TRACKS,
55 CONF_LOG_LEVEL: "GLOBAL",
56 }
57 config.get_value.side_effect = lambda key, default=None: values.get(key, default)
58 return FakeMusicProvider(mass_minimal, manifest, config)
59
60
61async def test_track_sync_commits_once_per_item(
62 music: MusicController, provider: FakeMusicProvider
63) -> None:
64 """
65 Test that an initial track sync performs roughly one commit per synced item.
66
67 Adding a single track involves many statements (track row, provider mappings,
68 artists, album, album/track relations, genre mappings) which previously each
69 committed individually (6-10 commits per track).
70 """
71 commits = 0
72 original_commit = music.database._db.commit
73
74 async def counting_commit() -> None:
75 nonlocal commits
76 commits += 1
77 await original_commit()
78
79 music.database._db.commit = counting_commit # type: ignore[method-assign]
80
81 cur_db_ids = await provider._sync_library_tracks()
82
83 # all tracks (and their artists/albums) actually landed in the library
84 assert len(cur_db_ids) == TOTAL_TRACKS
85 assert await music.database.get_count(DB_TABLE_TRACKS) == TOTAL_TRACKS
86 assert await music.database.get_count(DB_TABLE_ARTISTS) == NUM_ARTISTS
87 # every synced item is batched into a single commit
88 # (per-statement commits produce ~7x more here: 88 commits for these 12 tracks)
89 assert 0 < commits <= TOTAL_TRACKS + 2
90