Skip to content

Commit dacfd1d

Browse files
chore(ci_visibility): add pytest-benchmark support to new plugin
1 parent fdd8fed commit dacfd1d

File tree

4 files changed

+93
-10
lines changed

4 files changed

+93
-10
lines changed
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
from dataclasses import dataclass
2+
import typing as t
3+
4+
import pytest
5+
6+
7+
BENCHMARK_INFO_TAG = "benchmark.duration.info"
8+
9+
PYTEST_BENCHMARK_KEYS_TO_DATADOG_TAGS = {
10+
"outliers": "benchmark.duration.statistics.outliers",
11+
}
12+
13+
PYTEST_BENCHMARK_KEYS_TO_DATADOG_METRICS = {
14+
"hd15iqr": "benchmark.duration.statistics.hd15iqr",
15+
"iqr": "benchmark.duration.statistics.iqr",
16+
"iqr_outliers": "benchmark.duration.statistics.iqr_outliers",
17+
"ld15iqr": "benchmark.duration.statistics.ld15iqr",
18+
"max": "benchmark.duration.statistics.max",
19+
"mean": "benchmark.duration.statistics.mean",
20+
"median": "benchmark.duration.statistics.median",
21+
"min": "benchmark.duration.statistics.min",
22+
"ops": "benchmark.duration.statistics.ops",
23+
"q1": "benchmark.duration.statistics.q1",
24+
"q3": "benchmark.duration.statistics.q3",
25+
"rounds": "benchmark.duration.statistics.n",
26+
"stddev": "benchmark.duration.statistics.std_dev",
27+
"stddev_outliers": "benchmark.duration.statistics.std_dev_outliers",
28+
"total": "benchmark.duration.statistics.total",
29+
}
30+
31+
32+
@dataclass
33+
class BenchmarkData:
34+
tags: t.Dict[str, str]
35+
metrics: t.Dict[str, float]
36+
37+
38+
def get_benchmark_tags_and_metrics(item: pytest.Item) -> t.Optional[BenchmarkData]:
39+
if not item.config.pluginmanager.hasplugin("benchmark"):
40+
return None
41+
42+
funcargs = getattr(item, "funcargs", None)
43+
if not funcargs:
44+
return None
45+
46+
benchmark_fixture = item.funcargs.get("benchmark")
47+
if not benchmark_fixture or not benchmark_fixture.stats:
48+
return None
49+
50+
stats = item.funcargs.get("benchmark").stats.stats
51+
52+
data = BenchmarkData(tags={}, metrics={})
53+
data.tags[BENCHMARK_INFO_TAG] = "Time"
54+
55+
for stats_attr, tag_name in PYTEST_BENCHMARK_KEYS_TO_DATADOG_TAGS.items():
56+
value = getattr(stats, stats_attr, None)
57+
if value is not None:
58+
data.tags[tag_name] = value
59+
60+
for stats_attr, metric_name in PYTEST_BENCHMARK_KEYS_TO_DATADOG_METRICS.items():
61+
value = getattr(stats, stats_attr, None)
62+
if value is not None:
63+
data.metrics[metric_name] = value
64+
65+
return data

ddtrace/testing/internal/pytest/plugin.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
from ddtrace.testing.internal.git import get_workspace_path
1919
from ddtrace.testing.internal.logging import catch_and_log_exceptions
2020
from ddtrace.testing.internal.logging import setup_logging
21+
from ddtrace.testing.internal.pytest.benchmark import BenchmarkData
22+
from ddtrace.testing.internal.pytest.benchmark import get_benchmark_tags_and_metrics
2123
from ddtrace.testing.internal.retry_handlers import RetryHandler
2224
from ddtrace.testing.internal.session_manager import SessionManager
2325
from ddtrace.testing.internal.telemetry import TelemetryAPI
@@ -139,6 +141,7 @@ def __init__(self, session_manager: SessionManager) -> None:
139141
self.enable_ddtrace = False
140142
self.reports_by_nodeid: t.Dict[str, _ReportGroup] = defaultdict(lambda: {})
141143
self.excinfo_by_report: t.Dict[pytest.TestReport, t.Optional[pytest.ExceptionInfo[t.Any]]] = {}
144+
self.benchmark_data_by_nodeid: t.Dict[str, BenchmarkData] = {}
142145
self.tests_by_nodeid: t.Dict[str, Test] = {}
143146
self.is_xdist_worker = False
144147

