diff --git a/src/pytest_html/basereport.py b/src/pytest_html/basereport.py index cfcc74b2..e613db85 100644 --- a/src/pytest_html/basereport.py +++ b/src/pytest_html/basereport.py @@ -111,7 +111,7 @@ def _process_extras(self, report, test_id): for extra_index, extra in enumerate(report_extras): content = extra["content"] asset_name = self._asset_filename( - test_id.encode("utf-8").decode("unicode_escape"), + _unescape_test_id(test_id), extra_index, test_index, extra["extension"], @@ -355,6 +355,16 @@ def _process_outcome(report): return report.outcome.capitalize() +def _unescape_test_id(test_id): + # Restore the non-ASCII characters pytest escapes in test ids (e.g. "\u00e9"). + # Unescaped non-ASCII characters (e.g. in function names) are preserved, and + # ids that aren't valid escape sequences (e.g. "\N") are returned as-is. + try: + return test_id.encode("ascii", "backslashreplace").decode("unicode_escape") + except UnicodeDecodeError: + return test_id + + def _process_links(links): a_tag = '{name}' return "".join([a_tag.format_map(link) for link in links]) diff --git a/testing/test_unit.py b/testing/test_unit.py index 1ea945ea..c7b269ae 100644 --- a/testing/test_unit.py +++ b/testing/test_unit.py @@ -146,3 +146,68 @@ def test_custom_css_selfcontained(pytester, css_file_path, expandvar): with open(pytester.path / "report.html") as f: html = f.read() assert_that(html).contains("* " + str(css_file_path)).contains("* two.css") + + +@pytest.fixture +def text_extra(pytester): + pytester.makeconftest( + """ + import pytest + + @pytest.hookimpl(hookwrapper=True) + def pytest_runtest_makereport(item, call): + outcome = yield + report = outcome.get_result() + if report.when == "call": + from pytest_html import extras + report.extras = [extras.text("content")] + """ + ) + + +@pytest.mark.parametrize( + "test_file, asset_name", + [ + ("def test_pass(): pass", "test_pass_0_0.txt"), + ("def test_pässed(): pass", "test_pässed_0_0.txt"), + ( + """ + import pytest + @pytest.mark.parametrize("value", ["ä"]) + def test_pass(value): pass + """, + "test_pass_ä__0_0.txt", + ), + ], + ids=["ascii", "non-ascii-name", "non-ascii-param"], +) +def test_extra_asset_name(pytester, text_extra, test_file, asset_name): + pytester.makepyfile(test_extra_asset_name=test_file) + result = run(pytester) + result.assert_outcomes(passed=1) + + path = pytester.path.joinpath("assets", f"test_extra_asset_name.py__{asset_name}") + assert_that(str(path)).exists() + + +def test_extra_asset_name_invalid_escape_sequence(pytester, text_extra): + pytester.makeini( + """ + [pytest] + disable_test_id_escaping_and_forfeit_all_rights_to_community_support = true + """ + ) + pytester.makepyfile( + test_extra_asset_name=r""" + import pytest + @pytest.mark.parametrize("path", ["C:\\New Folder"]) + def test_pass(path): pass + """ + ) + result = run(pytester) + result.assert_outcomes(passed=1) + + path = pytester.path.joinpath( + "assets", "test_extra_asset_name.py__test_pass_C__New_Folder__0_0.txt" + ) + assert_that(str(path)).exists()