feedBack/tests/test_albums_view.py
Byron Gamatos 97a941c45d
fix(tests): join background scan/enrich workers before closing the DB (#735)
Root cause of the flaky pytest segfault (exit 139): the background scan and enrichment daemon threads (_scan_runner/_enrich_runner) use the shared MetadataDB connection, but test fixtures closed that connection in teardown without stopping them. A daemon thread mid-query on a freed SQLite conn is a native use-after-free → SIGSEGV. The app's startup kicks a scan, so almost any app-booting fixture was vulnerable. It only surfaced now because got-feedback/feedBack#728 added a push trigger, so ci/test runs on every push to main.

Fix: server.py retains the scan/enrich thread handles and adds _join_background_db_threads(); every test fixture now joins the workers before conn.close(). Verified: the full suite runs to completion (no segfault) where it previously crashed at ~25%.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 12:15:43 +02:00

95 lines
3.8 KiB
Python

"""Tests for the Albums-view follow-up (#689's client half): the feedpak
`track`/`disc` fields flowing scanner → songs columns → the `track` sort the
album track list orders by."""
import importlib
import sys
import pytest
from fastapi.testclient import TestClient
@pytest.fixture()
def server(tmp_path, monkeypatch, isolate_logging):
monkeypatch.setenv("CONFIG_DIR", str(tmp_path / "config"))
dlc = tmp_path / "dlc"
dlc.mkdir()
monkeypatch.setenv("DLC_DIR", str(dlc))
monkeypatch.setenv("FEEDBACK_SKIP_STARTUP_TASKS", "1")
sys.modules.pop("server", None)
srv = importlib.import_module("server")
try:
yield srv
finally:
conn = getattr(getattr(srv, "meta_db", None), "conn", None)
if conn is not None:
getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)()
conn.close()
sys.modules.pop("server", None)
@pytest.fixture()
def client(server):
return TestClient(server.app)
def _put(server, fn, title, track=None, disc=None, album="The Album",
artist="Artist", genre=""):
server.meta_db.put(fn, 0, 0, {
"title": title, "artist": artist, "album": album, "year": "1990",
"duration": 100, "arrangements": [{"name": "Lead", "index": 0}],
"track_number": track, "disc": disc, "genre": genre,
})
def test_sloppak_extract_meta_reads_track_and_disc(server):
d = server.DLC_DIR / "a.sloppak"
d.mkdir(parents=True)
(d / "manifest.yaml").write_text(
"title: Song\nartist: Artist\nduration: 100\n"
"arrangements: []\nstems: []\ntrack: 7\ndisc: 2\n", encoding="utf-8")
import sloppak
meta = sloppak.extract_meta(d)
assert meta["track_number"] == 7
assert meta["disc"] == 2
# Unauthored → None (the album view falls back to title order).
(d / "manifest.yaml").write_text(
"title: Song\nartist: Artist\nduration: 100\n"
"arrangements: []\nstems: []\n", encoding="utf-8")
meta = sloppak.extract_meta(d)
assert meta["track_number"] is None
assert meta["disc"] is None
def test_track_sort_orders_by_disc_then_track_nulls_last(server, client):
_put(server, "d2t1.sloppak", "Zeta", track=1, disc=2)
_put(server, "d1t2.sloppak", "Yankee", track=2, disc=1)
_put(server, "d1t1.sloppak", "Xray", track=1, disc=1)
_put(server, "nonum-b.sloppak", "Bravo") # unauthored → bottom,
_put(server, "nonum-a.sloppak", "Alpha") # ordered by title
body = client.get("/api/library", params={
"artist": "Artist", "album": "The Album", "sort": "track", "size": 50}).json()
assert [s["filename"] for s in body["songs"]] == [
"d1t1.sloppak", "d1t2.sloppak", "d2t1.sloppak",
"nonum-a.sloppak", "nonum-b.sloppak"]
def test_track_and_disc_survive_put_roundtrip(server):
_put(server, "a.sloppak", "Song", track=3, disc=1)
row = server.meta_db.conn.execute(
"SELECT track_number, disc FROM songs WHERE filename = 'a.sloppak'").fetchone()
assert row == (3, 1)
def test_albums_endpoint_honours_genre_filter(server, client):
"""The albums grid must respect the Genre drawer filter the client sends —
without this the /api/library/albums route silently dropped `genre` and
surfaced albums with no matching tracks."""
_put(server, "rock.sloppak", "Rocker", album="Rock LP", genre="Rock")
_put(server, "jazz.sloppak", "Smooth", album="Jazz LP", genre="Jazz")
all_albums = client.get("/api/library/albums", params={"artist": "Artist"}).json()
assert {a["album"] for a in all_albums["albums"]} == {"Rock LP", "Jazz LP"}
filtered = client.get("/api/library/albums",
params={"artist": "Artist", "genre": "Rock"}).json()
assert [a["album"] for a in filtered["albums"]] == ["Rock LP"]