132 lines
5.0 KiB
Python
132 lines
5.0 KiB
Python
# Tools for A Feminist Audit of Pop Music.
|
|
# Copyright 2026 imacat. All rights reserved.
|
|
# Authors:
|
|
# imacat@mail.imacat.idv.tw (imacat), 2026/7/31
|
|
# AI assistance: Claude Code (Anthropic)
|
|
"""Unit tests for the data models."""
|
|
import unittest
|
|
|
|
import sqlalchemy as sa
|
|
from sqlalchemy.orm import Session
|
|
|
|
from pop_fem_audit_tools import config
|
|
from pop_fem_audit_tools.database import Base, DataSource
|
|
from pop_fem_audit_tools.models import (
|
|
Artist,
|
|
ChartEntry,
|
|
Coding,
|
|
Role,
|
|
Song,
|
|
SongArtist,
|
|
)
|
|
|
|
|
|
class TestModels(unittest.TestCase):
|
|
"""Test cases for the data models."""
|
|
|
|
def setUp(self) -> None:
|
|
"""Create the schema on an in-memory SQLite database."""
|
|
config.set_settings(config.Settings(
|
|
SQLALCHEMY_DATABASE_URI="sqlite://",
|
|
ANTHROPIC_API_KEY="test-key"))
|
|
self.__ds: DataSource = DataSource()
|
|
self.addCleanup(self.__ds.engine.dispose)
|
|
Base.metadata.create_all(self.__ds.engine)
|
|
self.__session: Session = self.__ds.get_db()
|
|
self.addCleanup(self.__session.close)
|
|
|
|
def __add_song(self) -> None:
|
|
"""Add a song with chart entries, artists, and lyrics.
|
|
|
|
:return: None.
|
|
"""
|
|
song: Song = Song(title="One Dance",
|
|
artist_credit="Drake featuring Wizkid")
|
|
song.chart_entries = [ChartEntry(year=2016, rank=4)]
|
|
song.song_artists = [
|
|
SongArtist(artist=Artist(name="Drake"),
|
|
role=Role.PRIMARY, position=0),
|
|
SongArtist(artist=Artist(name="Wizkid"),
|
|
role=Role.FEATURED, position=1)]
|
|
song.lyrics = "Baby, I like your style"
|
|
song.codings = [
|
|
Coding(keyword="desire",
|
|
quotes="Baby, I like your style|One dance"),
|
|
Coding(keyword="nightlife", quotes="")]
|
|
self.__session.add(song)
|
|
self.__session.commit()
|
|
|
|
def test_song_graph(self) -> None:
|
|
"""Test reading a song graph back through relationships."""
|
|
self.__add_song()
|
|
self.__session.expunge_all()
|
|
song: Song | None = self.__session.scalar(
|
|
sa.select(Song).where(Song.title == "One Dance"))
|
|
assert song is not None
|
|
self.assertEqual(song.artist_credit,
|
|
"Drake featuring Wizkid")
|
|
self.assertEqual([(x.year, x.rank)
|
|
for x in song.chart_entries],
|
|
[(2016, 4)])
|
|
self.assertEqual([(x.artist.name, x.role, x.position)
|
|
for x in song.song_artists],
|
|
[("Drake", Role.PRIMARY, 0),
|
|
("Wizkid", Role.FEATURED, 1)])
|
|
self.assertEqual(song.lyrics,
|
|
"Baby, I like your style")
|
|
artist: Artist | None = self.__session.scalar(
|
|
sa.select(Artist).where(Artist.name == "Wizkid"))
|
|
assert artist is not None
|
|
self.assertEqual([x.song.title for x in artist.song_artists],
|
|
["One Dance"])
|
|
|
|
def test_duplicated_song_rejected(self) -> None:
|
|
"""Test that a duplicated title and artist credit fails."""
|
|
self.__add_song()
|
|
self.__session.add(
|
|
Song(title="One Dance",
|
|
artist_credit="Drake featuring Wizkid"))
|
|
with self.assertRaises(sa.exc.IntegrityError):
|
|
self.__session.commit()
|
|
|
|
def test_song_codings(self) -> None:
|
|
"""Test reading a song's codings back through the
|
|
relationship."""
|
|
self.__add_song()
|
|
self.__session.expunge_all()
|
|
song: Song | None = self.__session.scalar(
|
|
sa.select(Song).where(Song.title == "One Dance"))
|
|
assert song is not None
|
|
self.assertEqual(
|
|
{(x.keyword, x.quotes) for x in song.codings},
|
|
{("desire", "Baby, I like your style|One dance"),
|
|
("nightlife", "")})
|
|
coding: Coding | None = self.__session.scalar(
|
|
sa.select(Coding).where(Coding.keyword == "desire"))
|
|
assert coding is not None
|
|
self.assertEqual(coding.song.title, "One Dance")
|
|
|
|
def test_duplicated_coding_rejected(self) -> None:
|
|
"""Test that a repeated song and keyword fails."""
|
|
self.__add_song()
|
|
song: Song | None = self.__session.scalar(
|
|
sa.select(Song).where(Song.title == "One Dance"))
|
|
assert song is not None
|
|
self.__session.add(
|
|
Coding(song_id=song.id, keyword="desire",
|
|
quotes="another line"))
|
|
with self.assertRaises(sa.exc.IntegrityError):
|
|
self.__session.commit()
|
|
|
|
def test_invalid_role_rejected(self) -> None:
|
|
"""Test that an invalid song-artist role fails."""
|
|
self.__add_song()
|
|
song: Song | None = self.__session.scalar(
|
|
sa.select(Song).where(Song.title == "One Dance"))
|
|
assert song is not None
|
|
self.__session.add(
|
|
SongArtist(song=song, artist=Artist(name="Kyla"),
|
|
role="cover", position=2))
|
|
with self.assertRaises(sa.exc.IntegrityError):
|
|
self.__session.commit()
|