From 3d92d5a6595a6f5df5849e5355a8dbd6bda9843d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 18 Nov 2018 14:32:32 -0800 Subject: [PATCH 1/5] Make sure parametrize ids are printable --- src/_pytest/compat.py | 21 ++++++++++++++++----- testing/python/metafunc.py | 22 +++++++++++++++++----- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index ead9ffd8d..87d4d51c1 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -182,6 +182,15 @@ def get_default_arg_names(function): ) +_non_printable_ascii_translate_table = { + i: u"\\x{:02x}".format(i) for i in range(128) if i not in range(32, 127) +} + + +def _translate_non_printable(s): + return s.translate(_non_printable_ascii_translate_table) + + if _PY3: STRING_TYPES = bytes, str UNICODE_TYPES = six.text_type @@ -221,9 +230,10 @@ if _PY3: """ if isinstance(val, bytes): - return _bytes_to_ascii(val) + ret = _bytes_to_ascii(val) else: - return val.encode("unicode_escape").decode("ascii") + ret = val.encode("unicode_escape").decode("ascii") + return _translate_non_printable(ret) else: @@ -241,11 +251,12 @@ else: """ if isinstance(val, bytes): try: - return val.encode("ascii") + ret = val.decode("ascii") except UnicodeDecodeError: - return val.encode("string-escape") + ret = val.encode("string-escape").decode("ascii") else: - return val.encode("unicode-escape") + ret = val.encode("unicode-escape").decode("ascii") + return _translate_non_printable(ret) class _PytestWrapper(object): diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 1a9cbf408..605814e65 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -5,6 +5,7 @@ import textwrap import attr import hypothesis +import six from hypothesis import strategies import pytest @@ -262,11 +263,8 @@ class TestMetafunc(object): from _pytest.python import _idval escaped = _idval(value, "a", 6, None, item=None, config=None) - assert isinstance(escaped, str) - if PY3: - escaped.encode("ascii") - else: - escaped.decode("ascii") + assert isinstance(escaped, six.text_type) + escaped.encode("ascii") def test_unicode_idval(self): """This tests that Unicode strings outside the ASCII character set get @@ -382,6 +380,20 @@ class TestMetafunc(object): "\\xc3\\xb4-other", ] + def test_idmaker_non_printable_characters(self): + from _pytest.python import idmaker + + result = idmaker( + ("s", "n"), + [ + pytest.param("\x00", 1), + pytest.param("\x05", 2), + pytest.param(b"\x00", 3), + pytest.param(b"\x05", 4), + ], + ) + assert result == ["\\x00-1", "\\x05-2", "\\x00-3", "\\x05-4"] + def test_idmaker_enum(self): from _pytest.python import idmaker From 8395b9e25dd968124c239c303af4088aa6a348b9 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 18 Nov 2018 14:16:13 -0800 Subject: [PATCH 2/5] Require id=... to be a string This was documented before, but never enforced. Passing non-strings could have strange side-effects and enforcing a string simplifies other implementation. --- src/_pytest/mark/structures.py | 11 +++++++---- testing/test_mark.py | 32 ++++++++++++++++++++++++-------- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index b8fa011d1..f1892aa3f 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -5,6 +5,7 @@ from functools import reduce from operator import attrgetter import attr +import six from six.moves import map from ..compat import getfslineno @@ -70,10 +71,12 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")): else: assert isinstance(marks, (tuple, list, set)) - def param_extract_id(id=None): - return id - - id_ = param_extract_id(**kw) + id_ = kw.pop("id", None) + if id_ is not None: + if not isinstance(id_, six.string_types): + raise TypeError( + "Expected id to be a string, got {}: {!r}".format(type(id_), id_) + ) return cls(values, marks, id_) @classmethod diff --git a/testing/test_mark.py b/testing/test_mark.py index 1f50045c5..80979d7ee 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -5,20 +5,21 @@ from __future__ import print_function import os import sys +import six + +import pytest +from _pytest.mark import EMPTY_PARAMETERSET_OPTION +from _pytest.mark import MarkGenerator as Mark +from _pytest.mark import ParameterSet +from _pytest.mark import transfer_markers +from _pytest.nodes import Collector +from _pytest.nodes import Node from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG try: import mock except ImportError: import unittest.mock as mock -import pytest -from _pytest.mark import ( - MarkGenerator as Mark, - ParameterSet, - transfer_markers, - EMPTY_PARAMETERSET_OPTION, -) -from _pytest.nodes import Node, Collector ignore_markinfo = pytest.mark.filterwarnings( "ignore:MarkInfo objects:pytest.RemovedInPytest4Warning" @@ -1252,3 +1253,18 @@ def test_markers_from_parametrize(testdir): result = testdir.runpytest(SHOW_PYTEST_WARNINGS_ARG) result.assert_outcomes(passed=4) + + +def test_pytest_param_id_requires_string(): + with pytest.raises(TypeError) as excinfo: + pytest.param(id=True) + msg, = excinfo.value.args + if six.PY2: + assert msg == "Expected id to be a string, got : True" + else: + assert msg == "Expected id to be a string, got : True" + + +@pytest.mark.parametrize("s", (None, "hello world")) +def test_pytest_param_id_allows_none_or_string(s): + assert pytest.param(id=s) From 9ca0ab6e2be28a44dca3a52080a4bcd407ca0b7b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 18 Nov 2018 14:39:17 -0800 Subject: [PATCH 3/5] Ensure printable manually-specified param(id=...) --- src/_pytest/mark/structures.py | 2 ++ testing/python/metafunc.py | 12 ++++++++++++ 2 files changed, 14 insertions(+) diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index f1892aa3f..14a684745 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -8,6 +8,7 @@ import attr import six from six.moves import map +from ..compat import ascii_escaped from ..compat import getfslineno from ..compat import MappingMixin from ..compat import NOTSET @@ -77,6 +78,7 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")): raise TypeError( "Expected id to be a string, got {}: {!r}".format(type(id_), id_) ) + id_ = ascii_escaped(id_) return cls(values, marks, id_) @classmethod diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 605814e65..ef6993d94 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -394,6 +394,18 @@ class TestMetafunc(object): ) assert result == ["\\x00-1", "\\x05-2", "\\x00-3", "\\x05-4"] + def test_idmaker_manual_ids_must_be_printable(self): + from _pytest.python import idmaker + + result = idmaker( + ("s",), + [ + pytest.param("x00", id="hello \x00"), + pytest.param("x05", id="hello \x05"), + ], + ) + assert result == ["hello \\x00", "hello \\x05"] + def test_idmaker_enum(self): from _pytest.python import idmaker From 9a1e518cc3bedbcfc3eefa1576cc9a627f23aca6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 18 Nov 2018 14:40:55 -0800 Subject: [PATCH 4/5] Add changelog entry for printable node ids --- changelog/4397.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/4397.bugfix.rst diff --git a/changelog/4397.bugfix.rst b/changelog/4397.bugfix.rst new file mode 100644 index 000000000..d1a5bd3ba --- /dev/null +++ b/changelog/4397.bugfix.rst @@ -0,0 +1 @@ +Ensure that node ids are printable. From 95c6d591f7f5eff10f98e7768823c220ac830435 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 18 Nov 2018 15:12:43 -0800 Subject: [PATCH 5/5] Properly escape \r \n \t bytes --- src/_pytest/compat.py | 3 +++ testing/python/metafunc.py | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 87d4d51c1..1857f51a8 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -185,6 +185,9 @@ def get_default_arg_names(function): _non_printable_ascii_translate_table = { i: u"\\x{:02x}".format(i) for i in range(128) if i not in range(32, 127) } +_non_printable_ascii_translate_table.update( + {ord("\t"): u"\\t", ord("\r"): u"\\r", ord("\n"): u"\\n"} +) def _translate_non_printable(s): diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index ef6993d94..0d5b6037f 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -390,9 +390,11 @@ class TestMetafunc(object): pytest.param("\x05", 2), pytest.param(b"\x00", 3), pytest.param(b"\x05", 4), + pytest.param("\t", 5), + pytest.param(b"\t", 6), ], ) - assert result == ["\\x00-1", "\\x05-2", "\\x00-3", "\\x05-4"] + assert result == ["\\x00-1", "\\x05-2", "\\x00-3", "\\x05-4", "\\t-5", "\\t-6"] def test_idmaker_manual_ids_must_be_printable(self): from _pytest.python import idmaker