205 lines
7.7 KiB
Python
205 lines
7.7 KiB
Python
"""Test per core.acoustid — lookup AcoustID + cache SQLite.
|
|
|
|
Mock su `requests.get` in modo che i test siano offline. Ogni test
|
|
isola la cache SQLite in tmp_path via monkeypatch di
|
|
`_cache_db_path`.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest import mock
|
|
|
|
import pytest
|
|
|
|
from core import acoustid
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# Fixture: cache isolata + reset del rate-limit
|
|
# ------------------------------------------------------------------
|
|
@pytest.fixture
|
|
def patched_cache(tmp_path, monkeypatch):
|
|
db = tmp_path / "acoustid_cache_test.db"
|
|
monkeypatch.setattr(acoustid, "_cache_db_path", lambda: db)
|
|
# Rate-limit: azzera cosi' i test non aspettano throttle
|
|
monkeypatch.setattr(acoustid, "_RATE_LIMIT_SEC", 0.0)
|
|
monkeypatch.setattr(acoustid, "_last_request_at", [0.0])
|
|
return db
|
|
|
|
|
|
def _mock_resp(status_code: int = 200, json_data: dict = None):
|
|
"""Costruisce un mock di response `requests`."""
|
|
m = mock.Mock()
|
|
m.status_code = status_code
|
|
m.json.return_value = json_data or {}
|
|
return m
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# lookup — cache
|
|
# ------------------------------------------------------------------
|
|
class TestLookupCache:
|
|
def test_lookup_uses_cache(self, patched_cache):
|
|
"""Seconda chiamata con lo stesso fingerprint riusa la cache."""
|
|
payload = {
|
|
"status": "ok",
|
|
"results": [{
|
|
"score": 0.99,
|
|
"recordings": [{
|
|
"title": "Some Song",
|
|
"artists": [{"name": "Artist X"}],
|
|
"releases": [
|
|
{"date": {"year": 2005},
|
|
"releasegroup": {"tags": [{"name": "House", "count": 5}]}},
|
|
],
|
|
}],
|
|
}],
|
|
}
|
|
with mock.patch.object(acoustid.requests, "get",
|
|
return_value=_mock_resp(200, payload)) as m:
|
|
r1 = acoustid.lookup("FP-1", 180.0)
|
|
r2 = acoustid.lookup("FP-1", 180.0)
|
|
|
|
assert m.call_count == 1, "la seconda chiamata deve venire dalla cache"
|
|
assert r1 == r2
|
|
assert r1["matched"] is True
|
|
assert r1["year"] == 2005
|
|
assert r1["genre"].lower() == "house"
|
|
assert r1["title"] == "Some Song"
|
|
assert r1["artist"] == "Artist X"
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# lookup — no results / errori
|
|
# ------------------------------------------------------------------
|
|
class TestLookupNoResults:
|
|
def test_lookup_no_results_returns_unmatched(self, patched_cache):
|
|
payload = {"status": "ok", "results": []}
|
|
with mock.patch.object(acoustid.requests, "get",
|
|
return_value=_mock_resp(200, payload)):
|
|
r = acoustid.lookup("FP-NORESULT", 100.0)
|
|
|
|
assert r["matched"] is False
|
|
# deve essere cacheato
|
|
with mock.patch.object(acoustid.requests, "get") as m:
|
|
r2 = acoustid.lookup("FP-NORESULT", 100.0)
|
|
assert m.call_count == 0
|
|
assert r2 == r
|
|
|
|
def test_lookup_no_recordings_returns_unmatched(self, patched_cache):
|
|
"""results presenti ma senza recordings -> matched=False."""
|
|
payload = {
|
|
"status": "ok",
|
|
"results": [{"score": 0.5, "recordings": []}],
|
|
}
|
|
with mock.patch.object(acoustid.requests, "get",
|
|
return_value=_mock_resp(200, payload)):
|
|
r = acoustid.lookup("FP-EMPTYREC", 200)
|
|
assert r["matched"] is False
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# lookup — anno minimo
|
|
# ------------------------------------------------------------------
|
|
class TestLookupYearExtraction:
|
|
def test_lookup_extracts_min_year(self, patched_cache):
|
|
"""3 releases (2003, 1998, 2010) -> year=1998."""
|
|
payload = {
|
|
"status": "ok",
|
|
"results": [{
|
|
"score": 0.95,
|
|
"recordings": [{
|
|
"title": "Classic",
|
|
"artists": [{"name": "Artist"}],
|
|
"releases": [
|
|
{"date": {"year": 2003}},
|
|
{"date": {"year": 1998}},
|
|
{"date": {"year": 2010}},
|
|
],
|
|
}],
|
|
}],
|
|
}
|
|
with mock.patch.object(acoustid.requests, "get",
|
|
return_value=_mock_resp(200, payload)):
|
|
r = acoustid.lookup("FP-YEAR", 180)
|
|
|
|
assert r["matched"] is True
|
|
assert r["year"] == 1998
|
|
|
|
def test_lookup_missing_year_becomes_none(self, patched_cache):
|
|
"""Nessuna release con year valido -> year=None (matched se ha titolo)."""
|
|
payload = {
|
|
"status": "ok",
|
|
"results": [{
|
|
"score": 0.9,
|
|
"recordings": [{
|
|
"title": "T",
|
|
"artists": [{"name": "A"}],
|
|
"releases": [{"date": {}}, {"other": 1}],
|
|
}],
|
|
}],
|
|
}
|
|
with mock.patch.object(acoustid.requests, "get",
|
|
return_value=_mock_resp(200, payload)):
|
|
r = acoustid.lookup("FP-NOY", 100)
|
|
# matched puo' essere True se ha almeno un titolo
|
|
assert r["year"] is None
|
|
assert r["title"] == "T"
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# lookup — errori HTTP / rete
|
|
# ------------------------------------------------------------------
|
|
class TestLookupErrors:
|
|
def test_lookup_returns_error_on_http_failure(self, patched_cache):
|
|
with mock.patch.object(acoustid.requests, "get",
|
|
return_value=_mock_resp(500, {})):
|
|
r = acoustid.lookup("FP-HTTP", 100)
|
|
assert r["matched"] is False
|
|
assert "HTTP 500" in r.get("error", "")
|
|
|
|
def test_lookup_returns_error_on_network_failure(self, patched_cache):
|
|
with mock.patch.object(acoustid.requests, "get",
|
|
side_effect=acoustid.requests.ConnectionError("boom")):
|
|
r = acoustid.lookup("FP-NET", 100)
|
|
assert r["matched"] is False
|
|
assert "network" in r.get("error", "").lower() or "boom" in r.get("error", "")
|
|
|
|
def test_lookup_empty_fingerprint_returns_unmatched(self, patched_cache):
|
|
r = acoustid.lookup("", 100)
|
|
assert r["matched"] is False
|
|
assert "fingerprint" in r.get("error", "").lower()
|
|
|
|
def test_lookup_api_status_error(self, patched_cache):
|
|
payload = {"status": "error",
|
|
"error": {"message": "invalid fingerprint"}}
|
|
with mock.patch.object(acoustid.requests, "get",
|
|
return_value=_mock_resp(200, payload)):
|
|
r = acoustid.lookup("FP-BAD", 100)
|
|
assert r["matched"] is False
|
|
assert "invalid" in r.get("error", "").lower()
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# helper: _extract_top_genre
|
|
# ------------------------------------------------------------------
|
|
class TestGenreExtraction:
|
|
def test_top_genre_picks_highest_count(self):
|
|
releases = [{
|
|
"releasegroup": {
|
|
"tags": [
|
|
{"name": "electronic", "count": 3},
|
|
{"name": "house", "count": 12},
|
|
{"name": "dance", "count": 7},
|
|
]
|
|
}
|
|
}]
|
|
assert acoustid._extract_top_genre(releases).lower() == "house"
|
|
|
|
def test_top_genre_no_tags_returns_empty(self):
|
|
releases = [{"releasegroup": {"tags": []}}]
|
|
assert acoustid._extract_top_genre(releases) == ""
|
|
|
|
def test_top_genre_no_releasegroup_returns_empty(self):
|
|
assert acoustid._extract_top_genre([{}]) == ""
|