Skip to content

Commit 1f52838

Browse files
chore(ci_visibility): add missing config env vars to new plugin (#15615)
## Description Add support for missing Test Optimization environment variables. ## Testing Unit tests. ## Risks None. ## Additional Notes None.
1 parent cbf4d7e commit 1f52838

File tree

8 files changed

+195
-5
lines changed

8 files changed

+195
-5
lines changed

ddtrace/testing/internal/env_tags.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ def get_env_tags() -> t.Dict[str, str]:
3333
git.get_git_tags_from_git_command(),
3434
ci.get_ci_tags(os.environ),
3535
git.get_git_tags_from_dd_variables(os.environ),
36+
get_custom_dd_tags(os.environ),
3637
)
3738

3839
if head_sha := tags.get(GitTag.COMMIT_HEAD_SHA):
@@ -66,3 +67,45 @@ def normalize_git_tags(tags: _TagDict) -> None:
6667
tags[GitTag.TAG] = git.normalize_ref(tag)
6768

6869
tags[GitTag.REPOSITORY_URL] = _filter_sensitive_info(tags.get(GitTag.REPOSITORY_URL))
70+
71+
72+
def parse_tags_str(tags_str: t.Optional[str]) -> t.Dict[str, str]:
73+
"""
74+
Parses a string containing key-value pairs and returns a dictionary.
75+
Key-value pairs are delimited by ':', and pairs are separated by whitespace, comma, OR BOTH.
76+
77+
This implementation aligns with the way tags are parsed by the Agent and other Datadog SDKs
78+
79+
:param tags_str: A string of the above form to parse tags from.
80+
:return: A dict containing the tags that were parsed.
81+
"""
82+
tags: t.Dict[str, str] = {}
83+
if not tags_str:
84+
return tags
85+
86+
# falling back to comma as separator
87+
separator = "," if "," in tags_str else " "
88+
89+
for tag in tags_str.split(separator):
90+
tag = tag.strip()
91+
if not tag:
92+
# skip empty tags
93+
continue
94+
elif ":" in tag:
95+
# if tag contains a colon, split on the first colon
96+
key, val = tag.split(":", 1)
97+
else:
98+
# if tag does not contain a colon, use the whole string as the key
99+
key, val = tag, ""
100+
key, val = key.strip(), val.strip()
101+
if key:
102+
# only add the tag if the key is not empty
103+
tags[key] = val
104+
return tags
105+
106+
107+
def get_custom_dd_tags(env: t.MutableMapping[str, str]) -> _TagDict:
108+
tags: _TagDict = {}
109+
tags.update(parse_tags_str(env.get("DD_TAGS")))
110+
tags.update(parse_tags_str(env.get("_CI_DD_TAGS")))
111+
return tags

