✅ | Add tests
Some checks failed
SonarQube Code Quality Scan / SonarQube Scan (push) Failing after 2m37s
Some checks failed
SonarQube Code Quality Scan / SonarQube Scan (push) Failing after 2m37s
This commit is contained in:
246
tests/test_music_db.py
Normal file
246
tests/test_music_db.py
Normal file
@@ -0,0 +1,246 @@
|
||||
"""Tests for the music database module (music_db.py)."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
|
||||
# We'll create a fresh MusicDB for each test using a temporary file.
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def music_db():
|
||||
"""Yield a MusicDB backed by a temporary SQLite file."""
|
||||
from music_db import MusicDB
|
||||
|
||||
tmp = tempfile.NamedTemporaryFile(suffix=".db", delete=False)
|
||||
tmp.close()
|
||||
db = MusicDB(tmp.name)
|
||||
yield db
|
||||
db.close()
|
||||
os.unlink(tmp.name)
|
||||
|
||||
|
||||
# ── Helper ────────────────────────────────────────────────────────────────────
|
||||
|
||||
_SONG = dict(
|
||||
video_id="test_video_001",
|
||||
title="Test Song",
|
||||
artists=[{"name": "Test Artist", "id": "artist_001"}],
|
||||
album={"name": "Test Album", "id": "album_001"},
|
||||
duration=240,
|
||||
thumbnail="https://example.com/thumb.jpg",
|
||||
)
|
||||
|
||||
|
||||
def _record_play(db, **overrides):
|
||||
kwargs = dict(_SONG)
|
||||
kwargs.update(overrides)
|
||||
db.record_play(**kwargs)
|
||||
|
||||
|
||||
# ── Tests ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestRecordPlay:
|
||||
def test_records_play_and_history(self, music_db):
|
||||
"""record_play creates a song entry and a history row."""
|
||||
_record_play(music_db)
|
||||
stats = music_db.get_stats()
|
||||
assert stats["unique_songs"] == 1
|
||||
assert stats["total_plays"] == 1
|
||||
|
||||
history = music_db.get_history(limit=10)
|
||||
assert len(history) == 1
|
||||
assert history[0]["video_id"] == _SONG["video_id"]
|
||||
|
||||
def test_increments_play_count(self, music_db):
|
||||
"""Playing the same song multiple times increments play_count."""
|
||||
_record_play(music_db)
|
||||
_record_play(music_db)
|
||||
_record_play(music_db)
|
||||
stats = music_db.get_stats()
|
||||
assert stats["unique_songs"] == 1
|
||||
assert stats["total_plays"] == 3
|
||||
|
||||
def test_updates_last_played(self, music_db):
|
||||
"""Re-playing a song updates last_played timestamp."""
|
||||
_record_play(music_db)
|
||||
import time
|
||||
time.sleep(0.01)
|
||||
_record_play(music_db)
|
||||
history = music_db.get_history(limit=10)
|
||||
# The latest play should be the one returned
|
||||
assert history[0]["video_id"] == _SONG["video_id"]
|
||||
|
||||
def test_without_album(self, music_db):
|
||||
"""record_play works without album data."""
|
||||
_record_play(music_db, album=None)
|
||||
stats = music_db.get_stats()
|
||||
assert stats["unique_songs"] == 1
|
||||
|
||||
def test_without_thumbnail(self, music_db):
|
||||
"""record_play works without thumbnail."""
|
||||
_record_play(music_db, thumbnail=None)
|
||||
stats = music_db.get_stats()
|
||||
assert stats["unique_songs"] == 1
|
||||
|
||||
|
||||
class TestFavourites:
|
||||
def test_toggle_favourite(self, music_db):
|
||||
"""toggle_favourite flips the favourite flag."""
|
||||
_record_play(music_db)
|
||||
# Start unfavoured
|
||||
assert music_db.is_favourite(_SONG["video_id"]) is False
|
||||
# Toggle on
|
||||
assert music_db.toggle_favourite(_SONG["video_id"]) is True
|
||||
assert music_db.is_favourite(_SONG["video_id"]) is True
|
||||
# Toggle off
|
||||
assert music_db.toggle_favourite(_SONG["video_id"]) is False
|
||||
assert music_db.is_favourite(_SONG["video_id"]) is False
|
||||
|
||||
def test_toggle_favourite_nonexistent_song(self, music_db):
|
||||
"""toggle_favourite on a non-existent song returns False."""
|
||||
assert music_db.toggle_favourite("nonexistent") is False
|
||||
|
||||
def test_set_favourite(self, music_db):
|
||||
"""set_favourite explicitly sets the favourite state."""
|
||||
_record_play(music_db)
|
||||
vid = _SONG["video_id"]
|
||||
assert music_db.set_favourite(vid, True) is True
|
||||
assert music_db.is_favourite(vid) is True
|
||||
assert music_db.set_favourite(vid, False) is True
|
||||
assert music_db.is_favourite(vid) is False
|
||||
|
||||
def test_set_favourite_nonexistent(self, music_db):
|
||||
"""set_favourite on a non-existent song returns False."""
|
||||
assert music_db.set_favourite("nonexistent", True) is False
|
||||
|
||||
def test_get_favourites_returns_favourited_songs(self, music_db):
|
||||
"""get_favourites returns only favourited songs."""
|
||||
# Record two songs, favourite one
|
||||
_record_play(music_db, video_id="song_a", title="A")
|
||||
_record_play(music_db, video_id="song_b", title="B")
|
||||
music_db.toggle_favourite("song_a")
|
||||
favs = music_db.get_favourites()
|
||||
assert len(favs) == 1
|
||||
assert favs[0]["video_id"] == "song_a"
|
||||
|
||||
|
||||
class TestHistory:
|
||||
def test_history_ordered_by_recent(self, music_db):
|
||||
"""get_history returns songs ordered by most recent play."""
|
||||
_record_play(music_db, video_id="song_a", title="A")
|
||||
import time
|
||||
time.sleep(0.01)
|
||||
_record_play(music_db, video_id="song_b", title="B")
|
||||
history = music_db.get_history(limit=10)
|
||||
# Most recent (song_b) should be first
|
||||
assert history[0]["video_id"] == "song_b"
|
||||
assert history[1]["video_id"] == "song_a"
|
||||
|
||||
def test_history_deduplicates_by_video_id(self, music_db):
|
||||
"""get_history shows each song at most once (most recent play)."""
|
||||
_record_play(music_db, video_id="song_a", title="A")
|
||||
_record_play(music_db, video_id="song_b", title="B")
|
||||
_record_play(music_db, video_id="song_a", title="A")
|
||||
history = music_db.get_history(limit=10)
|
||||
# song_a appears once, but song_b is in there too
|
||||
assert len(history) == 2
|
||||
# song_a is most recent since it was played last
|
||||
assert history[0]["video_id"] == "song_a"
|
||||
|
||||
def test_history_respects_limit(self, music_db):
|
||||
"""get_history respects the limit parameter."""
|
||||
for i in range(5):
|
||||
_record_play(music_db, video_id=f"song_{i}", title=f"S{i}")
|
||||
history = music_db.get_history(limit=3)
|
||||
assert len(history) == 3
|
||||
|
||||
def test_empty_history(self, music_db):
|
||||
"""get_history on empty DB returns empty list."""
|
||||
assert music_db.get_history() == []
|
||||
|
||||
|
||||
class TestStats:
|
||||
def test_get_stats_empty(self, music_db):
|
||||
"""get_stats on empty DB returns zero counts."""
|
||||
stats = music_db.get_stats()
|
||||
assert stats["unique_songs"] == 0
|
||||
assert stats["total_plays"] is None or stats["total_plays"] == 0
|
||||
assert stats["favourites"] == 0
|
||||
assert stats["top_played"] == []
|
||||
assert stats["recent"] == []
|
||||
assert stats["plays_by_day"] == []
|
||||
|
||||
def test_get_stats_with_data(self, music_db):
|
||||
"""get_stats returns accurate aggregate data."""
|
||||
_record_play(music_db, video_id="song_a", title="A", duration=120)
|
||||
_record_play(music_db, video_id="song_b", title="B", duration=180)
|
||||
_record_play(music_db, video_id="song_a", title="A", duration=120)
|
||||
music_db.toggle_favourite("song_b")
|
||||
|
||||
stats = music_db.get_stats()
|
||||
assert stats["unique_songs"] == 2
|
||||
assert stats["total_plays"] == 3
|
||||
assert stats["favourites"] == 1
|
||||
assert stats["top_played"][0]["video_id"] == "song_a"
|
||||
assert stats["top_played"][0]["play_count"] == 2
|
||||
assert len(stats["recent"]) == 3
|
||||
assert len(stats["plays_by_day"]) >= 1 # today at least
|
||||
|
||||
def test_top_played_limit_returns_up_to_ten(self, music_db):
|
||||
"""top_played in stats returns at most 10 entries."""
|
||||
for i in range(15):
|
||||
_record_play(music_db, video_id=f"song_{i}", title=f"S{i}")
|
||||
for _ in range(i + 1):
|
||||
_record_play(music_db, video_id=f"song_{i}", title=f"S{i}")
|
||||
stats = music_db.get_stats()
|
||||
assert len(stats["top_played"]) == 10
|
||||
|
||||
|
||||
class TestEdgeCases:
|
||||
def test_unicode_song_title(self, music_db):
|
||||
"""record_play handles Unicode characters in titles and artists."""
|
||||
_record_play(music_db, title="🎵 Джаз", artists=[{"name": "José", "id": "j"}])
|
||||
history = music_db.get_history(limit=10)
|
||||
assert history[0]["title"] == "🎵 Джаз"
|
||||
|
||||
def test_empty_artists_list(self, music_db):
|
||||
"""record_play works with empty artists list."""
|
||||
_record_play(music_db, artists=[])
|
||||
stats = music_db.get_stats()
|
||||
assert stats["unique_songs"] == 1
|
||||
|
||||
def test_very_long_title(self, music_db):
|
||||
"""record_play handles very long titles."""
|
||||
long_title = "A" * 1000
|
||||
_record_play(music_db, title=long_title)
|
||||
history = music_db.get_history(limit=10)
|
||||
assert history[0]["title"] == long_title
|
||||
|
||||
def test_concurrent_access(self, music_db):
|
||||
"""Multiple rapid calls don't cause errors."""
|
||||
import threading
|
||||
|
||||
errors = []
|
||||
|
||||
def worker(vid):
|
||||
try:
|
||||
_record_play(music_db, video_id=vid, title=f"S{vid}")
|
||||
music_db.toggle_favourite(vid)
|
||||
music_db.is_favourite(vid)
|
||||
music_db.get_stats()
|
||||
except Exception as e:
|
||||
errors.append(e)
|
||||
|
||||
threads = [threading.Thread(target=worker, args=(f"vid_{i}",)) for i in range(20)]
|
||||
for t in threads:
|
||||
t.start()
|
||||
for t in threads:
|
||||
t.join(timeout=5)
|
||||
|
||||
assert not errors, f"Concurrent errors: {errors}"
|
||||
stats = music_db.get_stats()
|
||||
assert stats["unique_songs"] == 20
|
||||
Reference in New Issue
Block a user