From 9731e30e7b834b34941da0a56f4f4905dfa9bc14 Mon Sep 17 00:00:00 2001 From: Nigel Tan Date: Sat, 17 Oct 2026 20:48:31 +0800 Subject: [PATCH] Escape test names that the environment cannot encode A Unicode test name can stop setup and teardown when pytest sets PYTEST_CURRENT_TEST under an ASCII locale. Retry a failed environment assignment with readable backslash escapes using the filesystem encoding. Another process under the same locale can read the value. Add subprocess coverage for ASCII and UTF8 modes through all test phases and environment cleanup. Fixes #7786 Co-authored-by: Codex --- AUTHORS | 1 + changelog/7786.bugfix.rst | 2 ++ src/_pytest/runner.py | 8 +++++- testing/test_runner.py | 57 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 changelog/7786.bugfix.rst diff --git a/AUTHORS b/AUTHORS index bd1e27cb37d..8750e457966 100644 --- a/AUTHORS +++ b/AUTHORS @@ -367,6 +367,7 @@ Niclas Olofsson Nicolas Delaby Nicolas Simonds Nico Vidal +Nigel Tan Nikesh Chavhan Nikolay Kondratyev Nipunn Koorapati diff --git a/changelog/7786.bugfix.rst b/changelog/7786.bugfix.rst new file mode 100644 index 00000000000..61b335020aa --- /dev/null +++ b/changelog/7786.bugfix.rst @@ -0,0 +1,2 @@ +Tests with Unicode names no longer fail when the environment encoding cannot represent their names. +``PYTEST_CURRENT_TEST`` uses readable backslash escapes for those names. diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 27c5739845a..fac2311f720 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -212,7 +212,13 @@ def _update_current_test_var( value = f"{item.nodeid} ({when})" # don't allow null bytes on environment variables (see #2644, #2957) value = value.replace("\x00", "(null)") - os.environ[var_name] = value + try: + os.environ[var_name] = value + except UnicodeEncodeError: + encoding = sys.getfilesystemencoding() + os.environ[var_name] = value.encode(encoding, "backslashreplace").decode( + encoding + ) else: os.environ.pop(var_name) diff --git a/testing/test_runner.py b/testing/test_runner.py index b5c3839c79e..bc4a20efcae 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -1090,6 +1090,63 @@ def test(fix): assert "PYTEST_CURRENT_TEST" not in os.environ +@pytest.mark.parametrize( + ("utf8_mode", "expected_name"), + [("0", r"test_caf\xe9_\u4e2d\u6587"), ("1", "test_café_中文")], +) +def test_current_test_env_var_non_ascii( + pytester: Pytester, + monkeypatch: MonkeyPatch, + utf8_mode: str, + expected_name: str, +) -> None: + monkeypatch.setenv("LC_ALL", "C") + monkeypatch.setenv("PYTHONCOERCECLOCALE", "0") + monkeypatch.setenv("PYTHONUTF8", utf8_mode) + expected_encoding = "utf-8" if utf8_mode == "1" else "ascii" + probe = pytester.runpython_c("import sys; print(sys.getfilesystemencoding())") + assert probe.ret == ExitCode.OK + if probe.stdout.str().strip() != expected_encoding: + pytest.skip(f"{expected_encoding} filesystem encoding is unavailable") + expected_nodeid = "test_current_test_env_var_non_ascii.py::" + expected_name + pytester.makeconftest( + """ + import os + + def pytest_unconfigure(): + assert "PYTEST_CURRENT_TEST" not in os.environ + """ + ) + pytester.makepyfile( + f""" + import os + import subprocess + import sys + import pytest + + expected_nodeid = {expected_nodeid!r} + + @pytest.fixture + def check_current_test(): + assert os.environ["PYTEST_CURRENT_TEST"] == expected_nodeid + " (setup)" + yield + assert os.environ["PYTEST_CURRENT_TEST"] == expected_nodeid + " (teardown)" + + def test_café_中文(check_current_test): + assert sys.getfilesystemencoding() == {expected_encoding!r} + assert os.environ["PYTEST_CURRENT_TEST"] == expected_nodeid + " (call)" + inherited = subprocess.check_output( + [sys.executable, "-c", "import os\\nprint(os.environ['PYTEST_CURRENT_TEST'])"], + encoding=sys.getfilesystemencoding(), + ) + assert inherited.strip() == expected_nodeid + " (call)" + """ + ) + result = pytester.runpytest_subprocess() + result.assert_outcomes(passed=1, warnings=0) + assert result.ret == ExitCode.OK + + class TestReportContents: """Test user-level API of ``TestReport`` objects."""