mirror of
https://github.com/PR0M3TH3AN/unredact.git
synced 2026-10-27 16:50:30 +00:00
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]>
184 lines
5.8 KiB
Python
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
|