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."""