@@ -282,10 +285,7 @@ def pytest_runtest_protocol_wrapper(
282285
)
283286
test_run = test.make_test_run()
284287
test_run.start(start_ns=test.start_ns)
285-
status, tags = self._get_test_outcome(item.nodeid)
286-
test_run.set_status(status)
287-
test_run.set_tags(tags)
288-
test_run.set_context(context)
288+
self._set_test_run_data(test_run, item, context)
289289
test_run.finish()
290290
test.set_status(test_run.get_status()) # TODO: this should be automatic?
291291
self.manager.writer.put_item(test_run)
@@ -323,10 +323,7 @@ def _do_one_test_run(
323323
TelemetryAPI.get().record_test_created(test_framework=TEST_FRAMEWORK, test_run=test_run)
324324

325325
reports = _make_reports_dict(runtestprotocol(item, nextitem=nextitem, log=False))
326-
status, tags = self._get_test_outcome(item.nodeid)
327-
test_run.set_status(status)
328-
test_run.set_tags(tags)
329-
test_run.set_context(context)
326+
self._set_test_run_data(test_run, item, context)
330327

331328
TelemetryAPI.get().record_test_finished(
332329
test_framework=TEST_FRAMEWORK,
@@ -354,6 +351,17 @@ def _do_test_runs(self, item: pytest.Item, nextitem: t.Optional[pytest.Item]) ->
354351
test.set_status(test_run.get_status()) # TODO: this should be automatic?
355352
self.manager.writer.put_item(test_run)
356353

354+
def _set_test_run_data(self, test_run: TestRun, item: pytest.Item, context: TestContext) -> None:
355+
status, tags = self._get_test_outcome(item.nodeid)
356+
test_run.set_status(status)
357+
test_run.set_tags(tags)
358+
test_run.set_context(context)
359+
360+
if benchmark_data := self.benchmark_data_by_nodeid.pop(item.nodeid):
361+
test_run.set_tags(benchmark_data.tags)
362+
test_run.set_metrics(benchmark_data.metrics)
363+
test_run.mark_benchmark()
364+
357365
def _do_retries(
358366
self,
359367
item: pytest.Item,
@@ -526,6 +534,11 @@ def pytest_runtest_makereport(
526534
self.reports_by_nodeid[item.nodeid][call.when] = report
527535
self.excinfo_by_report[report] = call.excinfo
528536

537+
if call.when == TestPhase.TEARDOWN:
538+
# We need to extract pytest-benchmark data _before_ the fixture teardown.
539+
if benchmark_data := get_benchmark_tags_and_metrics(item):
540+
self.benchmark_data_by_nodeid[item.nodeid] = benchmark_data
541+
529542
def pytest_report_teststatus(self, report: pytest.TestReport) -> t.Optional[_ReportTestStatus]:
530543
if retry_outcome := _get_user_property(report, "dd_retry_outcome"):
531544
retry_reason = _get_user_property(report, "dd_retry_reason")

ddtrace/testing/internal/test_data.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,8 @@ def __init__(self, name: str, parent: Test) -> None:
154154
self.module = self.suite.parent
155155
self.session = self.module.parent
156156

157+
self.tags[TestTag.TEST_TYPE] = "test"
158+
157159
def __str__(self) -> str:
158160
return f"{self.test} #{self.attempt_number}"
159161

@@ -169,8 +171,11 @@ def is_retry(self) -> bool:
169171
def has_failed_all_retries(self) -> bool:
170172
return self.tags.get(TestTag.HAS_FAILED_ALL_RETRIES) == TAG_TRUE
171173

174+
def mark_benchmark(self) -> None:
175+
self.tags[TestTag.TEST_TYPE] = "benchmark"
176+
172177
def is_benchmark(self) -> bool:
173-
return False # TODO: change when benchmark tests are implemented
178+
return self.tags.get(TestTag.TEST_TYPE) == "benchmark"
174179

175180
# Selenium / RUM functionality. These tags are only available after the test has finished and ddtrace span tags have
176181
# been copied over to the test run object.
@@ -348,6 +353,7 @@ class TestTag:
348353

349354
SKIP_REASON = "test.skip_reason"
350355

356+
TEST_TYPE = "test.type"
351357
IS_NEW = "test.is_new"
352358
IS_QUARANTINED = "test.test_management.is_quarantined"
353359
IS_DISABLED = "test.test_management.is_test_disabled"

ddtrace/testing/internal/writer.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,6 @@ def serialize_test_run(test_run: TestRun) -> Event:
223223
"test.name": test_run.name,
224224
"test.status": test_run.get_status().value,
225225
"test.suite": test_run.suite.name,
226-
"test.type": "test",
227226
"type": "test",
228227
},
229228
"metrics": {

0 commit comments

Comments
 (0)