Files
pop-fem-audit/tools/tests/test_main.py
T
2026-08-17 22:38:34 +08:00

155 lines
6.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 package-level CLI dispatcher."""
import argparse
import io
import sys
import unittest
from contextlib import redirect_stderr, redirect_stdout
from unittest import mock
from pop_fem_audit_tools import __main__, commands
from pop_fem_audit_tools.commands import run_llm
class TestDispatcher(unittest.TestCase):
"""Test cases for the package-level CLI dispatcher."""
@staticmethod
def __run_main(argv: list[str]) -> tuple[int, str, str]:
"""Run the dispatcher with captured output.
:param argv: The command-line arguments.
:return: A tuple of the exit status, the standard output,
and the standard error.
"""
stdout: io.StringIO = io.StringIO()
stderr: io.StringIO = io.StringIO()
with redirect_stdout(stdout), redirect_stderr(stderr):
status: int = __main__.main(argv)
return status, stdout.getvalue(), stderr.getvalue()
def test_run_llm_registered(self) -> None:
"""Test that run-llm is bound to the run_llm wrapper."""
self.assertIs(__main__.SUBCOMMANDS["run-llm"],
commands.run_llm_command)
def test_every_subcommand_registered(self) -> None:
"""Test that every subcommand is bound to its wrapper."""
for name, command in __main__.SUBCOMMANDS.items():
with self.subTest(subcommand=name):
wrapper: str = f"{name.replace('-', '_')}_command"
self.assertIs(command, getattr(commands, wrapper))
def test_run_llm_command_delegates(self) -> None:
"""Test that the run-llm wrapper calls the run_llm main."""
tool: mock.Mock = mock.Mock(return_value=7)
with mock.patch.object(run_llm, "main", tool):
status: int = commands.run_llm_command(["--input", "x"])
self.assertEqual(status, 7)
tool.assert_called_once_with(["--input", "x"])
def test_run_llm_command_defaults_to_none(self) -> None:
"""Test that the run-llm wrapper defaults the arguments."""
tool: mock.Mock = mock.Mock(return_value=0)
with mock.patch.object(run_llm, "main", tool):
commands.run_llm_command()
tool.assert_called_once_with(None)
def test_run_llm_dispatch(self) -> None:
"""Test that run-llm forwards the arguments and the status."""
tool: mock.Mock = mock.Mock(return_value=7)
with mock.patch.dict(__main__.SUBCOMMANDS,
{"run-llm": tool}):
status: int = self.__run_main(
["run-llm", "--input", "items.jsonl"])[0]
self.assertEqual(status, 7)
tool.assert_called_once_with(["--input", "items.jsonl"])
def test_help_exits_zero_and_lists_subcommands(self) -> None:
"""Test that --help lists the subcommands and exits 0."""
status: int
stdout: str
stderr: str
status, stdout, stderr = self.__run_main(["--help"])
self.assertEqual(status, 0)
self.assertIn("run-llm", stdout)
def test_no_arguments_exits_non_zero(self) -> None:
"""Test that no arguments yields the usage and non-zero."""
status: int
stdout: str
stderr: str
status, stdout, stderr = self.__run_main([])
self.assertNotEqual(status, 0)
self.assertIn("run-llm", stderr)
def test_usage_prog_module_run(self) -> None:
"""Test the usage program name when run with python -m."""
argv: list[str] = ["/x/pop_fem_audit_tools/__main__.py"]
stdout: str
with mock.patch.object(sys, "argv", argv):
stdout = self.__run_main(["--help"])[1]
self.assertIn(
"usage: python -m pop_fem_audit_tools ", stdout)
def test_usage_prog_console_script(self) -> None:
"""Test the usage program name when run as a script."""
argv: list[str] = ["/x/bin/pop-fem-audit-tools"]
stdout: str
with mock.patch.object(sys, "argv", argv):
stdout = self.__run_main(["--help"])[1]
self.assertIn("usage: pop-fem-audit-tools ", stdout)
def test_subcommand_prog_module_run(self) -> None:
"""Test the subcommand program name with python -m."""
argv: list[str] = ["/x/pop_fem_audit_tools/__main__.py"]
self.assertEqual(
self.__dispatched_prog(argv),
"python -m pop_fem_audit_tools run-llm")
def test_subcommand_prog_console_script(self) -> None:
"""Test the subcommand program name as a script."""
argv: list[str] = ["/x/bin/pop-fem-audit-tools"]
self.assertEqual(
self.__dispatched_prog(argv),
"pop-fem-audit-tools run-llm")
def __dispatched_prog(self, argv: list[str]) -> str:
"""Run a stub run-llm tool and return the prog it sees.
:param argv: The value to patch ``sys.argv`` with.
:return: The default argparse program name seen by the
dispatched tool.
"""
seen: list[str] = []
def tool(tool_argv: list[str] | None) -> int:
"""Record the argparse program name and succeed.
:param tool_argv: The tool command-line arguments.
:return: Always 0.
"""
parser: argparse.ArgumentParser = \
argparse.ArgumentParser()
seen.append(parser.prog)
return 0
with mock.patch.object(sys, "argv", argv), \
mock.patch.dict(__main__.SUBCOMMANDS,
{"run-llm": tool}):
self.__run_main(["run-llm"])
return seen[0]
def test_unknown_subcommand_exits_non_zero(self) -> None:
"""Test that an unknown subcommand errors to standard error."""
status: int
stdout: str
stderr: str
status, stdout, stderr = self.__run_main(["nonsense"])
self.assertNotEqual(status, 0)
self.assertIn("nonsense", stderr)