From f00249db9d1ea051d29aa1bcca869fc4b88e83eb Mon Sep 17 00:00:00 2001 From: historia Date: Mon, 24 Aug 2026 02:59:26 -0400 Subject: refactor: add app directory, dir structure change --- tests/test_cover.py | 189 ---------------------------------------------------- 1 file changed, 189 deletions(-) delete mode 100644 tests/test_cover.py (limited to 'tests/test_cover.py') diff --git a/tests/test_cover.py b/tests/test_cover.py deleted file mode 100644 index f19db5a..0000000 --- a/tests/test_cover.py +++ /dev/null @@ -1,189 +0,0 @@ -"""Tests for stdlib-only cover generation: PNG structure, gradient, text.""" - -import random -import struct -import tempfile -import unittest -import zlib -from pathlib import Path - -from converter.cover import ( - _random_light_color, - _text_width, - _wrap_title, - generate_cover, -) - - -def _decode_png(data: bytes): - """Parse a PNG into (width, height, rows of RGB tuples).""" - assert data[:8] == b"\x89PNG\r\n\x1a\n", "bad PNG signature" - pos = 8 - idat = b"" - width = height = None - while pos < len(data): - length, chunk_type = struct.unpack(">I4s", data[pos:pos + 8]) - chunk_data = data[pos + 8:pos + 8 + length] - crc = struct.unpack(">I", data[pos + 8 + length:pos + 12 + length])[0] - assert crc == zlib.crc32(chunk_type + chunk_data) & 0xFFFFFFFF, "bad CRC" - if chunk_type == b"IHDR": - width, height, depth, color_type = struct.unpack(">IIBB", chunk_data[:10]) - assert depth == 8 and color_type == 2 # 8-bit RGB - elif chunk_type == b"IDAT": - idat += chunk_data - pos += 12 + length - raw = zlib.decompress(idat) - stride = 1 + width * 3 - assert len(raw) == height * stride, "unexpected decompressed size" - rows = [] - for y in range(height): - row = raw[y * stride + 1:(y + 1) * stride] - rows.append([tuple(row[x * 3:x * 3 + 3]) for x in range(width)]) - return width, height, rows - - -def _black_pixels(rows): - return sum(1 for row in rows for pixel in row if pixel == (0, 0, 0)) - - -class GenerateCoverTests(unittest.TestCase): - def _write(self, title, width=120, height=180, seed=7): - with tempfile.TemporaryDirectory() as tmp: - path = Path(tmp) / "cover.png" - result = generate_cover(title, path, width=width, height=height, seed=seed) - data = path.read_bytes() - return result, data - - def test_valid_png_with_requested_dimensions(self): - _, data = self._write("Hello") - width, height, rows = _decode_png(data) - self.assertEqual((width, height), (120, 180)) - self.assertEqual(len(rows), 180) - - def test_gradient_matches_seeded_colors(self): - _, data = self._write("Hello", seed=42) - width, height, rows = _decode_png(data) - rng = random.Random(42) - top = _random_light_color(rng) - bottom = _random_light_color(rng) - # Corners of the text-free top/bottom rows match the endpoints - self.assertEqual(rows[0][0], top) - self.assertEqual(rows[0][-1], top) - self.assertEqual(rows[height - 1][0], bottom) - self.assertEqual(rows[height - 1][-1], bottom) - - def test_gradient_colors_are_light(self): - # Text-free bottom row: every channel must stay in pastel territory - _, data = self._write("Hello", seed=1) - _, height, rows = _decode_png(data) - for channel in rows[height - 1][0]: - self.assertGreaterEqual(channel, 90) - - def test_title_renders_black_pixels(self): - _, data = self._write("Hello") - _, _, rows = _decode_png(data) - self.assertGreater(_black_pixels(rows), 50) - - def test_title_renders_white_pixels(self): - _, data = self._write("Hello") - _, _, rows = _decode_png(data) - white = sum(1 for row in rows for pixel in row if pixel == (255, 255, 255)) - self.assertGreater(white, 50) - - def test_white_text_sits_on_black_stroke(self): - # Directly above a white pixel row there must be a black stroke row: - # sample white pixels and confirm black neighbors within stroke width. - _, data = self._write("Hi", width=200, height=100, seed=3) - _, _, rows = _decode_png(data) - whites = [(x, y) for y, row in enumerate(rows) - for x, pixel in enumerate(row) if pixel == (255, 255, 255)] - self.assertTrue(whites) - checked = near_stroke = 0 - for x, y in whites[::5]: - neighborhood = [] - for dy in range(-3, 4): - for dx in range(-3, 4): - if 0 <= y + dy < len(rows) and 0 <= x + dx < len(rows[0]): - neighborhood.append(rows[y + dy][x + dx]) - checked += 1 - if (0, 0, 0) in neighborhood: - near_stroke += 1 - # Interior white pixels are surrounded by white; every sampled pixel - # should still see stroke black within 3px (font strokes are 5-6 px thick) - self.assertEqual(near_stroke, checked) - - def test_empty_title_renders_gradient_only(self): - _, data = self._write("") - _, _, rows = _decode_png(data) - self.assertEqual(_black_pixels(rows), 0) - - def test_unrenderable_title_degrades_to_gradient(self): - # CJK glyphs are not in the bitmap font; no crash, no text pixels - _, data = self._write("书名") - _, _, rows = _decode_png(data) - self.assertEqual(_black_pixels(rows), 0) - - def test_write_failure_returns_none(self): - result = generate_cover("Hello", Path("/nonexistent_dir/cover.png")) - self.assertIsNone(result) - - -class WrapTitleTests(unittest.TestCase): - def test_short_title_one_line(self): - self.assertEqual(len(_wrap_title("Dune", 500)), 1) - - def test_long_title_wraps(self): - lines = _wrap_title("The Extremely Long Windy Title of a Very Long Book", 600) - self.assertGreater(len(lines), 1) - for line in lines: - self.assertLessEqual(_text_width(line), 600) - - def test_single_long_word_kept_intact(self): - lines = _wrap_title("Antidisestablishmentarianism", 10) - self.assertEqual(lines, ["Antidisestablishmentarianism"]) - - def test_empty_title_no_lines(self): - self.assertEqual(_wrap_title("", 500), []) - - -class TextWidthTests(unittest.TestCase): - def test_empty(self): - self.assertEqual(_text_width(""), 0) - - def test_single_char_is_scaled_glyph(self): - self.assertEqual(_text_width("A"), 30) # 5 px * scale 6 - - def test_chars_include_spacing(self): - self.assertEqual(_text_width("AB"), 66) # (2 glyphs * 6 - 1) * 6 - - -class DropShadowTests(unittest.TestCase): - def _cover_rows(self, title, seed=7): - with tempfile.TemporaryDirectory() as tmp: - path = Path(tmp) / "cover.png" - generate_cover(title, path, width=200, height=200, seed=seed) - return _decode_png(path.read_bytes())[2] - - def test_shadow_pixels_survive_next_to_text(self): - rows = self._cover_rows("Hi") - # The shadow lives down-right of the glyphs: there must be darkened - # (but not pure black, not full-brightness) pixels beyond the text - # block's bottom edge. - blacks = {(x, y) for y, row in enumerate(rows) - for x, pixel in enumerate(row) if pixel == (0, 0, 0)} - self.assertTrue(blacks, "no text rendered") - text_bottom = max(y for _, y in blacks) - darkened = [pixel for y, row in enumerate(rows) - if y > text_bottom for pixel in row - if pixel != (0, 0, 0) and max(pixel) < 130] - self.assertTrue(darkened, "no shadow pixels below the text") - - def test_empty_title_has_no_shadow(self): - rows = self._cover_rows("") - for row in rows: - for pixel in row: - self.assertNotEqual(pixel, (0, 0, 0)) - - -if __name__ == "__main__": - unittest.main() -- cgit v1.2.3