chore: prevent UnicodeEncodeError crashing error logging on Windows consoles (#13482)

fix windows testes folder
This commit is contained in:
Cristhian Zanforlin Lousa
2026-06-03 11:35:15 -03:00
committed by GitHub
parent cc97a32aee
commit 1d3a186138
4 changed files with 94 additions and 0 deletions

View File

@ -1,5 +1,8 @@
"""Logging module for lfx package."""
from lfx.log._streams import make_streams_resilient
from lfx.log.logger import configure, logger
make_streams_resilient()
__all__ = ["configure", "logger"]

View File

@ -0,0 +1,29 @@
"""Make process stdout/stderr tolerate characters their console codec cannot encode."""
import contextlib
import sys
from typing import Any
def _reconfigure(stream: Any) -> None:
reconfigure = getattr(stream, "reconfigure", None)
if reconfigure is None:
return
with contextlib.suppress(Exception):
reconfigure(errors="backslashreplace")
def make_streams_resilient() -> None:
"""Escape unencodable characters on write instead of raising.
Windows consoles default to a legacy code page (cp1252) whose codec raises
UnicodeEncodeError on the box-drawing glyphs structlog's ConsoleRenderer
emits when formatting a traceback. That second exception is raised inside
the logging call itself, so it masks the original error and aborts the
request that logged it. ``errors="backslashreplace"`` keeps the stream
encoding but escapes anything the code page can't represent, so logging
never crashes the caller. The underlying streams are reconfigured too
because colorama wraps those same objects, regardless of wrap order.
"""
for stream in (sys.stdout, sys.stderr, sys.__stdout__, sys.__stderr__):
_reconfigure(stream)

View File

@ -0,0 +1 @@
"""Test suite for LFX logging."""

View File

@ -0,0 +1,61 @@
"""Regression tests for stdout/stderr encoding resilience on Windows consoles."""
import importlib
import io
import structlog
from lfx.log._streams import make_streams_resilient
def _cp1252_stream() -> tuple[io.TextIOWrapper, io.BytesIO]:
"""A text stream that behaves like a strict Windows cp1252 console."""
raw = io.BytesIO()
return io.TextIOWrapper(raw, encoding="cp1252"), raw
def test_make_streams_resilient_sets_backslashreplace(monkeypatch):
out, _ = _cp1252_stream()
err, _ = _cp1252_stream()
monkeypatch.setattr("sys.stdout", out)
monkeypatch.setattr("sys.stderr", err)
make_streams_resilient()
assert out.errors == "backslashreplace"
assert err.errors == "backslashreplace"
def test_logging_exception_to_cp1252_stdout_does_not_crash(monkeypatch):
"""A logged traceback must never raise UnicodeEncodeError on a cp1252 console.
structlog's ConsoleRenderer emits box-drawing glyphs that cp1252 cannot
encode; without the resilience shim the encode error is raised inside the
logging call, masking the original error and aborting the request.
"""
stream, raw = _cp1252_stream()
monkeypatch.setattr("sys.stdout", stream)
make_streams_resilient()
logmod = importlib.import_module("lfx.log.logger")
logmod.configure(log_level="ERROR", cache=False)
log = structlog.get_logger("test")
msg = "THIS IS A TEST ERROR MESSAGE"
try:
raise ValueError(msg)
except ValueError:
log.exception("boom")
stream.flush()
rendered = raw.getvalue().decode("cp1252", errors="replace")
assert "THIS IS A TEST ERROR MESSAGE" in rendered
def test_reconfigure_is_safe_when_stream_is_none(monkeypatch):
monkeypatch.setattr("sys.stdout", None)
monkeypatch.setattr("sys.stderr", None)
monkeypatch.setattr("sys.__stdout__", None)
monkeypatch.setattr("sys.__stderr__", None)
make_streams_resilient()