import initial
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from PIL import Image
|
||||
from logimage.infrastructure.image.local_file_source import LocalFileImageSource
|
||||
from logimage.domain.value_objects.image_data import ImageData
|
||||
|
||||
|
||||
def test_fetch_returns_image_data(tmp_path: Path) -> None:
|
||||
img = Image.new("RGB", (50, 50), color=(100, 150, 200))
|
||||
path = tmp_path / "test_icon.png"
|
||||
img.save(path, format="PNG")
|
||||
|
||||
source = LocalFileImageSource(path)
|
||||
result = source.fetch()
|
||||
|
||||
assert isinstance(result, ImageData)
|
||||
assert result.content == path.read_bytes()
|
||||
assert result.title == "test_icon"
|
||||
|
||||
|
||||
def test_fetch_title_is_stem(tmp_path: Path) -> None:
|
||||
img = Image.new("RGB", (10, 10))
|
||||
path = tmp_path / "my_cool_image.jpg"
|
||||
img.save(path, format="JPEG")
|
||||
|
||||
source = LocalFileImageSource(path)
|
||||
result = source.fetch()
|
||||
|
||||
assert result.title == "my_cool_image"
|
||||
|
||||
|
||||
def test_fetch_raises_if_file_not_found() -> None:
|
||||
source = LocalFileImageSource(Path("/nonexistent/image.png"))
|
||||
with pytest.raises(FileNotFoundError):
|
||||
source.fetch()
|
||||
@@ -0,0 +1,28 @@
|
||||
import os
|
||||
import pytest
|
||||
from logimage.infrastructure.image.pexels_source import PexelsImageSource
|
||||
from logimage.domain.value_objects.image_data import ImageData
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_fetch_returns_image_data() -> None:
|
||||
api_key = os.environ.get("PEXELS_API_KEY")
|
||||
if not api_key:
|
||||
pytest.skip("PEXELS_API_KEY not set")
|
||||
source = PexelsImageSource(api_key=api_key)
|
||||
result = source.fetch()
|
||||
assert isinstance(result, ImageData)
|
||||
assert len(result.content) > 1000
|
||||
assert isinstance(result.title, str)
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_fetch_with_theme_returns_image_data() -> None:
|
||||
api_key = os.environ.get("PEXELS_API_KEY")
|
||||
if not api_key:
|
||||
pytest.skip("PEXELS_API_KEY not set")
|
||||
source = PexelsImageSource(api_key=api_key)
|
||||
result = source.fetch(theme="forest")
|
||||
assert isinstance(result, ImageData)
|
||||
assert len(result.content) > 1000
|
||||
assert isinstance(result.title, str)
|
||||
@@ -0,0 +1,92 @@
|
||||
import io
|
||||
from PIL import Image, ImageDraw
|
||||
from logimage.infrastructure.image.pillow_converter import PillowImageConverter
|
||||
from logimage.domain.value_objects.grid import Grid
|
||||
from tests.helpers import count_blocks
|
||||
|
||||
|
||||
def _make_jpeg_bytes(width: int = 100, height: int = 100, color: tuple[int, int, int] = (128, 64, 192)) -> bytes:
|
||||
img = Image.new("RGB", (width, height), color=color)
|
||||
buf = io.BytesIO()
|
||||
img.save(buf, format="JPEG")
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
def _make_silhouette_bytes(width: int = 100, height: int = 100) -> bytes:
|
||||
"""Image avec un carré noir centré sur fond blanc."""
|
||||
img = Image.new("RGB", (width, height), color=(255, 255, 255))
|
||||
draw = ImageDraw.Draw(img)
|
||||
margin = width // 4
|
||||
draw.rectangle([margin, margin, width - margin, height - margin], fill=(0, 0, 0))
|
||||
buf = io.BytesIO()
|
||||
img.save(buf, format="PNG")
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
def test_output_grid_has_correct_dimensions() -> None:
|
||||
converter = PillowImageConverter()
|
||||
grid = converter.to_grid(_make_jpeg_bytes(), width=10, height=10)
|
||||
assert grid.width == 10
|
||||
assert grid.height == 10
|
||||
|
||||
|
||||
def test_output_is_grid_instance() -> None:
|
||||
converter = PillowImageConverter()
|
||||
result = converter.to_grid(_make_jpeg_bytes(), width=15, height=15)
|
||||
assert isinstance(result, Grid)
|
||||
|
||||
|
||||
def test_output_cells_are_booleans() -> None:
|
||||
converter = PillowImageConverter()
|
||||
grid = converter.to_grid(_make_jpeg_bytes(), width=10, height=10)
|
||||
for row_idx in range(grid.height):
|
||||
for cell in grid.row(row_idx):
|
||||
assert isinstance(cell, bool)
|
||||
|
||||
|
||||
def test_black_image_produces_all_true_cells() -> None:
|
||||
img = Image.new("RGB", (100, 100), color=(0, 0, 0))
|
||||
buf = io.BytesIO()
|
||||
img.save(buf, format="JPEG")
|
||||
converter = PillowImageConverter()
|
||||
grid = converter.to_grid(buf.getvalue(), width=10, height=10)
|
||||
total_true = sum(cell for row in range(grid.height) for cell in grid.row(row))
|
||||
assert total_true > 0
|
||||
|
||||
|
||||
def test_white_image_produces_mostly_false_cells() -> None:
|
||||
img = Image.new("RGB", (100, 100), color=(255, 255, 255))
|
||||
buf = io.BytesIO()
|
||||
img.save(buf, format="JPEG")
|
||||
converter = PillowImageConverter()
|
||||
grid = converter.to_grid(buf.getvalue(), width=10, height=10)
|
||||
total_true = sum(cell for row in range(grid.height) for cell in grid.row(row))
|
||||
assert total_true < grid.width * grid.height
|
||||
|
||||
|
||||
def test_max_clue_blocks_respected_on_silhouette() -> None:
|
||||
"""Une silhouette simple doit produire ≤ max_clue_blocks blocs par ligne/colonne."""
|
||||
converter = PillowImageConverter(max_clue_blocks=6)
|
||||
grid = converter.to_grid(_make_silhouette_bytes(), width=20, height=20)
|
||||
for i in range(grid.height):
|
||||
blocks = count_blocks(grid.row(i))
|
||||
assert blocks <= 6, f"Ligne {i} a {blocks} blocs (max=6)"
|
||||
for j in range(grid.width):
|
||||
blocks = count_blocks(grid.col(j))
|
||||
assert blocks <= 6, f"Colonne {j} a {blocks} blocs (max=6)"
|
||||
|
||||
|
||||
def test_custom_max_clue_blocks() -> None:
|
||||
"""max_clue_blocks=3 produit une grille encore plus simple."""
|
||||
converter = PillowImageConverter(max_clue_blocks=3)
|
||||
grid = converter.to_grid(_make_silhouette_bytes(), width=15, height=15)
|
||||
for i in range(grid.height):
|
||||
assert count_blocks(grid.row(i)) <= 3
|
||||
for j in range(grid.width):
|
||||
assert count_blocks(grid.col(j)) <= 3
|
||||
|
||||
|
||||
def test_accepts_png_bytes() -> None:
|
||||
converter = PillowImageConverter()
|
||||
grid = converter.to_grid(_make_silhouette_bytes(), width=10, height=10)
|
||||
assert grid.width == 10
|
||||
@@ -0,0 +1,51 @@
|
||||
import io
|
||||
from pathlib import Path
|
||||
from PIL import Image, ImageDraw
|
||||
from logimage.infrastructure.image.pillow_converter import PillowImageConverter
|
||||
from logimage.infrastructure.image.local_file_source import LocalFileImageSource
|
||||
from tests.helpers import count_blocks
|
||||
|
||||
|
||||
def _make_icon_bytes() -> bytes:
|
||||
"""Maison simple : rectangle + triangle sur fond blanc."""
|
||||
img = Image.new("RGB", (100, 100), color=(255, 255, 255))
|
||||
draw = ImageDraw.Draw(img)
|
||||
# Corps de la maison
|
||||
draw.rectangle([25, 50, 75, 90], fill=(0, 0, 0))
|
||||
# Toit (triangle approximatif)
|
||||
draw.polygon([(50, 10), (20, 50), (80, 50)], fill=(0, 0, 0))
|
||||
buf = io.BytesIO()
|
||||
img.save(buf, format="PNG")
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
def test_house_icon_produces_playable_grid() -> None:
|
||||
"""Une icône maison simple doit donner une grille jouable (≤ 6 blocs)."""
|
||||
converter = PillowImageConverter(max_clue_blocks=6)
|
||||
grid = converter.to_grid(_make_icon_bytes(), width=20, height=20)
|
||||
|
||||
for i in range(grid.height):
|
||||
blocks = count_blocks(grid.row(i))
|
||||
assert blocks <= 6, f"Ligne {i} a {blocks} blocs"
|
||||
|
||||
for j in range(grid.width):
|
||||
blocks = count_blocks(grid.col(j))
|
||||
assert blocks <= 6, f"Colonne {j} a {blocks} blocs"
|
||||
|
||||
|
||||
def test_local_source_feeds_converter(tmp_path: Path) -> None:
|
||||
"""LocalFileImageSource + PillowImageConverter produisent une grille valide."""
|
||||
icon_path = tmp_path / "house.png"
|
||||
icon_path.write_bytes(_make_icon_bytes())
|
||||
|
||||
source = LocalFileImageSource(icon_path)
|
||||
image_data = source.fetch()
|
||||
|
||||
converter = PillowImageConverter(max_clue_blocks=6)
|
||||
grid = converter.to_grid(image_data.content, width=15, height=15)
|
||||
|
||||
assert len(image_data.content) > 0
|
||||
assert grid.width == 15
|
||||
assert grid.height == 15
|
||||
total_filled = sum(cell for r in range(grid.height) for cell in grid.row(r))
|
||||
assert total_filled > 0, "Pipeline should produce some filled cells for house icon"
|
||||
@@ -0,0 +1,80 @@
|
||||
import io
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from PIL import Image as PilImage
|
||||
from logimage.infrastructure.pdf.reportlab_exporter import ReportLabPdfExporter
|
||||
from logimage.domain.entities.puzzle import NonogramPuzzle
|
||||
from logimage.domain.value_objects.grid import Grid
|
||||
|
||||
|
||||
def _make_puzzle(rows: int = 10, cols: int = 10, title: str = "Test") -> NonogramPuzzle:
|
||||
cells = [[i % 3 == 0] * cols for i in range(rows)]
|
||||
grid = Grid.from_list(cells)
|
||||
return NonogramPuzzle.from_grid(grid, title)
|
||||
|
||||
|
||||
def test_export_creates_pdf_file() -> None:
|
||||
exporter = ReportLabPdfExporter()
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp) / "output.pdf"
|
||||
exporter.export([_make_puzzle()], path)
|
||||
assert path.exists()
|
||||
assert path.stat().st_size > 1000
|
||||
|
||||
|
||||
def test_export_multiple_puzzles_creates_larger_file() -> None:
|
||||
exporter = ReportLabPdfExporter()
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path_single = Path(tmp) / "single.pdf"
|
||||
path_multi = Path(tmp) / "multi.pdf"
|
||||
exporter.export([_make_puzzle()], path_single)
|
||||
exporter.export([_make_puzzle(), _make_puzzle(), _make_puzzle()], path_multi)
|
||||
assert path_multi.stat().st_size > path_single.stat().st_size
|
||||
|
||||
|
||||
def test_export_with_solution_creates_larger_file() -> None:
|
||||
exporter = ReportLabPdfExporter()
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path_no_sol = Path(tmp) / "no_solution.pdf"
|
||||
path_with_sol = Path(tmp) / "with_solution.pdf"
|
||||
puzzle = _make_puzzle()
|
||||
exporter.export([puzzle], path_no_sol, with_solution=False)
|
||||
exporter.export([puzzle], path_with_sol, with_solution=True)
|
||||
assert path_with_sol.stat().st_size > path_no_sol.stat().st_size
|
||||
|
||||
|
||||
def test_export_starts_with_pdf_magic_bytes() -> None:
|
||||
exporter = ReportLabPdfExporter()
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp) / "output.pdf"
|
||||
exporter.export([_make_puzzle()], path)
|
||||
assert path.read_bytes()[:4] == b"%PDF"
|
||||
|
||||
|
||||
def _make_puzzle_with_image(rows: int = 10, cols: int = 10) -> NonogramPuzzle:
|
||||
buf = io.BytesIO()
|
||||
PilImage.new("RGB", (40, 40), color=(100, 150, 200)).save(buf, format="JPEG")
|
||||
cells = [[i % 3 == 0] * cols for i in range(rows)]
|
||||
grid = Grid.from_list(cells)
|
||||
return NonogramPuzzle.from_grid(grid, "Test With Image", buf.getvalue())
|
||||
|
||||
|
||||
def test_solution_with_image_bytes_creates_larger_file() -> None:
|
||||
exporter = ReportLabPdfExporter()
|
||||
puzzle_no_img = _make_puzzle()
|
||||
puzzle_with_img = _make_puzzle_with_image()
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path_no_img = Path(tmp) / "no_img.pdf"
|
||||
path_with_img = Path(tmp) / "with_img.pdf"
|
||||
exporter.export([puzzle_no_img], path_no_img, with_solution=True)
|
||||
exporter.export([puzzle_with_img], path_with_img, with_solution=True)
|
||||
assert path_with_img.stat().st_size > path_no_img.stat().st_size
|
||||
|
||||
|
||||
def test_solution_without_image_bytes_still_produces_valid_pdf() -> None:
|
||||
exporter = ReportLabPdfExporter()
|
||||
puzzle = _make_puzzle() # image_bytes is None
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp) / "sol.pdf"
|
||||
exporter.export([puzzle], path, with_solution=True)
|
||||
assert path.read_bytes()[:4] == b"%PDF"
|
||||
@@ -0,0 +1,27 @@
|
||||
import os
|
||||
import pytest
|
||||
from logimage.infrastructure.image.unsplash_source import UnsplashImageSource
|
||||
from logimage.domain.value_objects.image_data import ImageData
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_fetch_returns_image_data() -> None:
|
||||
api_key = os.environ.get("UNSPLASH_ACCESS_KEY")
|
||||
if not api_key:
|
||||
pytest.skip("UNSPLASH_ACCESS_KEY not set")
|
||||
source = UnsplashImageSource(api_key=api_key)
|
||||
result = source.fetch()
|
||||
assert isinstance(result, ImageData)
|
||||
assert len(result.content) > 1000
|
||||
assert isinstance(result.title, str)
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_fetch_with_theme_returns_image_data() -> None:
|
||||
api_key = os.environ.get("UNSPLASH_ACCESS_KEY")
|
||||
if not api_key:
|
||||
pytest.skip("UNSPLASH_ACCESS_KEY not set")
|
||||
source = UnsplashImageSource(api_key=api_key)
|
||||
result = source.fetch(theme="forest")
|
||||
assert isinstance(result, ImageData)
|
||||
assert len(result.content) > 1000
|
||||
Reference in New Issue
Block a user