diff --git a/src/pytest_reportlog/plugin.py b/src/pytest_reportlog/plugin.py index d44d56f..0a58d4c 100644 --- a/src/pytest_reportlog/plugin.py +++ b/src/pytest_reportlog/plugin.py @@ -139,12 +139,23 @@ def pytest_terminal_summary(self, terminalreporter): def cleanup_unserializable(d: Dict[str, Any]) -> Dict[str, Any]: - """Return new dict with entries that are not json serializable by their str().""" - result = {} - for k, v in d.items(): + """Return new dict with values converted to JSON-safe representations.""" + + def to_jsonable(value: Any) -> Any: try: - json.dumps({k: v}) - except TypeError: - v = str(v) - result[k] = v - return result + json.dumps(value) + except (TypeError, ValueError): + pass + else: + return value + + if isinstance(value, dict): + return {str(k): to_jsonable(v) for k, v in value.items()} + if isinstance(value, (list, tuple)): + return [to_jsonable(v) for v in value] + if isinstance(value, set): + return [to_jsonable(v) for v in sorted(value, key=str)] + + return str(value) + + return {str(k): to_jsonable(v) for k, v in d.items()} diff --git a/tests/test_reportlog.py b/tests/test_reportlog.py index 032df8b..8dec31a 100644 --- a/tests/test_reportlog.py +++ b/tests/test_reportlog.py @@ -182,8 +182,23 @@ def __str__(self): assert new == {"x": 1, "c": "C instance", "y": ["a", "b"]} +def test_cleanup_unserializable_does_not_stringify_entire_user_properties(): + """ + Regression test for #12. + + If a single user property value is not JSON-serializable (e.g. a set), we + should only sanitize that value instead of stringifying the entire + user_properties payload. + """ + data = {"user_properties": [("hello", {"world", "mars"})]} + new = cleanup_unserializable(data) + + assert new == {"user_properties": [["hello", ["mars", "world"]]]} + + def test_subtest(pytester, tmp_path): """Regression test for #90.""" + pytest.importorskip("pytest_subtests") pytester.makepyfile(""" def test_foo(subtests): with subtests.test():