|
| 1 | +"""Tests for ``commitizen.out``. |
| 2 | +
|
| 3 | +Mostly focused on the stdout-encoding helper introduced for #956: the |
| 4 | +function must reconfigure non-UTF-8 streams to UTF-8 with a permissive |
| 5 | +``errors="replace"`` strategy so commitizen output (emoji, typographic |
| 6 | +quotes) doesn't crash with ``UnicodeEncodeError`` on terminals using |
| 7 | +locale-dependent encodings such as ``cp1252`` (Windows) or |
| 8 | +``ISO8859-1`` (Linux/macOS). |
| 9 | +""" |
| 10 | + |
| 11 | +from __future__ import annotations |
| 12 | + |
| 13 | +import io |
| 14 | +from typing import Any |
| 15 | + |
| 16 | +from commitizen.out import _ensure_utf8_stdout |
| 17 | + |
| 18 | + |
| 19 | +class _StubStream(io.TextIOWrapper): |
| 20 | + """Light-weight ``TextIOWrapper`` that records calls to ``reconfigure``. |
| 21 | +
|
| 22 | + Subclassing ``TextIOWrapper`` keeps the ``isinstance`` check in |
| 23 | + ``_ensure_utf8_stdout`` happy without monkey-patching ``sys.stdout``. |
| 24 | + """ |
| 25 | + |
| 26 | + reconfigure_calls: list[dict[str, Any]] |
| 27 | + |
| 28 | + def __init__(self, encoding: str) -> None: |
| 29 | + super().__init__(io.BytesIO(), encoding=encoding) |
| 30 | + self.reconfigure_calls = [] |
| 31 | + |
| 32 | + def reconfigure(self, **kwargs: Any) -> None: |
| 33 | + self.reconfigure_calls.append(kwargs) |
| 34 | + super().reconfigure(**kwargs) |
| 35 | + |
| 36 | + |
| 37 | +def test_ensure_utf8_stdout_noop_when_already_utf8(): |
| 38 | + stream = _StubStream(encoding="utf-8") |
| 39 | + _ensure_utf8_stdout(stream) |
| 40 | + assert stream.reconfigure_calls == [] |
| 41 | + |
| 42 | + |
| 43 | +def test_ensure_utf8_stdout_noop_for_dashless_utf8_alias(): |
| 44 | + stream = _StubStream(encoding="UTF8") |
| 45 | + _ensure_utf8_stdout(stream) |
| 46 | + assert stream.reconfigure_calls == [] |
| 47 | + |
| 48 | + |
| 49 | +def test_ensure_utf8_stdout_reconfigures_iso8859_1_terminal(): |
| 50 | + """Regression test for #956 (Linux/macOS ``LANG=de_CH.ISO8859-1``).""" |
| 51 | + stream = _StubStream(encoding="latin-1") |
| 52 | + _ensure_utf8_stdout(stream) |
| 53 | + assert stream.reconfigure_calls == [{"encoding": "utf-8", "errors": "replace"}] |
| 54 | + |
| 55 | + |
| 56 | +def test_ensure_utf8_stdout_reconfigures_windows_cp1252(): |
| 57 | + """Regression test for the historical Windows ``cmd.exe`` case.""" |
| 58 | + stream = _StubStream(encoding="cp1252") |
| 59 | + _ensure_utf8_stdout(stream) |
| 60 | + assert stream.reconfigure_calls == [{"encoding": "utf-8", "errors": "replace"}] |
| 61 | + |
| 62 | + |
| 63 | +def test_ensure_utf8_stdout_skips_non_textio_streams(): |
| 64 | + class NotATextIO: |
| 65 | + encoding = "latin-1" |
| 66 | + reconfigure_calls: list[dict[str, Any]] = [] |
| 67 | + |
| 68 | + def reconfigure(self, **kwargs: Any) -> None: # pragma: no cover - unused |
| 69 | + self.reconfigure_calls.append(kwargs) |
| 70 | + |
| 71 | + stream = NotATextIO() |
| 72 | + _ensure_utf8_stdout(stream) |
| 73 | + assert stream.reconfigure_calls == [] |
| 74 | + |
| 75 | + |
| 76 | +def test_ensure_utf8_stdout_after_reconfigure_can_emit_emoji(): |
| 77 | + """End-to-end: after reconfiguration, writing an emoji must not raise.""" |
| 78 | + stream = _StubStream(encoding="latin-1") |
| 79 | + _ensure_utf8_stdout(stream) |
| 80 | + |
| 81 | + # Should not raise UnicodeEncodeError; ``errors="replace"`` lets |
| 82 | + # genuinely-unrenderable bytes fall through as ``?`` instead of |
| 83 | + # crashing the whole command. |
| 84 | + stream.write("Configuration complete \U0001f680") |
| 85 | + stream.flush() |
0 commit comments