mirror of
https://github.com/denizsafak/abogen.git
synced 2026-09-20 11:40:57 +02:00
test: regression tests for conversion flow unification
Three new test files describing expected behavior before refactoring: - test_conversion_planner.py: chapter parsing, voice markers, TTSContext, intro/outro, output paths, subtitles (30 tests) - test_conversion_request.py: settings, context building, chapter selection, cancellation/logging protocols (15 tests) - test_conversion_executor.py: synthesize_text, process_and_write_subtitles, full pipeline with fake backend/sink (12 tests) All 1310 tests pass (1253 existing + 57 new).
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
"""Regression tests for ConversionRequest building.
|
||||
|
||||
These tests verify that both WebUI and PyQt adapters can produce
|
||||
a valid ConversionRequest from their respective Job/thread state.
|
||||
They serve as a specification for the adapter code that will be
|
||||
created in Phase 2/3 of the refactor.
|
||||
|
||||
Currently these tests verify the EXISTING behavior by testing the
|
||||
domain functions that the adapters will call. After the adapters
|
||||
are created, these tests should be updated to test the adapters
|
||||
directly.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from abogen.domain.normalization import TTSContext
|
||||
from abogen.domain.settings_core import settings_defaults
|
||||
from abogen.domain.split_pattern import get_split_pattern
|
||||
|
||||
|
||||
class TestConversionRequestBasics:
|
||||
"""Verify that basic request parameters can be derived from settings."""
|
||||
|
||||
def test_settings_defaults_exist(self):
|
||||
defaults = settings_defaults()
|
||||
assert isinstance(defaults, dict)
|
||||
assert "output_format" in defaults
|
||||
assert "subtitle_format" in defaults
|
||||
assert "save_mode" in defaults
|
||||
assert "use_gpu" in defaults
|
||||
assert "silence_between_chapters" in defaults
|
||||
assert "merge_chapters_at_end" in defaults
|
||||
|
||||
def test_split_pattern_computation(self):
|
||||
pattern = get_split_pattern("a", "Disabled")
|
||||
assert isinstance(pattern, str)
|
||||
assert len(pattern) > 0
|
||||
|
||||
def test_split_pattern_varies_by_subtitle_mode(self):
|
||||
pattern_disabled = get_split_pattern("a", "Disabled")
|
||||
pattern_sentence = get_split_pattern("a", "Sentence")
|
||||
# Different modes should produce different patterns
|
||||
assert isinstance(pattern_disabled, str)
|
||||
assert isinstance(pattern_sentence, str)
|
||||
|
||||
|
||||
class TestTTSContextBuilding:
|
||||
"""Verify TTSContext can be built from settings parameters."""
|
||||
|
||||
def test_build_context_from_params(self):
|
||||
ctx = TTSContext(
|
||||
split_pattern=r"(?<=[.!?\-])\s+",
|
||||
pronunciation_rules=None,
|
||||
heteronym_rules=None,
|
||||
normalization_overrides=None,
|
||||
)
|
||||
assert ctx.split_pattern
|
||||
assert ctx.normalize("Hello world.") is not None
|
||||
|
||||
def test_build_context_with_compiled_rules(self):
|
||||
from abogen.domain.pronunciation import compile_pronunciation_rules
|
||||
rules = compile_pronunciation_rules([{"pattern": "test", "replacement": "Test"}])
|
||||
ctx = TTSContext(
|
||||
split_pattern=r"\n+",
|
||||
pronunciation_rules=rules,
|
||||
)
|
||||
result = ctx.normalize("test text")
|
||||
assert isinstance(result, str)
|
||||
|
||||
def test_usage_counter_tracking(self):
|
||||
ctx = TTSContext()
|
||||
ctx.usage_counter["token1"] = 0
|
||||
ctx.normalize("Some text with token1")
|
||||
# Usage counter should be passed through (may or may not increment
|
||||
# depending on whether the token matches)
|
||||
assert isinstance(ctx.usage_counter, dict)
|
||||
|
||||
|
||||
class TestOutputDirectoryResolution:
|
||||
"""Verify output directory can be resolved from parameters."""
|
||||
|
||||
def test_resolve_output_directory(self, tmp_path):
|
||||
from abogen.domain.output_paths import resolve_output_directory
|
||||
result = resolve_output_directory(
|
||||
save_mode="Save next to input file",
|
||||
stored_path=tmp_path / "test.txt",
|
||||
output_folder=None,
|
||||
desktop_dir=tmp_path,
|
||||
user_output_path=None,
|
||||
user_cache_outputs=tmp_path,
|
||||
)
|
||||
assert result is not None
|
||||
assert isinstance(result, Path)
|
||||
|
||||
def test_resolve_output_with_explicit_folder(self, tmp_path):
|
||||
from abogen.domain.output_paths import resolve_output_directory
|
||||
custom_dir = tmp_path / "custom_output"
|
||||
custom_dir.mkdir()
|
||||
result = resolve_output_directory(
|
||||
save_mode="Save to custom folder",
|
||||
stored_path=tmp_path / "test.txt",
|
||||
output_folder=str(custom_dir),
|
||||
desktop_dir=tmp_path,
|
||||
user_output_path=None,
|
||||
user_cache_outputs=tmp_path,
|
||||
)
|
||||
assert result is not None
|
||||
|
||||
|
||||
class TestChapterSelection:
|
||||
"""Verify chapter selection logic works with various inputs."""
|
||||
|
||||
def test_auto_select_relevant_chapters(self):
|
||||
from abogen.domain.file_type import auto_select_relevant_chapters
|
||||
from abogen.text_extractor import ExtractedChapter
|
||||
chapters = [
|
||||
ExtractedChapter(title="Chapter 1", text="A" * 500),
|
||||
ExtractedChapter(title="Chapter 2", text="B" * 50),
|
||||
ExtractedChapter(title="Chapter 3", text="C" * 600),
|
||||
]
|
||||
result = auto_select_relevant_chapters(chapters, "txt")
|
||||
# Should filter out short chapters
|
||||
assert len(result.kept) >= 1
|
||||
assert isinstance(result.skipped, list)
|
||||
|
||||
def test_auto_select_all_long_chapters(self):
|
||||
from abogen.domain.file_type import auto_select_relevant_chapters
|
||||
from abogen.text_extractor import ExtractedChapter
|
||||
chapters = [
|
||||
ExtractedChapter(title="Chapter 1", text="A" * 500),
|
||||
ExtractedChapter(title="Chapter 2", text="B" * 500),
|
||||
]
|
||||
result = auto_select_relevant_chapters(chapters, "txt")
|
||||
assert len(result.kept) == 2
|
||||
assert len(result.skipped) == 0
|
||||
|
||||
def test_metadata_merge(self):
|
||||
from abogen.domain.metadata_merge import merge_metadata
|
||||
base = {"title": "Original Title", "author": "Author A"}
|
||||
overrides = {"title": "New Title"}
|
||||
result = merge_metadata(base, overrides)
|
||||
assert result["title"] == "New Title"
|
||||
assert result["author"] == "Author A"
|
||||
|
||||
|
||||
class TestCancellationProtocol:
|
||||
"""Verify cancellation mechanism can be implemented as a callback."""
|
||||
|
||||
def test_cancellation_flag_check(self):
|
||||
class FakeJob:
|
||||
def __init__(self):
|
||||
self.cancel_requested = False
|
||||
|
||||
job = FakeJob()
|
||||
check = lambda: job.cancel_requested
|
||||
assert check() is False
|
||||
|
||||
job.cancel_requested = True
|
||||
assert check() is True
|
||||
|
||||
def test_cancellation_exception_pattern(self):
|
||||
"""WebUI uses exception-based cancellation."""
|
||||
class JobCancelled(Exception):
|
||||
pass
|
||||
|
||||
def canceller():
|
||||
raise JobCancelled()
|
||||
|
||||
with pytest.raises(JobCancelled):
|
||||
canceller()
|
||||
|
||||
|
||||
class TestLoggingProtocol:
|
||||
"""Verify logging can be abstracted as a callback."""
|
||||
|
||||
def test_log_callback(self):
|
||||
logs = []
|
||||
def log_fn(msg, level="info"):
|
||||
logs.append((msg, level))
|
||||
|
||||
log_fn("Test message", "info")
|
||||
assert len(logs) == 1
|
||||
assert logs[0] == ("Test message", "info")
|
||||
|
||||
def test_progress_callback(self):
|
||||
progress_calls = []
|
||||
def progress_fn(processed, total, etr):
|
||||
progress_calls.append((processed, total, etr))
|
||||
|
||||
progress_fn(100, 1000, "0:05:00")
|
||||
assert len(progress_calls) == 1
|
||||
assert progress_calls[0] == (100, 1000, "0:05:00")
|
||||
Reference in New Issue
Block a user