Skip to content

Commit f5bc986

Browse files
authored
fix[pytest]: Default parameter JSON encoding to use repr (#2684)
* Remove JSON encoding for pytest parameters, default to string representation of parameters * Added type hinting
1 parent 3dbde5c commit f5bc986

File tree

3 files changed

+48
-7
lines changed

3 files changed

+48
-7
lines changed

ddtrace/contrib/pytest/plugin.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
from typing import Dict
23

34
import pytest
45

@@ -123,8 +124,14 @@ def pytest_runtest_protocol(item, nextitem):
123124
# Parameterized test cases will have a `callspec` attribute attached to the pytest Item object.
124125
# Pytest docs: https://docs.pytest.org/en/6.2.x/reference.html#pytest.Function
125126
if getattr(item, "callspec", None):
126-
params = {"arguments": item.callspec.params, "metadata": {}}
127-
span.set_tag(test.PARAMETERS, json.dumps(params, default=repr))
127+
parameters = {"arguments": {}, "metadata": {}} # type: Dict[str, Dict[str, str]]
128+
for param_name, param_val in item.callspec.params.items():
129+
try:
130+
parameters["arguments"][param_name] = repr(param_val)
131+
except Exception:
132+
parameters["arguments"][param_name] = "Could not encode"
133+
log.warning("Failed to encode %r", param_name, exc_info=True)
134+
span.set_tag(test.PARAMETERS, json.dumps(parameters))
128135

129136
markers = [marker.kwargs for marker in item.iter_markers(name="dd_tags")]
130137
for tags in markers:
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
fixes:
3+
- |
4+
Fixed JSON encoding errors in the pytest plugin for parameterized tests with dictionary parameters with tuple keys.
5+
The pytest plugin now always JSON encodes the string representations of test parameters.

tests/contrib/pytest/test_pytest.py

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,10 @@ def test_1(self, item):
126126
expected_params = [1, 2, 3, 4, [1, 2, 3]]
127127
assert len(spans) == 5
128128
for i in range(len(expected_params)):
129-
extracted_params = json.loads(spans[i].meta[test.PARAMETERS])
130-
assert extracted_params == {"arguments": {"item": expected_params[i]}, "metadata": {}}
129+
assert json.loads(spans[i].meta[test.PARAMETERS]) == {
130+
"arguments": {"item": str(expected_params[i])},
131+
"metadata": {},
132+
}
131133

132134
def test_parameterize_case_complex_objects(self):
133135
"""Test parametrize case with complex objects."""
@@ -156,6 +158,7 @@ def item_param():
156158
pytest.param({"a": A("test_name", "value"), "b": [1, 2, 3]}, marks=pytest.mark.skip),
157159
pytest.param(MagicMock(value=MagicMock()), marks=pytest.mark.skip),
158160
pytest.param(circular_reference, marks=pytest.mark.skip),
161+
pytest.param({("x", "y"): 12345}, marks=pytest.mark.skip),
159162
]
160163
)
161164
class Test1(object):
@@ -165,7 +168,7 @@ def test_1(self, item):
165168
)
166169
file_name = os.path.basename(py_file.strpath)
167170
rec = self.inline_run("--ddtrace", file_name)
168-
rec.assertoutcome(skipped=6)
171+
rec.assertoutcome(skipped=7)
169172
spans = self.pop_spans()
170173

171174
# Since object will have arbitrary addresses, only need to ensure that
@@ -174,14 +177,40 @@ def test_1(self, item):
174177
"test_parameterize_case_complex_objects.A",
175178
"test_parameterize_case_complex_objects.A",
176179
"<function item_param at 0x",
177-
'"a": "<test_parameterize_case_complex_objects.A',
180+
"'a': <test_parameterize_case_complex_objects.A",
178181
"<MagicMock id=",
179182
"test_parameterize_case_complex_objects.A",
183+
"{('x', 'y'): 12345}",
180184
]
181-
assert len(spans) == 6
185+
assert len(spans) == 7
182186
for i in range(len(expected_params_contains)):
183187
assert expected_params_contains[i] in spans[i].meta[test.PARAMETERS]
184188

189+
def test_parameterize_case_encoding_error(self):
190+
"""Test parametrize case with complex objects that cannot be JSON encoded."""
191+
py_file = self.testdir.makepyfile(
192+
"""
193+
from mock import MagicMock
194+
import pytest
195+
196+
class A:
197+
def __repr__(self):
198+
raise Exception("Cannot __repr__")
199+
200+
@pytest.mark.parametrize('item',[A()])
201+
class Test1(object):
202+
def test_1(self, item):
203+
assert True
204+
"""
205+
)
206+
file_name = os.path.basename(py_file.strpath)
207+
rec = self.inline_run("--ddtrace", file_name)
208+
rec.assertoutcome(passed=1)
209+
spans = self.pop_spans()
210+
211+
assert len(spans) == 1
212+
assert json.loads(spans[0].meta[test.PARAMETERS]) == {"arguments": {"item": "Could not encode"}, "metadata": {}}
213+
185214
def test_skip(self):
186215
"""Test parametrize case."""
187216
py_file = self.testdir.makepyfile(

0 commit comments

Comments
 (0)