Skip to content

Commit 8195942

Browse files
feat(ci_visibility): print report links at the end of the test session (#13545)
This introduces report links to the pytest plugin. At the end of a test session, ddtrace shows links to the Datadog Test Optimization pages with the test results for the current commit and for the current CI job(provided that the CI environment variables with the current job and pipeline ID are available). This is a feature that we already have [in `datadog-ci`](https://github.com/DataDog/datadog-ci/blob/v3.7.0/src/commands/junit/renderer.ts#L50) for JUnit XML upload , and internally in the dogweb CI. This PR makes it available in dd-trace-py. ## Checklist - [x] PR author has checked that all the criteria below are met - The PR description includes an overview of the change - The PR description articulates the motivation for the change - The change includes tests OR the PR description describes a testing strategy - The PR description notes risks associated with the change, if any - Newly-added code is easy to change - The change follows the [library release note guidelines](https://ddtrace.readthedocs.io/en/stable/releasenotes.html) - The change includes or references documentation updates if necessary - Backport labels are set (if [applicable](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)) ## Reviewer Checklist - [x] Reviewer has checked that all the criteria below are met - Title is accurate - All changes are related to the pull request's stated goal - Avoids breaking [API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces) changes - Testing strategy adequately addresses listed risks - Newly-added code is easy to change - Release note makes sense to a user of the library - If necessary, author has acknowledged and discussed the performance implications of this PR as reported in the benchmarks PR comment - Backport labels are set in a manner that is consistent with the [release branch maintenance policy](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)
1 parent dacc9a8 commit 8195942

File tree

6 files changed

+371
-9
lines changed

6 files changed

+371
-9
lines changed

ddtrace/contrib/internal/pytest/_plugin_v2.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from ddtrace.contrib.internal.coverage.utils import _is_coverage_patched
1818
from ddtrace.contrib.internal.pytest._benchmark_utils import _set_benchmark_data_from_item
1919
from ddtrace.contrib.internal.pytest._plugin_v1 import _is_pytest_cov_enabled
20+
from ddtrace.contrib.internal.pytest._report_links import print_test_report_links
2021
from ddtrace.contrib.internal.pytest._types import _pytest_report_teststatus_return_type
2122
from ddtrace.contrib.internal.pytest._types import pytest_CallInfo
2223
from ddtrace.contrib.internal.pytest._types import pytest_Config
@@ -673,6 +674,7 @@ def _pytest_terminal_summary_post_yield(terminalreporter, failed_reports_initial
673674
quarantine_pytest_terminal_summary_post_yield(terminalreporter)
674675
attempt_to_fix_pytest_terminal_summary_post_yield(terminalreporter)
675676

677+
print_test_report_links(terminalreporter)
676678
return
677679

678680

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import os
2+
import re
3+
from urllib.parse import quote
4+
5+
from ddtrace.ext import ci
6+
from ddtrace.internal.ci_visibility import CIVisibility
7+
8+
9+
DEFAULT_DATADOG_SITE = "datadoghq.com"
10+
DEFAULT_DATADOG_SUBDOMAIN = "app"
11+
12+
SAFE_FOR_QUERY = re.compile(r"\A[A-Za-z0-9._-]+\Z")
13+
14+
15+
def print_test_report_links(terminalreporter):
16+
base_url = _get_base_url(
17+
dd_site=os.getenv("DD_SITE", DEFAULT_DATADOG_SITE), dd_subdomain=os.getenv("DD_SUBDOMAIN", "")
18+
)
19+
ci_tags = CIVisibility.get_ci_tags()
20+
settings = CIVisibility.get_session_settings()
21+
service = settings.test_service
22+
env = CIVisibility.get_dd_env()
23+
24+
redirect_test_commit_url = _build_test_commit_redirect_url(base_url, ci_tags, service, env)
25+
test_runs_url = _build_test_runs_url(base_url, ci_tags)
26+
27+
if not (redirect_test_commit_url or test_runs_url):
28+
return
29+
30+
terminalreporter.section("Datadog Test Reports", cyan=True, bold=True)
31+
terminalreporter.line("View detailed reports in Datadog (they may take a few minutes to become available):")
32+
33+
if redirect_test_commit_url:
34+
terminalreporter.line("")
35+
terminalreporter.line("* Commit report:")
36+
terminalreporter.line(f" → {redirect_test_commit_url}")
37+
38+
if test_runs_url:
39+
terminalreporter.line("")
40+
terminalreporter.line("* Test runs report:")
41+
terminalreporter.line(f" → {test_runs_url}")
42+
43+
44+
def _get_base_url(dd_site, dd_subdomain):
45+
# Based on <https://github.com/DataDog/datadog-ci/blob/v3.7.0/src/helpers/app.ts>.
46+
subdomain = dd_subdomain or DEFAULT_DATADOG_SUBDOMAIN
47+
dd_site_parts = dd_site.split(".")
48+
if len(dd_site_parts) == 3:
49+
if subdomain == DEFAULT_DATADOG_SUBDOMAIN:
50+
return f"https://{dd_site}"
51+
else:
52+
return f"https://{subdomain}.{dd_site_parts[1]}.{dd_site_parts[2]}"
53+
else:
54+
return f"https://{subdomain}.{dd_site}"
55+
56+
57+
def _build_test_commit_redirect_url(base_url, ci_tags, service, env):
58+
params = {
59+
"repo_url": ci_tags.get(ci.git.REPOSITORY_URL),
60+
"branch": ci_tags.get(ci.git.BRANCH),
61+
"commit_sha": ci_tags.get(ci.git.COMMIT_SHA),
62+
"service": service,
63+
}
64+
if any(v is None for v in params.values()):
65+
return None
66+
67+
url_format = "/ci/redirect/tests/{repo_url}/-/{service}/-/{branch}/-/{commit_sha}"
68+
url = base_url + url_format.format(**{k: quote(v, safe="") for k, v in params.items()})
69+
if env:
70+
url += f"?env={env}".format(env=quote(env, safe=""))
71+
72+
return url
73+
74+
75+
def _quote_for_query(text):
76+
if SAFE_FOR_QUERY.match(text):
77+
return text
78+
79+
escaped_text = text.replace("\\", "\\\\").replace('"', '\\"')
80+
return f'"{escaped_text}"'
81+
82+
83+
def _build_test_runs_url(base_url, ci_tags):
84+
ci_job_name = ci_tags.get(ci.JOB_NAME)
85+
ci_pipeline_id = ci_tags.get(ci.PIPELINE_ID)
86+
87+
if not (ci_job_name and ci_pipeline_id):
88+
return None
89+
90+
query = "@ci.job.name:{} @ci.pipeline.id:{}".format(_quote_for_query(ci_job_name), _quote_for_query(ci_pipeline_id))
91+
92+
url_format = "/ci/test-runs?query={query}&index=citest"
93+
url = base_url + url_format.format(query=quote(query, safe=""))
94+
return url

ddtrace/internal/ci_visibility/recorder.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -233,14 +233,14 @@ def __init__(
233233

234234
self._git_data: GitData = get_git_data_from_tags(self._tags)
235235

236-
dd_env = os.getenv("_CI_DD_ENV", ddconfig.env)
236+
self._dd_env = os.getenv("_CI_DD_ENV", ddconfig.env)
237237
dd_env_msg = ""
238238

239239
if ddconfig._ci_visibility_agentless_enabled:
240240
# In agentless mode, normalize an unset env to none (this is already done by the backend in most cases, so
241241
# it does not override default behavior)
242-
if dd_env is None:
243-
dd_env = "none"
242+
if self._dd_env is None:
243+
self._dd_env = "none"
244244
dd_env_msg = " (not set in environment)"
245245
if not self._api_key:
246246
raise EnvironmentError(
@@ -257,13 +257,13 @@ def __init__(
257257
self._dd_site,
258258
ddconfig._ci_visibility_agentless_url if ddconfig._ci_visibility_agentless_url else None,
259259
self._service,
260-
dd_env,
260+
self._dd_env,
261261
)
262262
elif evp_proxy_base_url := self._agent_evp_proxy_base_url():
263263
# In EVP-proxy cases, if an env is not provided, we need to get the agent's default env in order to make
264264
# the correct decision:
265-
if dd_env is None:
266-
dd_env = self._agent_get_default_env()
265+
if self._dd_env is None:
266+
self._dd_env = self._agent_get_default_env()
267267
dd_env_msg = " (default environment provided by agent)"
268268
self._requests_mode = REQUESTS_MODE.EVP_PROXY_EVENTS
269269
requests_mode_str = "EVP Proxy"
@@ -273,7 +273,7 @@ def __init__(
273273
self._configurations,
274274
self.tracer._agent_url,
275275
self._service,
276-
dd_env,
276+
self._dd_env,
277277
evp_proxy_base_url=evp_proxy_base_url,
278278
)
279279
else:
@@ -293,7 +293,7 @@ def __init__(
293293

294294
self._configure_writer(coverage_enabled=self._collect_coverage_enabled, url=self.tracer._agent_url)
295295

296-
log.info("Service: %s (env: %s%s)", self._service, dd_env, dd_env_msg)
296+
log.info("Service: %s (env: %s%s)", self._service, self._dd_env, dd_env_msg)
297297
log.info("Requests mode: %s", requests_mode_str)
298298
log.info("Git metadata upload enabled: %s", self._should_upload_git_metadata)
299299
log.info("API-provided settings: coverage collection: %s", self._api_settings.coverage_enabled)
@@ -991,6 +991,16 @@ def set_library_capabilities(cls, capabilities: LibraryCapabilities) -> None:
991991
return
992992
client.set_metadata("test", capabilities.tags())
993993

994+
@classmethod
995+
def get_ci_tags(cls):
996+
instance = cls.get_instance()
997+
return instance._tags
998+
999+
@classmethod
1000+
def get_dd_env(cls):
1001+
instance = cls.get_instance()
1002+
return instance._dd_env
1003+
9941004
@classmethod
9951005
def is_known_test(cls, test_id: Union[TestId, InternalTestId]) -> bool:
9961006
instance = cls.get_instance()

hatch.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -741,7 +741,8 @@ dependencies = [
741741
"requests",
742742
"hypothesis",
743743
"pytest{matrix:pytest}",
744-
"pytest-cov"
744+
"pytest-cov",
745+
"pytest-mock"
745746
]
746747

747748
[envs.pytest_plugin_v2.env-vars]
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
features:
3+
- |
4+
CI Visibility: This introduces report links to the pytest plugin. At the end of a test session, ddtrace shows links
5+
to the Datadog Test Optimization pages with the test results for the current commit and for the current CI job
6+
(provided that the CI environment variables with the current job and pipeline ID are available).

0 commit comments

Comments
 (0)