From 3d6b95504820cae4e01acce772bdb7619205552b Mon Sep 17 00:00:00 2001 From: Ghanchu Date: Tue, 18 Nov 2025 21:01:25 -0500 Subject: [PATCH 1/4] Fixing an issue with pluggy and surrogate escape characters during tracing. #13750 in Pytest --- src/pluggy/_tracing.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pluggy/_tracing.py b/src/pluggy/_tracing.py index f0b36db1..6b471600 100644 --- a/src/pluggy/_tracing.py +++ b/src/pluggy/_tracing.py @@ -41,7 +41,8 @@ def _format_message(self, tags: Sequence[str], args: Sequence[object]) -> str: def _processmessage(self, tags: tuple[str, ...], args: tuple[object, ...]) -> None: if self._writer is not None and args: - self._writer(self._format_message(tags, args)) + msg = self._format_message(tags, args) + self._writer(msg.encode("utf-8", "replace").decode("utf-8")) try: processor = self._tags2proc[tags] except KeyError: From b41c07a5038e0f60ca043f810cec9a173a36e71e Mon Sep 17 00:00:00 2001 From: Ghanchu Date: Tue, 18 Nov 2025 21:39:05 -0500 Subject: [PATCH 2/4] added test in test_tracer.py to expose changes and increase code coverage --- changelog/13750(pytest).bugfix.rst | 3 +++ testing/test_tracer.py | 11 +++++++++++ 2 files changed, 14 insertions(+) create mode 100644 changelog/13750(pytest).bugfix.rst diff --git a/changelog/13750(pytest).bugfix.rst b/changelog/13750(pytest).bugfix.rst new file mode 100644 index 00000000..62d2dc7e --- /dev/null +++ b/changelog/13750(pytest).bugfix.rst @@ -0,0 +1,3 @@ +This change addresses an issue in pluggy that occured when running pytest with any pluggy tracing enabled when parametrized values contained surrogate escape characters. +Before, pluggy attempted to write trace messages using UTF-8 enconding, which fails for lone surrogates. Tracing now encodes lone surrogates with errors="replace" in order +to ensure that trace logging will not crash hook execution in the future. diff --git a/testing/test_tracer.py b/testing/test_tracer.py index c90c78f1..a336215b 100644 --- a/testing/test_tracer.py +++ b/testing/test_tracer.py @@ -75,3 +75,14 @@ def test_setprocessor(rootlogger: TagTracer) -> None: log2("seen") tags, args = l2[0] assert args == ("seen",) + + +def test_unicode_surrogate_handling(rootlogger: TagTracer) -> None: + out: list[str] = [] + rootlogger.setwriter(out.append) + log = rootlogger.get("pytest") + s = "hello \ud800 world" + log(s) + assert len(out) == 1 + assert "\ud800" not in out + assert "hello ? world" in out[0] From c07d4387b8757898fb76f660e39f65f768bd03e0 Mon Sep 17 00:00:00 2001 From: Ghanchu Date: Tue, 18 Nov 2025 22:13:34 -0500 Subject: [PATCH 3/4] added pluggy/testing to .coveragerc --- .coveragerc | 1 + testing/test_tracer.py | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/.coveragerc b/.coveragerc index b79041a4..a2340263 100644 --- a/.coveragerc +++ b/.coveragerc @@ -2,6 +2,7 @@ include = pluggy/* src/pluggy/* + pluggy/testing/* testing/* */lib/python*/site-packages/pluggy/* */pypy*/site-packages/pluggy/* diff --git a/testing/test_tracer.py b/testing/test_tracer.py index a336215b..9ed7d539 100644 --- a/testing/test_tracer.py +++ b/testing/test_tracer.py @@ -86,3 +86,27 @@ def test_unicode_surrogate_handling(rootlogger: TagTracer) -> None: assert len(out) == 1 assert "\ud800" not in out assert "hello ? world" in out[0] + + +def test_unicode_surrogate_handling_2(rootlogger: TagTracer) -> None: + out: list[str] = [] + rootlogger.setwriter(out.append) + log = rootlogger.get("pytest") + + bad = b"\xed\xa0\x80".decode("utf-8", "surrogatepass") + + log(bad) + + assert len(out) == 1 + assert "\ud800" not in out[0] + assert "?" in out[0] + + +def test_unicode_surrogate_handling_normal(rootlogger: TagTracer) -> None: + out: list[str] = [] + rootlogger.setwriter(out.append) + log = rootlogger.get("pytest") + s = "hello world" + log(s) + assert len(out) == 1 + assert "hello world" in out[0] From 4d552bc8eaaae2f10d5f6227fc2dd82cd3f2f5f7 Mon Sep 17 00:00:00 2001 From: Ghanchu Date: Wed, 19 Nov 2025 15:44:15 -0500 Subject: [PATCH 4/4] removed pluggy/testing/* from .coveragerc and removed redundant tests from test_tracer.py --- .coveragerc | 1 - testing/test_tracer.py | 10 ---------- 2 files changed, 11 deletions(-) diff --git a/.coveragerc b/.coveragerc index a2340263..b79041a4 100644 --- a/.coveragerc +++ b/.coveragerc @@ -2,7 +2,6 @@ include = pluggy/* src/pluggy/* - pluggy/testing/* testing/* */lib/python*/site-packages/pluggy/* */pypy*/site-packages/pluggy/* diff --git a/testing/test_tracer.py b/testing/test_tracer.py index 9ed7d539..49744aa0 100644 --- a/testing/test_tracer.py +++ b/testing/test_tracer.py @@ -100,13 +100,3 @@ def test_unicode_surrogate_handling_2(rootlogger: TagTracer) -> None: assert len(out) == 1 assert "\ud800" not in out[0] assert "?" in out[0] - - -def test_unicode_surrogate_handling_normal(rootlogger: TagTracer) -> None: - out: list[str] = [] - rootlogger.setwriter(out.append) - log = rootlogger.get("pytest") - s = "hello world" - log(s) - assert len(out) == 1 - assert "hello world" in out[0]