From 9f047aa7a92f5af9c3f7b6804f6360b9699b531d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADtor=20De=20Ara=C3=BAjo?= Date: Fri, 14 Nov 2025 10:26:20 +0000 Subject: [PATCH 1/5] Add tags generated by ddtrace to test events --- ddtestpy/internal/ddtrace/__init__.py | 11 ++--- ddtestpy/internal/test_data.py | 1 + ddtestpy/internal/utils.py | 41 +++++++++++++++++-- .../pytest/test_pytest_ddtrace_tags.py | 34 +++++++++++++++ tests/internal/test_utils.py | 18 ++++---- 5 files changed, 87 insertions(+), 18 deletions(-) create mode 100644 tests/internal/pytest/test_pytest_ddtrace_tags.py diff --git a/ddtestpy/internal/ddtrace/__init__.py b/ddtestpy/internal/ddtrace/__init__.py index b568498..8b00b69 100644 --- a/ddtestpy/internal/ddtrace/__init__.py +++ b/ddtestpy/internal/ddtrace/__init__.py @@ -7,8 +7,9 @@ import typing as t from ddtestpy.internal.utils import DDTESTOPT_ROOT_SPAN_RESOURCE +from ddtestpy.internal.utils import DDTraceTestContext +from ddtestpy.internal.utils import PlainTestContext from ddtestpy.internal.utils import TestContext -from ddtestpy.internal.utils import _gen_item_id from ddtestpy.internal.writer import TestOptWriter @@ -82,7 +83,7 @@ def trace_context(ddtrace_enabled: bool) -> t.ContextManager[TestContext]: @contextlib.contextmanager -def _ddtrace_context() -> t.Generator[TestContext, None, None]: +def _ddtrace_context() -> t.Generator[DDTraceTestContext, None, None]: import ddtrace # TODO: check if this breaks async tests. @@ -91,9 +92,9 @@ def _ddtrace_context() -> t.Generator[TestContext, None, None]: ddtrace.tracer.context_provider.activate(None) # type: ignore[attr-defined] with ddtrace.tracer.trace(DDTESTOPT_ROOT_SPAN_RESOURCE) as root_span: # type: ignore[attr-defined] - yield TestContext(trace_id=root_span.trace_id % (1 << 64), span_id=root_span.span_id % (1 << 64)) + yield DDTraceTestContext(root_span) @contextlib.contextmanager -def _plain_context() -> t.Generator[TestContext, None, None]: - yield TestContext(trace_id=_gen_item_id(), span_id=_gen_item_id()) +def _plain_context() -> t.Generator[PlainTestContext, None, None]: + yield PlainTestContext() diff --git a/ddtestpy/internal/test_data.py b/ddtestpy/internal/test_data.py index cbdc010..0294d48 100644 --- a/ddtestpy/internal/test_data.py +++ b/ddtestpy/internal/test_data.py @@ -151,6 +151,7 @@ def __init__(self, name: str, parent: Test) -> None: def set_context(self, context: TestContext) -> None: self.span_id = context.span_id self.trace_id = context.trace_id + self.set_tags(context.get_tags()) class Test(TestItem["TestSuite", "TestRun"]): diff --git a/ddtestpy/internal/utils.py b/ddtestpy/internal/utils.py index 8e29732..9a01780 100644 --- a/ddtestpy/internal/utils.py +++ b/ddtestpy/internal/utils.py @@ -1,9 +1,14 @@ -from dataclasses import dataclass +from __future__ import annotations + import random import re import typing as t +if t.TYPE_CHECKING: + from ddtrace.trace import Span + + DDTESTOPT_ROOT_SPAN_RESOURCE = "ddtestpy_root_span" @@ -21,6 +26,14 @@ def asbool(value: t.Union[str, bool, None]) -> bool: return value.lower() in ("true", "1") +def ensure_text(s: t.Any) -> str: + if isinstance(s, str): + return s + if isinstance(s, bytes): + return s.decode("utf-8", errors="ignore") + return str(s) + + _RE_URL = re.compile(r"(https?://|ssh://)[^/]*@") @@ -28,8 +41,28 @@ def _filter_sensitive_info(url: t.Optional[str]) -> t.Optional[str]: return _RE_URL.sub("\\1", url) if url is not None else None -@dataclass -class TestContext: +class TestContext(t.Protocol): span_id: int trace_id: int - __test__ = False + + def get_tags(self) -> t.Dict[str, str]: ... + + +class PlainTestContext(TestContext): + def __init__(self, span_id: t.Optional[int] = None, trace_id: t.Optional[int] = None): + self.span_id = span_id or _gen_item_id() + self.trace_id = trace_id or _gen_item_id() + + def get_tags(self) -> t.Dict[str, str]: + return {} + + +class DDTraceTestContext(TestContext): + def __init__(self, span: Span): + self.trace_id = span.trace_id % (1 << 64) + self.span_id = span.span_id % (1 << 64) + self._span = span + + def get_tags(self) -> t.Dict[str, str]: + # DEV: in ddtrace < 4.x, key names can be bytes. + return {ensure_text(k): v for k, v in self._span.get_tags().items()} diff --git a/tests/internal/pytest/test_pytest_ddtrace_tags.py b/tests/internal/pytest/test_pytest_ddtrace_tags.py new file mode 100644 index 0000000..9552c6d --- /dev/null +++ b/tests/internal/pytest/test_pytest_ddtrace_tags.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +from unittest.mock import patch + +from _pytest.pytester import Pytester +import pytest + +from tests.mocks import EventCapture +from tests.mocks import mock_api_client_settings +from tests.mocks import setup_standard_mocks + + +class TestDDTraceTags: + @pytest.mark.slow + def test_ddtrace_tags_are_reflected_in_ddtestpy_events(self, pytester: Pytester) -> None: + pytester.makepyfile( + test_foo=""" + def test_set_ddtrace_tags(): + from ddtrace import tracer + tracer.current_span().set_tag("my_custom_tag", "foo") + """ + ) + + with patch( + "ddtestpy.internal.session_manager.APIClient", + return_value=mock_api_client_settings(), + ), setup_standard_mocks(): + with EventCapture.capture() as event_capture: + result = pytester.inline_run("--ddtestpy", "--ddtestpy-with-ddtrace", "-p", "no:ddtrace", "-v", "-s") + + assert result.ret == 0 + + test_event = event_capture.event_by_test_name("test_set_ddtrace_tags") + assert test_event["content"]["meta"].get("my_custom_tag") == "foo" diff --git a/tests/internal/test_utils.py b/tests/internal/test_utils.py index 706288b..c927dd4 100644 --- a/tests/internal/test_utils.py +++ b/tests/internal/test_utils.py @@ -1,6 +1,6 @@ """Tests for ddtestpy.internal.utils module.""" -from ddtestpy.internal.utils import TestContext +from ddtestpy.internal.utils import PlainTestContext from ddtestpy.internal.utils import _gen_item_id from ddtestpy.internal.utils import asbool @@ -68,23 +68,23 @@ def test_asbool_with_arbitrary_string(self) -> None: assert asbool("hello") is False -class TestTestContext: - """Tests for TestContext dataclass.""" +class TestPlainTestContext: + """Tests for PlainTestContext dataclass.""" def test_test_context_creation(self) -> None: - """Test that TestContext can be created with span_id and trace_id.""" + """Test that PlainTestContext can be created with span_id and trace_id.""" span_id = 12345 trace_id = 67890 - context = TestContext(span_id=span_id, trace_id=trace_id) + context = PlainTestContext(span_id=span_id, trace_id=trace_id) assert context.span_id == span_id assert context.trace_id == trace_id def test_test_context_equality(self) -> None: - """Test that TestContext instances with same values are equal.""" - context1 = TestContext(span_id=123, trace_id=456) - context2 = TestContext(span_id=123, trace_id=456) - context3 = TestContext(span_id=123, trace_id=789) + """Test that PlainTestContext instances with same values are equal.""" + context1 = PlainTestContext(span_id=123, trace_id=456) + context2 = PlainTestContext(span_id=123, trace_id=456) + context3 = PlainTestContext(span_id=123, trace_id=789) assert context1 == context2 assert context1 != context3 From 6a85b68aad2153db791ed469de2bcc6598121de9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADtor=20De=20Ara=C3=BAjo?= Date: Fri, 14 Nov 2025 10:32:47 +0000 Subject: [PATCH 2/5] metrics are people too! --- ddtestpy/internal/test_data.py | 4 ++++ ddtestpy/internal/utils.py | 9 +++++++++ tests/internal/pytest/test_pytest_ddtrace_tags.py | 4 ++++ 3 files changed, 17 insertions(+) diff --git a/ddtestpy/internal/test_data.py b/ddtestpy/internal/test_data.py index 0294d48..c9ee81d 100644 --- a/ddtestpy/internal/test_data.py +++ b/ddtestpy/internal/test_data.py @@ -133,6 +133,9 @@ def get_or_create_child(self, name: str) -> t.Tuple[TChildClass, bool]: def set_tags(self, tags: t.Dict[str, str]) -> None: self.tags.update(tags) + def set_metrics(self, metrics: t.Dict[str, float]) -> None: + self.metrics.update(metrics) + class TestRun(TestItem["Test", t.NoReturn]): __test__ = False @@ -152,6 +155,7 @@ def set_context(self, context: TestContext) -> None: self.span_id = context.span_id self.trace_id = context.trace_id self.set_tags(context.get_tags()) + self.set_metrics(context.get_metrics()) class Test(TestItem["TestSuite", "TestRun"]): diff --git a/ddtestpy/internal/utils.py b/ddtestpy/internal/utils.py index 9a01780..9e2f8be 100644 --- a/ddtestpy/internal/utils.py +++ b/ddtestpy/internal/utils.py @@ -47,6 +47,8 @@ class TestContext(t.Protocol): def get_tags(self) -> t.Dict[str, str]: ... + def get_metrics(self) -> t.Dict[str, float]: ... + class PlainTestContext(TestContext): def __init__(self, span_id: t.Optional[int] = None, trace_id: t.Optional[int] = None): @@ -56,6 +58,9 @@ def __init__(self, span_id: t.Optional[int] = None, trace_id: t.Optional[int] = def get_tags(self) -> t.Dict[str, str]: return {} + def get_metrics(self) -> t.Dict[str, float]: + return {} + class DDTraceTestContext(TestContext): def __init__(self, span: Span): @@ -66,3 +71,7 @@ def __init__(self, span: Span): def get_tags(self) -> t.Dict[str, str]: # DEV: in ddtrace < 4.x, key names can be bytes. return {ensure_text(k): v for k, v in self._span.get_tags().items()} + + def get_metrics(self) -> t.Dict[str, float]: + # DEV: in ddtrace < 4.x, key names can be bytes. + return {ensure_text(k): v for k, v in self._span.get_metrics().items()} diff --git a/tests/internal/pytest/test_pytest_ddtrace_tags.py b/tests/internal/pytest/test_pytest_ddtrace_tags.py index 9552c6d..70ca073 100644 --- a/tests/internal/pytest/test_pytest_ddtrace_tags.py +++ b/tests/internal/pytest/test_pytest_ddtrace_tags.py @@ -18,6 +18,8 @@ def test_ddtrace_tags_are_reflected_in_ddtestpy_events(self, pytester: Pytester) def test_set_ddtrace_tags(): from ddtrace import tracer tracer.current_span().set_tag("my_custom_tag", "foo") + tracer.current_span().set_tag("my_other_tag", "bar") + tracer.current_span().set_metric("my_custom_metric", 42) """ ) @@ -32,3 +34,5 @@ def test_set_ddtrace_tags(): test_event = event_capture.event_by_test_name("test_set_ddtrace_tags") assert test_event["content"]["meta"].get("my_custom_tag") == "foo" + assert test_event["content"]["meta"].get("my_other_tag") == "bar" + assert test_event["content"]["metrics"].get("my_custom_metric") == 42 From c93925bd6a668c9455b1057998dff1a306d818f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADtor=20De=20Ara=C3=BAjo?= Date: Fri, 14 Nov 2025 10:35:57 +0000 Subject: [PATCH 3/5] remove useless test --- tests/internal/test_utils.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/tests/internal/test_utils.py b/tests/internal/test_utils.py index c927dd4..1b649b2 100644 --- a/tests/internal/test_utils.py +++ b/tests/internal/test_utils.py @@ -79,12 +79,5 @@ def test_test_context_creation(self) -> None: assert context.span_id == span_id assert context.trace_id == trace_id - - def test_test_context_equality(self) -> None: - """Test that PlainTestContext instances with same values are equal.""" - context1 = PlainTestContext(span_id=123, trace_id=456) - context2 = PlainTestContext(span_id=123, trace_id=456) - context3 = PlainTestContext(span_id=123, trace_id=789) - - assert context1 == context2 - assert context1 != context3 + assert context.get_tags() == {} + assert context.get_metrics() == {} From 4d7a7021b37c46e5dff30b2a2e237c5b7e3b1f68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADtor=20De=20Ara=C3=BAjo?= Date: Fri, 14 Nov 2025 10:45:32 +0000 Subject: [PATCH 4/5] __test__ --- ddtestpy/internal/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ddtestpy/internal/utils.py b/ddtestpy/internal/utils.py index 9e2f8be..3c2afde 100644 --- a/ddtestpy/internal/utils.py +++ b/ddtestpy/internal/utils.py @@ -44,6 +44,7 @@ def _filter_sensitive_info(url: t.Optional[str]) -> t.Optional[str]: class TestContext(t.Protocol): span_id: int trace_id: int + __test__ = False def get_tags(self) -> t.Dict[str, str]: ... From 8f1f06a01cb07b5ca47c3d8374dcce78459bb647 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADtor=20De=20Ara=C3=BAjo?= Date: Fri, 14 Nov 2025 10:54:48 +0000 Subject: [PATCH 5/5] undo --- ddtestpy/internal/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ddtestpy/internal/utils.py b/ddtestpy/internal/utils.py index 3c2afde..9e2f8be 100644 --- a/ddtestpy/internal/utils.py +++ b/ddtestpy/internal/utils.py @@ -44,7 +44,6 @@ def _filter_sensitive_info(url: t.Optional[str]) -> t.Optional[str]: class TestContext(t.Protocol): span_id: int trace_id: int - __test__ = False def get_tags(self) -> t.Dict[str, str]: ...