Skip to content

Commit cf51c1f

Browse files
committed
Move ascii_art tests to new file and require cv2
Extract the ASCII-art-related fixtures and tests from tests/gui/test_app_entrypoint.py into a new tests/gui/test_ascii_art.py to separate concerns and improve test organization. Replace pytest.skip on missing OpenCV with raising ImportError so tests fail fast when cv2 is not installed (ensures main deps are present). Keep a small assertion fix in the original file (win_instance.show.assert_called_once()).
1 parent 0a72842 commit cf51c1f

File tree

2 files changed

+278
-276
lines changed

2 files changed

+278
-276
lines changed

tests/gui/test_app_entrypoint.py

Lines changed: 0 additions & 276 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,11 @@
22
from __future__ import annotations
33

44
import importlib
5-
import os
65
import sys
7-
from pathlib import Path
86
from unittest.mock import MagicMock
97

10-
import numpy as np
118
import pytest
129

13-
try:
14-
import cv2 as cv
15-
except Exception:
16-
pytest.skip("OpenCV (opencv-python) is required for these tests.", allow_module_level=True)
17-
18-
import dlclivegui.assets.ascii_art as ascii_mod
19-
2010
MODULE_UNDER_TEST = "dlclivegui.main"
2111

2212

@@ -141,269 +131,3 @@ def test_main_without_splash(monkeypatch, set_use_splash_false):
141131
show_splash_mock.assert_not_called()
142132
assert calls["count"] == 0
143133
win_instance.show.assert_called_once()
144-
145-
146-
# -------------------------
147-
# Fixtures & small helpers
148-
# -------------------------
149-
150-
151-
@pytest.fixture
152-
def tmp_png_gray(tmp_path: Path):
153-
"""Create a simple 16x8 gray gradient PNG without alpha."""
154-
h, w = 8, 16
155-
# Horizontal gradient from black to white in BGR
156-
x = np.linspace(0, 255, w, dtype=np.uint8)
157-
img = np.tile(x, (h, 1))
158-
bgr = cv.cvtColor(img, cv.COLOR_GRAY2BGR)
159-
p = tmp_path / "gray.png"
160-
assert cv.imwrite(str(p), bgr)
161-
return p
162-
163-
164-
@pytest.fixture
165-
def tmp_png_bgra_logo(tmp_path: Path):
166-
"""Create a small BGRA image with a transparent border and opaque center."""
167-
h, w = 10, 20
168-
bgra = np.zeros((h, w, 4), dtype=np.uint8)
169-
# Opaque blue rectangle in center
170-
bgra[2:8, 5:15, 0] = 255 # B
171-
bgra[2:8, 5:15, 3] = 255 # A
172-
p = tmp_path / "logo_bgra.png"
173-
assert cv.imwrite(str(p), bgra)
174-
return p
175-
176-
177-
def _force_isatty(monkeypatch, obj, value: bool):
178-
"""
179-
Ensure obj.isatty() returns value.
180-
Try instance patch first; if the object disallows attribute assignment,
181-
patch the method on its class.
182-
"""
183-
try:
184-
# Try patching the instance directly
185-
monkeypatch.setattr(obj, "isatty", lambda: value, raising=False)
186-
except Exception:
187-
# Fallback: patch the class method
188-
cls = type(obj)
189-
monkeypatch.setattr(cls, "isatty", lambda self: value, raising=True)
190-
191-
192-
@pytest.fixture
193-
def force_tty(monkeypatch):
194-
"""
195-
Pretend stdout is a TTY and provide a stable terminal size inside the
196-
module-under-test namespace (and the actual sys).
197-
"""
198-
# NO_COLOR must be unset for should_use_color("auto")
199-
monkeypatch.delenv("NO_COLOR", raising=False)
200-
201-
# Make whatever stdout object exists report TTY=True
202-
_force_isatty(monkeypatch, sys.stdout, True)
203-
_force_isatty(monkeypatch, ascii_mod.sys.stdout, True)
204-
205-
# Ensure terminal size used by the module is deterministic
206-
monkeypatch.setattr(
207-
ascii_mod.shutil,
208-
"get_terminal_size",
209-
lambda fallback=None: os.terminal_size((80, 24)),
210-
raising=True,
211-
)
212-
return sys.stdout # not used directly, but handy
213-
214-
215-
@pytest.fixture
216-
def force_notty(monkeypatch):
217-
"""
218-
Pretend stdout is not a TTY.
219-
"""
220-
_force_isatty(monkeypatch, sys.stdout, False)
221-
_force_isatty(monkeypatch, ascii_mod.sys.stdout, False)
222-
return sys.stdout
223-
224-
225-
# -------------------------
226-
# Terminal / ANSI behavior
227-
# -------------------------
228-
229-
230-
def test_get_terminal_width_tty(force_tty):
231-
width = ascii_mod.get_terminal_width(default=123)
232-
assert width == 80
233-
234-
235-
def test_get_terminal_width_notty(force_notty):
236-
width = ascii_mod.get_terminal_width(default=123)
237-
assert width == 123
238-
239-
240-
def test_should_use_color_auto_tty(force_tty, monkeypatch):
241-
monkeypatch.delenv("NO_COLOR", raising=False)
242-
assert ascii_mod.should_use_color("auto") is True
243-
244-
245-
def test_should_use_color_auto_no_color(force_tty, monkeypatch):
246-
monkeypatch.setenv("NO_COLOR", "1")
247-
assert ascii_mod.should_use_color("auto") is False
248-
249-
250-
def test_should_use_color_modes(force_notty):
251-
assert ascii_mod.should_use_color("never") is False
252-
assert ascii_mod.should_use_color("always") is True
253-
254-
255-
def test_terminal_is_wide_enough(force_tty):
256-
assert ascii_mod.terminal_is_wide_enough(60) is True
257-
assert ascii_mod.terminal_is_wide_enough(100) is False
258-
259-
260-
# -------------------------
261-
# Image helpers
262-
# -------------------------
263-
264-
265-
def test__to_bgr_converts_gray():
266-
gray = np.zeros((5, 7), dtype=np.uint8)
267-
bgr = ascii_mod._to_bgr(gray)
268-
assert bgr.shape == (5, 7, 3)
269-
assert bgr.dtype == np.uint8
270-
271-
272-
def test_composite_over_color_bgra(tmp_png_bgra_logo):
273-
img = cv.imread(str(tmp_png_bgra_logo), cv.IMREAD_UNCHANGED)
274-
assert img.shape[2] == 4
275-
bgr = ascii_mod.composite_over_color(img, bg_bgr=(10, 20, 30))
276-
assert bgr.shape[2] == 3
277-
# Transparent border should become the bg color
278-
assert tuple(bgr[0, 0]) == (10, 20, 30)
279-
# Opaque center should keep blue channel high
280-
assert bgr[5, 10, 0] == 255
281-
282-
283-
def test_crop_to_content_alpha(tmp_png_bgra_logo):
284-
img = cv.imread(str(tmp_png_bgra_logo), cv.IMREAD_UNCHANGED)
285-
cropped = ascii_mod.crop_to_content_alpha(img, alpha_thresh=1, pad=0)
286-
h, w = cropped.shape[:2]
287-
assert h == 6 # 2..7 -> 6 rows
288-
assert w == 10 # 5..14 -> 10 cols
289-
assert cropped[..., 3].min() == 255
290-
291-
292-
def test_crop_to_content_bg_white(tmp_path):
293-
# Create white background with a black rectangle
294-
h, w = 12, 20
295-
bgr = np.full((h, w, 3), 255, dtype=np.uint8)
296-
bgr[3:10, 4:15] = 0
297-
p = tmp_path / "white_bg.png"
298-
assert cv.imwrite(str(p), bgr)
299-
cropped = ascii_mod.crop_to_content_bg(bgr, bg="white", tol=10, pad=0)
300-
assert cropped.shape[0] == 7 # 3..9 -> 7 rows
301-
assert cropped.shape[1] == 11 # 4..14 -> 11 cols
302-
303-
304-
def test_resize_for_terminal_aspect_env(monkeypatch):
305-
img = np.zeros((100, 200, 3), dtype=np.uint8)
306-
monkeypatch.setenv("DLCLIVE_ASCII_ASPECT", "0.25")
307-
resized = ascii_mod.resize_for_terminal(img, width=80, aspect=None)
308-
# new_h = (h/w) * width * aspect = (100/200)*80*0.25 = 10
309-
assert resized.shape[:2] == (10, 80)
310-
311-
312-
# -------------------------
313-
# Rendering
314-
# -------------------------
315-
316-
317-
def test_map_luminance_to_chars_simple():
318-
gray = np.array([[0, 127, 255]], dtype=np.uint8)
319-
lines = list(ascii_mod._map_luminance_to_chars(gray, fine=False))
320-
assert len(lines) == 1
321-
# First char should be the densest in the simple ramp '@', last should be space
322-
assert lines[0][0] == ascii_mod.ASCII_RAMP_SIMPLE[0]
323-
assert lines[0][-1] == ascii_mod.ASCII_RAMP_SIMPLE[-1]
324-
325-
326-
def test_color_ascii_lines_basic():
327-
# Small 2x3 color blocks
328-
img = np.zeros((2, 3, 3), dtype=np.uint8)
329-
img[:] = (10, 20, 30)
330-
lines = list(ascii_mod._color_ascii_lines(img, fine=False, invert=False))
331-
assert len(lines) == 2
332-
# Expect ANSI 24-bit color sequence present
333-
assert "\x1b[38;2;" in lines[0]
334-
# Reset code present
335-
assert lines[0].endswith("\x1b[0m" * 3) is False # individual chars have resets, but line won't end with triple
336-
337-
338-
# -------------------------
339-
# Public API: generate & print
340-
# -------------------------
341-
342-
343-
@pytest.mark.parametrize("use_color", ["never", "always"])
344-
def test_generate_ascii_lines_gray(tmp_png_gray, use_color, force_tty):
345-
lines = list(
346-
ascii_mod.generate_ascii_lines(
347-
str(tmp_png_gray),
348-
width=40,
349-
aspect=0.5,
350-
color=use_color,
351-
fine=False,
352-
invert=False,
353-
crop_content=False,
354-
crop_bg="none",
355-
)
356-
)
357-
assert len(lines) > 0
358-
# Width equals number of characters per line
359-
assert all(len(line) == 40 or ("\x1b[38;2;" in line and len(_strip_ansi(line)) == 40) for line in lines)
360-
361-
362-
def _strip_ansi(s: str) -> str:
363-
import re
364-
365-
return re.sub(r"\x1b\[[0-9;]*m", "", s)
366-
367-
368-
def test_generate_ascii_lines_crop_alpha(tmp_png_bgra_logo, force_tty):
369-
lines_no_crop = list(
370-
ascii_mod.generate_ascii_lines(str(tmp_png_bgra_logo), width=40, aspect=0.5, color="never", crop_content=False)
371-
)
372-
lines_crop = list(
373-
ascii_mod.generate_ascii_lines(str(tmp_png_bgra_logo), width=40, aspect=0.5, color="never", crop_content=True)
374-
)
375-
# Both are non-empty; height may change either way depending on aspect ratio
376-
assert len(lines_no_crop) > 0 and len(lines_crop) > 0
377-
# Cropping should affect the generated ASCII content
378-
assert lines_crop != lines_no_crop
379-
380-
381-
def test_print_ascii_writes_file(tmp_png_gray, force_tty, tmp_path):
382-
out_path = tmp_path / "out.txt"
383-
ascii_mod.print_ascii(
384-
str(tmp_png_gray),
385-
width=30,
386-
aspect=0.5,
387-
color="never",
388-
output=str(out_path),
389-
)
390-
assert out_path.exists()
391-
text = out_path.read_text(encoding="utf-8")
392-
# Expect multiple lines of length 30
393-
lines = [ln for ln in text.splitlines() if ln]
394-
assert len(lines) > 0
395-
assert all(len(ln) == 30 for ln in lines)
396-
397-
398-
def test_build_help_description_tty(tmp_png_bgra_logo, monkeypatch, force_tty):
399-
monkeypatch.setattr(ascii_mod, "ASCII_IMAGE_PATH", Path(tmp_png_bgra_logo))
400-
desc = ascii_mod.build_help_description(static_banner=None, color="always", min_width=60)
401-
assert "DeepLabCut-Live GUI" in desc
402-
assert "\x1b[36m" in desc # cyan wrapper now present since TTY is mocked correctly
403-
404-
405-
def test_build_help_description_notty(tmp_png_bgra_logo, monkeypatch, force_notty):
406-
monkeypatch.setattr(ascii_mod, "ASCII_IMAGE_PATH", Path(tmp_png_bgra_logo))
407-
desc = ascii_mod.build_help_description(static_banner=None, color="always", min_width=60)
408-
# Not a TTY -> no banner, just the plain description
409-
assert desc.strip() == "DeepLabCut-Live GUI — launch the graphical interface."

0 commit comments

Comments
 (0)