Uvicorn заводил свои обработчики с propagate=False, поэтому его строки шли мимо нашего формата и рвались посреди слова. log_config=None отдаёт всё общему обработчику. Занятый порт сервис теперь замечает до запуска и объясняет, что делать, вместо строки uvicorn про Errno 10048. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
114 lines
4.6 KiB
Python
114 lines
4.6 KiB
Python
"""Проверки читаемости консольного вывода."""
|
|
import logging
|
|
import sys
|
|
from unittest.mock import patch
|
|
|
|
from app.console import ConsoleFormatter, enable_ansi, setup_logging
|
|
|
|
|
|
def record(level: int, message: str) -> logging.LogRecord:
|
|
return logging.LogRecord("talkscore-asr", level, __file__, 1, message, None, None)
|
|
|
|
|
|
class TestWrapping:
|
|
def test_short_message_stays_on_one_line(self):
|
|
out = ConsoleFormatter(colored=False).format(record(logging.INFO, "готово"))
|
|
assert out.count("\n") == 0
|
|
assert out.endswith("готово")
|
|
|
|
def test_long_message_wraps(self):
|
|
out = ConsoleFormatter(colored=False, width=lambda: 80).format(
|
|
record(logging.INFO, "слово " * 60))
|
|
assert out.count("\n") >= 1
|
|
assert all(len(line) <= 80 for line in out.splitlines())
|
|
|
|
def test_continuation_aligned_under_message(self):
|
|
"""Перенос идёт под текст, а не под начало строки - так видно границу сообщений."""
|
|
out = ConsoleFormatter(colored=False, width=lambda: 70).format(
|
|
record(logging.WARNING, "слово " * 40))
|
|
first, second = out.splitlines()[:2]
|
|
head = len(first) - len(first.lstrip()) + first.index("слово")
|
|
assert second.startswith(" " * head)
|
|
assert second.lstrip()[0] != " "
|
|
|
|
def test_existing_newlines_are_kept(self):
|
|
out = ConsoleFormatter(colored=False).format(
|
|
record(logging.INFO, "первая\nвторая"))
|
|
assert "первая" in out.splitlines()[0]
|
|
assert "вторая" in out.splitlines()[1]
|
|
|
|
|
|
class TestColour:
|
|
def test_no_escape_codes_when_colour_off(self):
|
|
"""Главное требование: без цвета в выводе не должно быть ANSI-мусора."""
|
|
for level in (logging.INFO, logging.WARNING, logging.ERROR):
|
|
out = ConsoleFormatter(colored=False).format(record(level, "текст"))
|
|
assert "\033" not in out
|
|
|
|
def test_warning_and_error_are_painted(self):
|
|
for level in (logging.WARNING, logging.ERROR):
|
|
out = ConsoleFormatter(colored=True).format(record(level, "текст"))
|
|
assert "\033[" in out
|
|
|
|
def test_info_level_is_not_painted(self):
|
|
"""Обычные сообщения остаются неокрашенными, иначе цвет перестаёт что-то значить."""
|
|
out = ConsoleFormatter(colored=True).format(record(logging.INFO, "текст"))
|
|
assert "\033[3" not in out # ни жёлтого, ни красного
|
|
|
|
|
|
class TestAnsiDetection:
|
|
def test_disabled_when_output_is_redirected(self):
|
|
"""Перенаправленный в файл вывод красить нельзя - в файл попадут коды."""
|
|
with patch.object(sys.stdout, "isatty", return_value=False):
|
|
assert enable_ansi() is False
|
|
|
|
|
|
class TestSetup:
|
|
def test_setup_replaces_handlers(self):
|
|
root = logging.getLogger()
|
|
before = root.handlers[:]
|
|
try:
|
|
setup_logging(logging.INFO)
|
|
assert len(root.handlers) == 1
|
|
assert isinstance(root.handlers[0].formatter, ConsoleFormatter)
|
|
finally:
|
|
root.handlers[:] = before
|
|
|
|
|
|
class TestPortCheck:
|
|
"""Занятый порт должен диагностироваться до запуска, а не строкой Errno 10048."""
|
|
|
|
def test_free_port_is_not_busy(self):
|
|
import socket as sk
|
|
|
|
from app.main import _port_busy
|
|
|
|
with sk.socket() as probe:
|
|
probe.bind(("127.0.0.1", 0))
|
|
free = probe.getsockname()[1]
|
|
# Сокет закрыт, порт снова свободен.
|
|
assert _port_busy("127.0.0.1", free) is False
|
|
|
|
def test_listening_port_is_busy(self):
|
|
import socket as sk
|
|
|
|
from app.main import _port_busy
|
|
|
|
with sk.socket() as server:
|
|
server.bind(("127.0.0.1", 0))
|
|
server.listen(1)
|
|
port = server.getsockname()[1]
|
|
assert _port_busy("127.0.0.1", port) is True
|
|
|
|
def test_wildcard_host_is_probed_via_loopback(self):
|
|
"""При host=0.0.0.0 подключаться к 0.0.0.0 нельзя, проверяем петлю."""
|
|
import socket as sk
|
|
|
|
from app.main import _port_busy
|
|
|
|
with sk.socket() as server:
|
|
server.bind(("127.0.0.1", 0))
|
|
server.listen(1)
|
|
port = server.getsockname()[1]
|
|
assert _port_busy("0.0.0.0", port) is True
|