Skip to content

Commit 8dce0d2

Browse files
Add tags generated by ddtrace to test events (#66)
1 parent 6fc8468 commit 8dce0d2

5 files changed

Lines changed: 102 additions & 23 deletions

File tree

ddtestpy/internal/ddtrace/__init__.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77
import typing as t
88

99
from ddtestpy.internal.utils import DDTESTOPT_ROOT_SPAN_RESOURCE
10+
from ddtestpy.internal.utils import DDTraceTestContext
11+
from ddtestpy.internal.utils import PlainTestContext
1012
from ddtestpy.internal.utils import TestContext
11-
from ddtestpy.internal.utils import _gen_item_id
1213
from ddtestpy.internal.writer import TestOptWriter
1314

1415

@@ -82,7 +83,7 @@ def trace_context(ddtrace_enabled: bool) -> t.ContextManager[TestContext]:
8283

8384

8485
@contextlib.contextmanager
85-
def _ddtrace_context() -> t.Generator[TestContext, None, None]:
86+
def _ddtrace_context() -> t.Generator[DDTraceTestContext, None, None]:
8687
import ddtrace
8788

8889
# TODO: check if this breaks async tests.
@@ -91,9 +92,9 @@ def _ddtrace_context() -> t.Generator[TestContext, None, None]:
9192
ddtrace.tracer.context_provider.activate(None) # type: ignore[attr-defined]
9293

9394
with ddtrace.tracer.trace(DDTESTOPT_ROOT_SPAN_RESOURCE) as root_span: # type: ignore[attr-defined]
94-
yield TestContext(trace_id=root_span.trace_id % (1 << 64), span_id=root_span.span_id % (1 << 64))
95+
yield DDTraceTestContext(root_span)
9596

9697

9798
@contextlib.contextmanager
98-
def _plain_context() -> t.Generator[TestContext, None, None]:
99-
yield TestContext(trace_id=_gen_item_id(), span_id=_gen_item_id())
99+
def _plain_context() -> t.Generator[PlainTestContext, None, None]:
100+
yield PlainTestContext()

ddtestpy/internal/test_data.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,9 @@ def get_or_create_child(self, name: str) -> t.Tuple[TChildClass, bool]:
133133
def set_tags(self, tags: t.Dict[str, str]) -> None:
134134
self.tags.update(tags)
135135

136+
def set_metrics(self, metrics: t.Dict[str, float]) -> None:
137+
self.metrics.update(metrics)
138+
136139

137140
class TestRun(TestItem["Test", t.NoReturn]):
138141
__test__ = False
@@ -151,6 +154,8 @@ def __init__(self, name: str, parent: Test) -> None:
151154
def set_context(self, context: TestContext) -> None:
152155
self.span_id = context.span_id
153156
self.trace_id = context.trace_id
157+
self.set_tags(context.get_tags())
158+
self.set_metrics(context.get_metrics())
154159

155160

156161
class Test(TestItem["TestSuite", "TestRun"]):

ddtestpy/internal/utils.py

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1-
from dataclasses import dataclass
1+
from __future__ import annotations
2+
23
import random
34
import re
45
import typing as t
56

67

8+
if t.TYPE_CHECKING:
9+
from ddtrace.trace import Span
10+
11+
712
DDTESTOPT_ROOT_SPAN_RESOURCE = "ddtestpy_root_span"
813

914

@@ -21,15 +26,52 @@ def asbool(value: t.Union[str, bool, None]) -> bool:
2126
return value.lower() in ("true", "1")
2227

2328

29+
def ensure_text(s: t.Any) -> str:
30+
if isinstance(s, str):
31+
return s
32+
if isinstance(s, bytes):
33+
return s.decode("utf-8", errors="ignore")
34+
return str(s)
35+
36+
2437
_RE_URL = re.compile(r"(https?://|ssh://)[^/]*@")
2538

2639

2740
def _filter_sensitive_info(url: t.Optional[str]) -> t.Optional[str]:
2841
return _RE_URL.sub("\\1", url) if url is not None else None
2942

3043

31-
@dataclass
32-
class TestContext:
44+
class TestContext(t.Protocol):
3345
span_id: int
3446
trace_id: int
35-
__test__ = False
47+
48+
def get_tags(self) -> t.Dict[str, str]: ...
49+
50+
def get_metrics(self) -> t.Dict[str, float]: ...
51+
52+
53+
class PlainTestContext(TestContext):
54+
def __init__(self, span_id: t.Optional[int] = None, trace_id: t.Optional[int] = None):
55+
self.span_id = span_id or _gen_item_id()
56+
self.trace_id = trace_id or _gen_item_id()
57+
58+
def get_tags(self) -> t.Dict[str, str]:
59+
return {}
60+
61+
def get_metrics(self) -> t.Dict[str, float]:
62+
return {}
63+
64+
65+
class DDTraceTestContext(TestContext):
66+
def __init__(self, span: Span):
67+
self.trace_id = span.trace_id % (1 << 64)
68+
self.span_id = span.span_id % (1 << 64)
69+
self._span = span
70+
71+
def get_tags(self) -> t.Dict[str, str]:
72+
# DEV: in ddtrace < 4.x, key names can be bytes.
73+
return {ensure_text(k): v for k, v in self._span.get_tags().items()}
74+
75+
def get_metrics(self) -> t.Dict[str, float]:
76+
# DEV: in ddtrace < 4.x, key names can be bytes.
77+
return {ensure_text(k): v for k, v in self._span.get_metrics().items()}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from __future__ import annotations
2+
3+
from unittest.mock import patch
4+
5+
from _pytest.pytester import Pytester
6+
import pytest
7+
8+
from tests.mocks import EventCapture
9+
from tests.mocks import mock_api_client_settings
10+
from tests.mocks import setup_standard_mocks
11+
12+
13+
class TestDDTraceTags:
14+
@pytest.mark.slow
15+
def test_ddtrace_tags_are_reflected_in_ddtestpy_events(self, pytester: Pytester) -> None:
16+
pytester.makepyfile(
17+
test_foo="""
18+
def test_set_ddtrace_tags():
19+
from ddtrace import tracer
20+
tracer.current_span().set_tag("my_custom_tag", "foo")
21+
tracer.current_span().set_tag("my_other_tag", "bar")
22+
tracer.current_span().set_metric("my_custom_metric", 42)
23+
"""
24+
)
25+
26+
with patch(
27+
"ddtestpy.internal.session_manager.APIClient",
28+
return_value=mock_api_client_settings(),
29+
), setup_standard_mocks():
30+
with EventCapture.capture() as event_capture:
31+
result = pytester.inline_run("--ddtestpy", "--ddtestpy-with-ddtrace", "-p", "no:ddtrace", "-v", "-s")
32+
33+
assert result.ret == 0
34+
35+
test_event = event_capture.event_by_test_name("test_set_ddtrace_tags")
36+
assert test_event["content"]["meta"].get("my_custom_tag") == "foo"
37+
assert test_event["content"]["meta"].get("my_other_tag") == "bar"
38+
assert test_event["content"]["metrics"].get("my_custom_metric") == 42

tests/internal/test_utils.py

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Tests for ddtestpy.internal.utils module."""
22

3-
from ddtestpy.internal.utils import TestContext
3+
from ddtestpy.internal.utils import PlainTestContext
44
from ddtestpy.internal.utils import _gen_item_id
55
from ddtestpy.internal.utils import asbool
66

@@ -68,23 +68,16 @@ def test_asbool_with_arbitrary_string(self) -> None:
6868
assert asbool("hello") is False
6969

7070

71-
class TestTestContext:
72-
"""Tests for TestContext dataclass."""
71+
class TestPlainTestContext:
72+
"""Tests for PlainTestContext dataclass."""
7373

7474
def test_test_context_creation(self) -> None:
75-
"""Test that TestContext can be created with span_id and trace_id."""
75+
"""Test that PlainTestContext can be created with span_id and trace_id."""
7676
span_id = 12345
7777
trace_id = 67890
78-
context = TestContext(span_id=span_id, trace_id=trace_id)
78+
context = PlainTestContext(span_id=span_id, trace_id=trace_id)
7979

8080
assert context.span_id == span_id
8181
assert context.trace_id == trace_id
82-
83-
def test_test_context_equality(self) -> None:
84-
"""Test that TestContext instances with same values are equal."""
85-
context1 = TestContext(span_id=123, trace_id=456)
86-
context2 = TestContext(span_id=123, trace_id=456)
87-
context3 = TestContext(span_id=123, trace_id=789)
88-
89-
assert context1 == context2
90-
assert context1 != context3
82+
assert context.get_tags() == {}
83+
assert context.get_metrics() == {}

0 commit comments

Comments
 (0)