forked from Rockachopa/Timmy-time-dashboard
This commit is contained in:
161
tests/unit/test_content_narrator.py
Normal file
161
tests/unit/test_content_narrator.py
Normal file
@@ -0,0 +1,161 @@
|
||||
"""Unit tests for content.narration.narrator."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from content.narration.narrator import (
|
||||
NarrationResult,
|
||||
_kokoro_available,
|
||||
_piper_available,
|
||||
build_episode_script,
|
||||
generate_narration,
|
||||
)
|
||||
|
||||
# ── _kokoro_available / _piper_available ──────────────────────────────────────
|
||||
|
||||
|
||||
class TestBackendAvailability:
|
||||
def test_kokoro_returns_bool(self):
|
||||
assert isinstance(_kokoro_available(), bool)
|
||||
|
||||
def test_piper_returns_bool(self):
|
||||
assert isinstance(_piper_available(), bool)
|
||||
|
||||
def test_kokoro_false_when_spec_missing(self):
|
||||
with patch("importlib.util.find_spec", return_value=None):
|
||||
assert _kokoro_available() is False
|
||||
|
||||
def test_piper_false_when_binary_missing(self):
|
||||
with patch("content.narration.narrator.shutil.which", return_value=None):
|
||||
assert _piper_available() is False
|
||||
|
||||
def test_piper_true_when_binary_found(self):
|
||||
with patch("content.narration.narrator.shutil.which", return_value="/usr/bin/piper"):
|
||||
assert _piper_available() is True
|
||||
|
||||
|
||||
# ── generate_narration ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestGenerateNarration:
|
||||
@pytest.mark.asyncio
|
||||
async def test_empty_text_returns_failure(self, tmp_path):
|
||||
result = await generate_narration("", str(tmp_path / "out.wav"))
|
||||
assert result.success is False
|
||||
assert "empty" in result.error.lower()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_whitespace_only_returns_failure(self, tmp_path):
|
||||
result = await generate_narration(" \n\t ", str(tmp_path / "out.wav"))
|
||||
assert result.success is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_no_backend_returns_failure(self, tmp_path):
|
||||
with (
|
||||
patch("content.narration.narrator._kokoro_available", return_value=False),
|
||||
patch("content.narration.narrator._piper_available", return_value=False),
|
||||
):
|
||||
result = await generate_narration("Hello world", str(tmp_path / "out.wav"))
|
||||
assert result.success is False
|
||||
assert "no TTS backend" in result.error
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_kokoro_success(self, tmp_path):
|
||||
async def _fake_kokoro(text, output_path):
|
||||
return NarrationResult(success=True, audio_path=output_path, backend="kokoro")
|
||||
|
||||
with (
|
||||
patch("content.narration.narrator._kokoro_available", return_value=True),
|
||||
patch("content.narration.narrator._generate_kokoro", side_effect=_fake_kokoro),
|
||||
):
|
||||
result = await generate_narration("Test narration", str(tmp_path / "out.wav"))
|
||||
|
||||
assert result.success is True
|
||||
assert result.backend == "kokoro"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_falls_back_to_piper_when_kokoro_fails(self, tmp_path):
|
||||
async def _failing_kokoro(text, output_path):
|
||||
return NarrationResult(success=False, backend="kokoro", error="kokoro error")
|
||||
|
||||
async def _ok_piper(text, output_path):
|
||||
return NarrationResult(success=True, audio_path=output_path, backend="piper")
|
||||
|
||||
with (
|
||||
patch("content.narration.narrator._kokoro_available", return_value=True),
|
||||
patch("content.narration.narrator._piper_available", return_value=True),
|
||||
patch("content.narration.narrator._generate_kokoro", side_effect=_failing_kokoro),
|
||||
patch("content.narration.narrator._generate_piper", side_effect=_ok_piper),
|
||||
):
|
||||
result = await generate_narration("Test narration", str(tmp_path / "out.wav"))
|
||||
|
||||
assert result.success is True
|
||||
assert result.backend == "piper"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_piper_called_when_kokoro_unavailable(self, tmp_path):
|
||||
async def _ok_piper(text, output_path):
|
||||
return NarrationResult(success=True, audio_path=output_path, backend="piper")
|
||||
|
||||
with (
|
||||
patch("content.narration.narrator._kokoro_available", return_value=False),
|
||||
patch("content.narration.narrator._piper_available", return_value=True),
|
||||
patch("content.narration.narrator._generate_piper", side_effect=_ok_piper),
|
||||
):
|
||||
result = await generate_narration("Hello", str(tmp_path / "out.wav"))
|
||||
|
||||
assert result.success is True
|
||||
assert result.backend == "piper"
|
||||
|
||||
|
||||
# ── build_episode_script ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestBuildEpisodeScript:
|
||||
def test_contains_title(self):
|
||||
script = build_episode_script("Daily Highlights", [])
|
||||
assert "Daily Highlights" in script
|
||||
|
||||
def test_contains_highlight_descriptions(self):
|
||||
highlights = [
|
||||
{"description": "Epic kill streak"},
|
||||
{"description": "Clutch win"},
|
||||
]
|
||||
script = build_episode_script("Episode 1", highlights)
|
||||
assert "Epic kill streak" in script
|
||||
assert "Clutch win" in script
|
||||
|
||||
def test_highlight_numbering(self):
|
||||
highlights = [{"description": "First"}, {"description": "Second"}]
|
||||
script = build_episode_script("EP", highlights)
|
||||
assert "Highlight 1" in script
|
||||
assert "Highlight 2" in script
|
||||
|
||||
def test_uses_title_as_fallback_when_no_description(self):
|
||||
highlights = [{"title": "Big Moment"}]
|
||||
script = build_episode_script("EP", highlights)
|
||||
assert "Big Moment" in script
|
||||
|
||||
def test_uses_index_as_fallback_when_no_title_or_description(self):
|
||||
highlights = [{}]
|
||||
script = build_episode_script("EP", highlights)
|
||||
assert "Highlight 1" in script
|
||||
|
||||
def test_contains_default_outro(self):
|
||||
script = build_episode_script("EP", [])
|
||||
assert "subscribe" in script.lower()
|
||||
|
||||
def test_custom_outro_replaces_default(self):
|
||||
script = build_episode_script("EP", [], outro_text="Custom outro text here.")
|
||||
assert "Custom outro text here." in script
|
||||
assert "subscribe" not in script.lower()
|
||||
|
||||
def test_empty_highlights_still_has_intro(self):
|
||||
script = build_episode_script("My Show", [])
|
||||
assert "Welcome to My Show" in script
|
||||
|
||||
def test_returns_string(self):
|
||||
assert isinstance(build_episode_script("EP", []), str)
|
||||
Reference in New Issue
Block a user