ddtrace/testing/internal/http.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ def _detect_agentless_setup(cls) -> BackendConnectorSetup:
9898
Detect settings for agentless backend connection mode.
9999
"""
100100
site = os.environ.get("DD_SITE") or DEFAULT_SITE
101-
api_key = os.environ.get("DD_API_KEY")
101+
api_key = os.environ.get("_CI_DD_API_KEY") or os.environ.get("DD_API_KEY")
102102

103103
if not api_key:
104104
raise SetupError("DD_API_KEY environment variable is not set")
@@ -110,7 +110,7 @@ def _detect_evp_proxy_setup(cls) -> BackendConnectorSetup:
110110
"""
111111
Detect settings for EVP proxy mode backend connection mode.
112112
"""
113-
agent_url = os.environ.get("DD_TRACE_AGENT_URL")
113+
agent_url = os.environ.get("_CI_DD_AGENT_URL") or os.environ.get("DD_TRACE_AGENT_URL")
114114
if not agent_url:
115115
user_provided_host = os.environ.get("DD_TRACE_AGENT_HOSTNAME") or os.environ.get("DD_AGENT_HOST")
116116
user_provided_port = os.environ.get("DD_TRACE_AGENT_PORT") or os.environ.get("DD_AGENT_PORT")

ddtrace/testing/internal/logging.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
def setup_logging() -> None:
1515
testing_logger.propagate = False
1616

17-
log_level = logging.DEBUG if asbool(os.getenv("DD_TEST_DEBUG")) else logging.INFO
17+
debug_enabled = asbool(os.getenv("DD_TEST_DEBUG")) or asbool(os.getenv("DD_TRACE_DEBUG"))
18+
19+
log_level = logging.DEBUG if debug_enabled else logging.INFO
1820
testing_logger.setLevel(log_level)
1921

2022
for handler in list(testing_logger.handlers):

ddtrace/testing/internal/pytest/plugin.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
from ddtrace.testing.internal.tracer_api.coverage import coverage_collection
3838
from ddtrace.testing.internal.tracer_api.coverage import install_coverage
3939
from ddtrace.testing.internal.utils import TestContext
40+
from ddtrace.testing.internal.utils import asbool
4041

4142

4243
DISABLED_BY_TEST_MANAGEMENT_REASON = "Flaky test is disabled by Datadog"
@@ -659,7 +660,14 @@ def pytest_addoption(parser: pytest.Parser) -> None:
659660
parser.addini("ddtrace-patch-all", "Enable all integrations with ddtrace", type="bool")
660661

661662

663+
def _is_test_optimization_disabled_by_kill_switch() -> bool:
664+
return not asbool(os.environ.get("DD_CIVISIBILITY_ENABLED", "true"))
665+
666+
662667
def _is_enabled_early(early_config: pytest.Config, args: t.List[str]) -> bool:
668+
if _is_test_optimization_disabled_by_kill_switch():
669+
return False
670+
663671
if _is_option_true("no-ddtrace", early_config, args):
664672
return False
665673

@@ -708,6 +716,9 @@ def setup_coverage_collection() -> None:
708716

709717

710718
def pytest_configure(config: pytest.Config) -> None:
719+
if _is_test_optimization_disabled_by_kill_switch():
720+
return
721+
711722
session_manager = config.stash.get(SESSION_MANAGER_STASH_KEY, None)
712723
if not session_manager:
713724
log.debug("Session manager not initialized (plugin was not enabled)")

ddtrace/testing/internal/session_manager.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ def __init__(self, session: TestSession) -> None:
6666

6767
self.is_auto_injected = bool(os.getenv("DD_CIVISIBILITY_AUTO_INSTRUMENTATION_PROVIDER", ""))
6868

69-
self.env = os.environ.get("DD_ENV") or DEFAULT_ENV_NAME
69+
self.env = os.environ.get("_CI_DD_ENV") or os.environ.get("DD_ENV") or DEFAULT_ENV_NAME
7070

7171
self.api_client = APIClient(
7272
service=self.service,
@@ -78,6 +78,8 @@ def __init__(self, session: TestSession) -> None:
7878
telemetry_api=self.telemetry_api,
7979
)
8080
self.settings = self.api_client.get_settings()
81+
self.override_settings_with_env_vars()
82+
8183
self.known_tests = self.api_client.get_known_tests() if self.settings.known_tests_enabled else set()
8284
self.test_properties = (
8385
self.api_client.get_test_management_properties() if self.settings.test_management.enabled else {}
@@ -151,7 +153,7 @@ def setup_retry_handlers(self) -> None:
151153
else:
152154
log.info("Not enabling Early Flake Detection: no known tests")
153155

154-
if self.settings.auto_test_retries.enabled and asbool(os.getenv("DD_CIVISIBILITY_FLAKY_RETRY_ENABLED", "true")):
156+
if self.settings.auto_test_retries.enabled:
155157
self.retry_handlers.append(AutoTestRetriesHandler(self))
156158

157159
def start(self) -> None:
@@ -310,6 +312,33 @@ def is_skippable_test(self, test_ref: TestRef) -> bool:
310312
def has_codeowners(self) -> bool:
311313
return self.codeowners is not None
312314

315+
def override_settings_with_env_vars(self) -> None:
316+
# Kill switches.
317+
# These variables default to true, and if explicitly given a false value, disable a feature.
318+
if not asbool(os.environ.get("DD_CIVISIBILITY_ITR_ENABLED", "true")):
319+
log.debug("Test Impact Analysis is disabled by environment variable")
320+
self.settings.itr_enabled = False
321+
322+
if not asbool(os.environ.get("DD_CIVISIBILITY_EARLY_FLAKE_DETECTION_ENABLED", "true")):
323+
log.debug("Early Flake Detection is disabled by environment variable")
324+
self.settings.early_flake_detection.enabled = False
325+
326+
if not asbool(os.environ.get("DD_CIVISIBILITY_FLAKY_RETRY_ENABLED", "true")):
327+
log.debug("Auto Test Retries is disabled by environment variable")
328+
self.settings.auto_test_retries.enabled = False
329+
330+
# "Reverse" kill switches.
331+
# These variables default to false, and if explicitly given a true value, disable a feature.
332+
if asbool(os.environ.get("_DD_CIVISIBILITY_ITR_PREVENT_TEST_SKIPPING", "false")):
333+
log.debug("TIA test skipping is disabled by environment variable")
334+
self.settings.skipping_enabled = False
335+
336+
# Other overrides.
337+
# These variables default to false, and if explicitly given a true value, enable a feature.
338+
if asbool(os.environ.get("_DD_CIVISIBILITY_ITR_FORCE_ENABLE_COVERAGE", "false")):
339+
log.debug("TIA code coverage collection is enabled by environment variable")
340+
self.settings.coverage_enabled = True
341+
313342

314343
def _get_service_name_from_git_repo(env_tags: t.Dict[str, str]) -> t.Optional[str]:
315344
repo_name = env_tags.get(GitTag.REPOSITORY_URL)

tests/testing/internal/test_env_tags.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,3 +319,17 @@ def test_falsey_ci_provider_values_overwritten_by_git_executable(
319319
assert tags["git.commit.author.name"] == "John Doe"
320320
assert tags["git.commit.author.email"] == "[email protected]"
321321
assert tags["ci.workspace_path"] == git_repo
322+
323+
324+
def test_custom_dd_tags(monkeypatch: pytest.MonkeyPatch, git_repo: str) -> None:
325+
env = {
326+
"DD_TAGS": "foo:1 bar.baz:2",
327+
}
328+
329+
monkeypatch.setattr(os, "environ", env)
330+
monkeypatch.chdir(git_repo)
331+
332+
tags = get_env_tags()
333+
334+
assert tags["foo"] == "1"
335+
assert tags["bar.baz"] == "2"

tests/testing/internal/test_session_manager.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
1+
from contextlib import contextmanager
12
import os
3+
import typing as t
4+
from unittest.mock import patch
25

36
import pytest
47

58
from ddtrace.testing.internal.ci import CITag
9+
from ddtrace.testing.internal.session_manager import SessionManager
610
from ddtrace.testing.internal.test_data import ModuleRef
711
from ddtrace.testing.internal.test_data import SuiteRef
812
from ddtrace.testing.internal.test_data import TestRef
13+
from ddtrace.testing.internal.test_data import TestSession
914
from tests.testing.mocks import MockDefaults
15+
from tests.testing.mocks import mock_api_client_settings
1016
from tests.testing.mocks import session_manager_mock
17+
from tests.testing.mocks import setup_standard_mocks
1118

1219

1320
class TestSessionManagerIsSkippableTest:
@@ -200,3 +207,62 @@ def test_session_name_from_test_command(self, monkeypatch: pytest.MonkeyPatch) -
200207
expected_name = "pytest"
201208
assert session_manager._get_test_session_name() == expected_name
202209
assert session_manager.writer.metadata["*"]["test_session.name"] == expected_name
210+
211+
212+
class TestSessionManagerEnvVarOverrides:
213+
def setup_method(self) -> None:
214+
self.session = TestSession("pytest")
215+
self.session.set_attributes(
216+
test_command="pytest --ddtrace", test_framework="pytest", test_framework_version="9.0.0"
217+
)
218+
219+
@contextmanager
220+
def mock_settings(self, **kwargs) -> t.Generator[None, None, None]:
221+
with (
222+
patch(
223+
"ddtrace.testing.internal.session_manager.APIClient",
224+
return_value=mock_api_client_settings(**kwargs),
225+
),
226+
setup_standard_mocks(),
227+
):
228+
yield
229+
230+
@pytest.mark.parametrize("env_var_value, expected_setting", [(None, True), ("true", True), ("false", False)])
231+
def test_session_manager_efd_kill_switch(self, monkeypatch, env_var_value, expected_setting):
232+
with self.mock_settings(efd_enabled=True):
233+
if env_var_value is not None:
234+
monkeypatch.setenv("DD_CIVISIBILITY_EARLY_FLAKE_DETECTION_ENABLED", env_var_value)
235+
session_manager = SessionManager(self.session)
236+
assert session_manager.settings.early_flake_detection.enabled is expected_setting
237+
238+
@pytest.mark.parametrize("env_var_value, expected_setting", [(None, True), ("true", True), ("false", False)])
239+
def test_session_manager_atr_kill_switch(self, monkeypatch, env_var_value, expected_setting):
240+
with self.mock_settings(auto_retries_enabled=True):
241+
if env_var_value is not None:
242+
monkeypatch.setenv("DD_CIVISIBILITY_FLAKY_RETRY_ENABLED", env_var_value)
243+
session_manager = SessionManager(self.session)
244+
assert session_manager.settings.auto_test_retries.enabled is expected_setting
245+
246+
@pytest.mark.parametrize("env_var_value, expected_setting", [(None, True), ("true", True), ("false", False)])
247+
def test_session_manager_itr_kill_switch(self, monkeypatch, env_var_value, expected_setting):
248+
with self.mock_settings(skipping_enabled=True):
249+
if env_var_value is not None:
250+
monkeypatch.setenv("DD_CIVISIBILITY_ITR_ENABLED", env_var_value)
251+
session_manager = SessionManager(self.session)
252+
assert session_manager.settings.itr_enabled is expected_setting
253+
254+
@pytest.mark.parametrize("env_var_value, expected_setting", [(None, True), ("true", False), ("false", True)])
255+
def test_session_manager_skipping_kill_switch(self, monkeypatch, env_var_value, expected_setting):
256+
with self.mock_settings(skipping_enabled=True):
257+
if env_var_value is not None:
258+
monkeypatch.setenv("_DD_CIVISIBILITY_ITR_PREVENT_TEST_SKIPPING", env_var_value)
259+
session_manager = SessionManager(self.session)
260+
assert session_manager.settings.skipping_enabled is expected_setting
261+
262+
@pytest.mark.parametrize("env_var_value, expected_setting", [(None, False), ("true", True), ("false", False)])
263+
def test_session_manager_force_coverage(self, monkeypatch, env_var_value, expected_setting):
264+
with self.mock_settings():
265+
if env_var_value is not None:
266+
monkeypatch.setenv("_DD_CIVISIBILITY_ITR_FORCE_ENABLE_COVERAGE", env_var_value)
267+
session_manager = SessionManager(self.session)
268+
assert session_manager.settings.coverage_enabled is expected_setting

tests/testing/test_integration.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,31 @@ def test_simple():
115115
assert result.ret == 0
116116
result.assert_outcomes(passed=1)
117117

118+
def test_simple_plugin_disabled_by_kill_switch(self, pytester: Pytester, monkeypatch: MonkeyPatch) -> None:
119+
"""Test that plugin does not run when DD_CIVISIBILITY_ENABLED is false."""
120+
# Create a simple test file
121+
pytester.makepyfile(
122+
"""
123+
def test_simple():
124+
'''A simple test.'''
125+
assert True
126+
"""
127+
)
128+
129+
monkeypatch.setenv("DD_CIVISIBILITY_ENABLED", "false")
130+
131+
# Use network mocks to prevent all real HTTP calls
132+
with network_mocks(), patch("ddtrace.testing.internal.session_manager.APIClient") as mock_api_client:
133+
mock_api_client.return_value = mock_api_client_settings()
134+
135+
result = pytester.runpytest("--ddtrace", "-v")
136+
137+
assert mock_api_client.call_count == 0
138+
139+
# Test should pass
140+
assert result.ret == 0
141+
result.assert_outcomes(passed=1)
142+
118143
def test_retry_functionality_with_pytester(self, pytester: Pytester, monkeypatch: MonkeyPatch) -> None:
119144
"""Test that failing tests are retried when auto retry is enabled."""
120145
# Create a test file with a failing test

0 commit comments

Comments
 (0)