Files
unredact/tests/test_solver.py
T
Alex-GilbertandClaude Opus 4.6 fc9eff2da5 test: add round-trip integration test for solver
Measures a known word's width in font context, then verifies the solver
recovers that word from its width. Proves the width table and solver
work correctly end-to-end.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
2026-02-15 22:26:20 -06:00

184 lines
5.8 KiB
Python

import pytest
from PIL import ImageFont
from unredact.pipeline.solver import solve_gap, solve_gap_parallel, SolveResult
def _get_test_font() -> ImageFont.FreeTypeFont:
import subprocess
result = subprocess.run(
["fc-match", "--format=%{file}", "Liberation Serif"],
capture_output=True, text=True,
)
path = result.stdout.strip()
return ImageFont.truetype(path, 40)
class TestSolveGap:
def test_finds_exact_match(self):
"""Given a gap that exactly fits 'hello', solver should find it."""
font = _get_test_font()
target = font.getlength("hello")
results = solve_gap(
font=font,
charset="ehlo",
target_width=target,
tolerance=0.5,
min_length=5,
max_length=5,
left_context="",
right_context="",
)
texts = [r.text for r in results]
assert "hello" in texts
def test_respects_tolerance_zero(self):
"""With zero tolerance, only exact pixel matches are returned."""
font = _get_test_font()
target = font.getlength("ab")
results = solve_gap(
font=font,
charset="ab",
target_width=target,
tolerance=0.0,
min_length=2,
max_length=2,
left_context="",
right_context="",
)
texts = [r.text for r in results]
assert "ab" in texts
def test_respects_length_bounds(self):
"""Results should all be within min/max length."""
font = _get_test_font()
target = font.getlength("test")
results = solve_gap(
font=font,
charset="tes",
target_width=target,
tolerance=2.0,
min_length=3,
max_length=5,
left_context="",
right_context="",
)
for r in results:
assert 3 <= len(r.text) <= 5
def test_with_context_chars(self):
"""Solver accounts for left/right boundary kerning."""
font = _get_test_font()
# Measure "es" when preceded by "T"
target = font.getlength("Tes") - font.getlength("T")
results = solve_gap(
font=font,
charset="est",
target_width=target,
tolerance=0.5,
min_length=2,
max_length=2,
left_context="T",
right_context="",
)
texts = [r.text for r in results]
assert "es" in texts
def test_empty_results_for_impossible_width(self):
"""If target is impossibly small, return no results."""
font = _get_test_font()
results = solve_gap(
font=font,
charset="abc",
target_width=0.1,
tolerance=0.0,
min_length=1,
max_length=5,
left_context="",
right_context="",
)
assert len(results) == 0
def test_result_contains_width_and_error(self):
"""Each result should have text, width, and error fields."""
font = _get_test_font()
target = font.getlength("ab")
results = solve_gap(
font=font,
charset="ab",
target_width=target,
tolerance=1.0,
min_length=2,
max_length=2,
left_context="",
right_context="",
)
assert len(results) > 0
r = results[0]
assert isinstance(r.text, str)
assert isinstance(r.width, float)
assert isinstance(r.error, float)
assert r.error >= 0
class TestSolveGapParallel:
def test_same_results_as_serial(self):
"""Parallel solver should find the same results as serial."""
font = _get_test_font()
target = font.getlength("hello")
serial = solve_gap(
font=font, charset="ehlo", target_width=target,
tolerance=0.5, min_length=5, max_length=5,
)
parallel = solve_gap_parallel(
font=font, charset="ehlo", target_width=target,
tolerance=0.5, min_length=5, max_length=5,
)
assert set(r.text for r in serial) == set(r.text for r in parallel)
def test_parallel_with_larger_charset(self):
"""Should handle a real-sized charset without errors."""
font = _get_test_font()
target = font.getlength("cat")
results = solve_gap_parallel(
font=font, charset="abcdefghijklmnopqrstuvwxyz",
target_width=target, tolerance=0.5,
min_length=3, max_length=3,
)
texts = [r.text for r in results]
assert "cat" in texts
def test_progress_callback(self):
"""Progress callback should be called with node counts."""
font = _get_test_font()
target = font.getlength("ab")
progress = []
solve_gap_parallel(
font=font, charset="abc", target_width=target,
tolerance=1.0, min_length=2, max_length=2,
on_progress=lambda checked, found: progress.append((checked, found)),
)
assert len(progress) > 0
class TestRoundTrip:
def test_known_word_found_with_real_font(self):
"""Measure a word, then solve for it — the word should appear in results."""
font = _get_test_font()
word = "Smith"
# Measure "Smith" when preceded by "o" (left context)
target = font.getlength("o" + word) - font.getlength("o")
results = solve_gap(
font=font,
charset="abcdefghijklmnopqrstuvwxyzST", # include needed uppercase
target_width=target,
tolerance=0.5,
min_length=5,
max_length=5,
left_context="o",
right_context="",
)
texts = [r.text for r in results]
assert "Smith" in texts