Restructure tally-annotations into the house command shape

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 09:49:39 +08:00
co-authored by Claude Opus 5
parent bfbbe1875e
commit 6d055c1ee5
2 changed files with 645 additions and 602 deletions
File diff suppressed because it is too large Load Diff
+65 -27
View File
@@ -11,12 +11,14 @@ import tempfile
import unittest import unittest
from contextlib import redirect_stderr from contextlib import redirect_stderr
from pathlib import Path from pathlib import Path
from typing import Any
from unittest import mock
import sqlalchemy as sa
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from pop_fem_audit_tools import config
from pop_fem_audit_tools.commands import tally_annotations from pop_fem_audit_tools.commands import tally_annotations
from pop_fem_audit_tools.database import Base from pop_fem_audit_tools.database import Base, DataSource
from pop_fem_audit_tools.models import Song from pop_fem_audit_tools.models import Song
@@ -24,8 +26,8 @@ class TestTallyAnnotations(unittest.TestCase):
"""Test cases for the step-5 pattern annotation tally.""" """Test cases for the step-5 pattern annotation tally."""
def setUp(self) -> None: def setUp(self) -> None:
"""Create the archive directories, the database, and the """Create the archive directories, the working store, and
output paths.""" the output paths."""
tmp: tempfile.TemporaryDirectory[str] \ tmp: tempfile.TemporaryDirectory[str] \
= tempfile.TemporaryDirectory() = tempfile.TemporaryDirectory()
self.addCleanup(tmp.cleanup) self.addCleanup(tmp.cleanup)
@@ -37,7 +39,6 @@ class TestTallyAnnotations(unittest.TestCase):
self.__male_synthesis.mkdir() self.__male_synthesis.mkdir()
self.__female_synthesis.mkdir() self.__female_synthesis.mkdir()
self.__mixed_synthesis.mkdir() self.__mixed_synthesis.mkdir()
self.__db_path: Path = self.__dir / "working.sqlite3"
self.__patterns_csv: Path \ self.__patterns_csv: Path \
= self.__dir / "results" / "patterns.csv" = self.__dir / "results" / "patterns.csv"
self.__annotations_csv: Path \ self.__annotations_csv: Path \
@@ -48,6 +49,15 @@ class TestTallyAnnotations(unittest.TestCase):
run_dir: Path = self.__dir / f"run{number}" run_dir: Path = self.__dir / f"run{number}"
run_dir.mkdir() run_dir.mkdir()
self.__runs.append(run_dir) self.__runs.append(run_dir)
config.set_settings(config.Settings(
SQLALCHEMY_DATABASE_URL="sqlite://",
ANTHROPIC_API_KEY="test-key"))
self.__ds: DataSource = DataSource()
self.addCleanup(self.__ds.engine.dispose)
patcher: Any = mock.patch.object(
tally_annotations, "ds", self.__ds)
patcher.start()
self.addCleanup(patcher.stop)
def __write_default_synthesis_archives(self) -> None: def __write_default_synthesis_archives(self) -> None:
"""Write the male, female, and mixed synthesis archives. """Write the male, female, and mixed synthesis archives.
@@ -108,22 +118,20 @@ class TestTallyAnnotations(unittest.TestCase):
stored performer gender of every fixture song. stored performer gender of every fixture song.
:return: None. :return: None.
""" """
engine: sa.Engine = sa.create_engine( Base.metadata.create_all(self.__ds.engine)
f"sqlite:///{self.__db_path}") session: Session = self.__ds.get_db()
Base.metadata.create_all(engine) try:
session: Session
with Session(engine) as session:
song_id: int song_id: int
title: str title: str
artist_credit: str artist_credit: str
gender: str | None gender: str | None
for song_id, title, artist_credit, gender in songs: for song_id, title, artist_credit, gender in songs:
session.add(Song( session.add(Song(
id=song_id, title=title, id=song_id, title=title, artist_credit=artist_credit,
artist_credit=artist_credit,
performer_gender=gender)) performer_gender=gender))
session.commit() session.commit()
engine.dispose() finally:
session.close()
def __write_run( def __write_run(
self, run_dir: Path, self, run_dir: Path,
@@ -165,11 +173,10 @@ class TestTallyAnnotations(unittest.TestCase):
status: int status: int
with redirect_stderr(stderr): with redirect_stderr(stderr):
status = tally_annotations.main([ status = tally_annotations.main([
str(self.__male_synthesis), "--female", str(self.__female_synthesis),
str(self.__female_synthesis), "--male", str(self.__male_synthesis),
str(self.__mixed_synthesis), str(self.__db_path), "--mixed", str(self.__mixed_synthesis),
str(self.__patterns_csv), str(self.__patterns_csv), str(self.__annotations_csv)]
str(self.__annotations_csv)]
+ [str(x) for x in self.__runs]) + [str(x) for x in self.__runs])
return status, stderr.getvalue() return status, stderr.getvalue()
@@ -315,11 +322,10 @@ class TestTallyAnnotations(unittest.TestCase):
status: int status: int
with redirect_stderr(stderr): with redirect_stderr(stderr):
status = tally_annotations.main([ status = tally_annotations.main([
str(self.__male_synthesis), "--female", str(self.__female_synthesis),
str(self.__female_synthesis), "--male", str(self.__male_synthesis),
str(self.__mixed_synthesis), str(self.__db_path), "--mixed", str(self.__mixed_synthesis),
str(self.__patterns_csv), str(self.__patterns_csv), str(self.__annotations_csv)]
str(self.__annotations_csv)]
+ [str(x) for x in self.__runs] + [str(x) for x in self.__runs]
+ [str(rescue_dir)]) + [str(rescue_dir)])
self.assertEqual(status, 0) self.assertEqual(status, 0)
@@ -329,6 +335,40 @@ class TestTallyAnnotations(unittest.TestCase):
["Song", "Artist Credit", "Pattern", "Votes"], ["Song", "Artist Credit", "Pattern", "Votes"],
["Song A", "Artist A", "M1", "3"]]) ["Song A", "Artist A", "M1", "3"]])
def test_dead_record_with_empty_text_rescued_from_extra_run_dir(
self) -> None:
"""Test that a dead record whose "text" is an empty
string (malformed JSON) in one run directory is skipped
with a warning, and its ballot is rescued from an extra
run directory, tallying successfully."""
self.__write_default_synthesis_archives()
self.__seed_songs([(1, "Song A", "Artist A", "male")])
self.__write_same_ballots_to_all_runs({1: ["M1"]})
(self.__runs[0] / "output.jsonl").write_text(
json.dumps({"id": "song-1", "text": ""}) + "\n",
encoding="utf-8")
rescue_dir: Path = self.__dir / "rescue"
rescue_dir.mkdir()
self.__write_run(rescue_dir, {1: ["M1"]})
stderr: io.StringIO = io.StringIO()
status: int
with redirect_stderr(stderr):
status = tally_annotations.main([
"--female", str(self.__female_synthesis),
"--male", str(self.__male_synthesis),
"--mixed", str(self.__mixed_synthesis),
str(self.__patterns_csv), str(self.__annotations_csv)]
+ [str(x) for x in self.__runs]
+ [str(rescue_dir)])
self.assertEqual(status, 0)
self.assertIn("warning:", stderr.getvalue())
self.assertIn("song-1", stderr.getvalue())
self.assertIn("\"text\" is malformed JSON", stderr.getvalue())
self.assertIn("skipped", stderr.getvalue())
self.assertEqual(self.__read_rows(self.__annotations_csv), [
["Song", "Artist Credit", "Pattern", "Votes"],
["Song A", "Artist A", "M1", "3"]])
def test_unknown_pattern_id_dropped_with_warning(self) -> None: def test_unknown_pattern_id_dropped_with_warning(self) -> None:
"""Test that a selected ID outside the extracted patterns """Test that a selected ID outside the extracted patterns
is dropped, the occurrence reported on standard error.""" is dropped, the occurrence reported on standard error."""
@@ -511,7 +551,7 @@ class TestTallyAnnotations(unittest.TestCase):
def test_summary_line_reports_counts(self) -> None: def test_summary_line_reports_counts(self) -> None:
"""Test that the closing summary reports the settled """Test that the closing summary reports the settled
pair, song, and dropped vote counts.""" annotation count."""
self.__write_default_synthesis_archives() self.__write_default_synthesis_archives()
self.__seed_songs([(1, "Song A", "Artist A", "male")]) self.__seed_songs([(1, "Song A", "Artist A", "male")])
self.__write_same_ballots_to_all_runs( self.__write_same_ballots_to_all_runs(
@@ -520,6 +560,4 @@ class TestTallyAnnotations(unittest.TestCase):
stderr: str stderr: str
status, stderr = self.__run_tally() status, stderr = self.__run_tally()
self.assertEqual(status, 0) self.assertEqual(status, 0)
self.assertIn("Tallied 1 settled pairs across 1 songs", self.assertIn("Tallied 1 annotations.", stderr)
stderr)
self.assertIn("3 votes dropped", stderr)