Skip to content

Commit b0c1496

Browse files
fix(ci_visibility): handle PYTEST_ADDOPTS for enabling code coverage collection (#13820)
This fix resolves an issue where code coverage would not be enabled if ddtrace was enabled via the ``PYTEST_ADDOPTS`` environment variable. ## 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 009ecef commit b0c1496

File tree

6 files changed

+129
-10
lines changed

6 files changed

+129
-10
lines changed

ddtrace/contrib/internal/pytest/_plugin_v1.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,7 @@ def _get_test_class_hierarchy(item):
417417

418418

419419
def pytest_load_initial_conftests(early_config, parser, args):
420-
if _is_enabled_early(early_config):
420+
if _is_enabled_early(early_config, args):
421421
# Enables experimental use of ModuleCodeCollector for coverage collection.
422422
from ddtrace.internal.ci_visibility.coverage import USE_DD_COVERAGE
423423
from ddtrace.internal.logger import get_logger

ddtrace/contrib/internal/pytest/_plugin_v2.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ def _pytest_load_initial_conftests_pre_yield(early_config, parser, args):
235235
ModuleCodeCollector has a tangible impact on the time it takes to load modules, so it should only be installed if
236236
coverage collection is requested by the backend.
237237
"""
238-
if not _is_enabled_early(early_config):
238+
if not _is_enabled_early(early_config, args):
239239
return
240240

241241
try:

ddtrace/contrib/internal/pytest/_utils.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ def _extract_span(item):
211211
return getattr(item, "_datadog_span", None)
212212

213213

214-
def _is_enabled_early(early_config):
214+
def _is_enabled_early(early_config, args):
215215
"""Checks if the ddtrace plugin is enabled before the config is fully populated.
216216
217217
This is necessary because the module watchdog for coverage collection needs to be enabled as early as possible.
@@ -222,15 +222,14 @@ def _is_enabled_early(early_config):
222222
if not _pytest_version_supports_itr():
223223
return False
224224

225-
if (
226-
"--no-ddtrace" in early_config.invocation_params.args
227-
or early_config.getini("no-ddtrace")
228-
or "ddtrace" in early_config.inicfg
229-
and early_config.getini("ddtrace") is False
230-
):
225+
if _is_option_true("no-ddtrace", early_config, args):
231226
return False
232227

233-
return "--ddtrace" in early_config.invocation_params.args or early_config.getini("ddtrace")
228+
return _is_option_true("ddtrace", early_config, args)
229+
230+
231+
def _is_option_true(option, early_config, args):
232+
return early_config.getoption(option) or early_config.getini(option) or f"--{option}" in args
234233

235234

236235
class _TestOutcome(t.NamedTuple):
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
fixes:
3+
- |
4+
This fix resolves an issue where code coverage would not be enabled if ddtrace was enabled via the
5+
``PYTEST_ADDOPTS`` environment variable.

tests/contrib/pytest/test_pytest.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,41 @@ def test_ini(ddspan):
218218

219219
assert len(spans) == 4
220220

221+
def test_pytest_addopts_env_var(self):
222+
"""Test enabling ddtrace via the PYTEST_ADDOPTS environment variable."""
223+
py_file = self.testdir.makepyfile(
224+
"""
225+
import pytest
226+
227+
def test_trace(ddspan):
228+
assert ddspan is not None
229+
"""
230+
)
231+
file_name = os.path.basename(py_file.strpath)
232+
rec = self.inline_run(file_name, extra_env={"PYTEST_ADDOPTS": "--ddtrace"})
233+
rec.assertoutcome(passed=1)
234+
spans = self.pop_spans()
235+
236+
assert len(spans) == 4
237+
238+
def test_pytest_addopts_ini(self):
239+
"""Test enabling ddtrace via the `addopts` option in the ini file."""
240+
self.testdir.makefile(".ini", pytest="[pytest]\naddopts = --ddtrace\n")
241+
py_file = self.testdir.makepyfile(
242+
"""
243+
import pytest
244+
245+
def test_ini(ddspan):
246+
assert ddspan is not None
247+
"""
248+
)
249+
file_name = os.path.basename(py_file.strpath)
250+
rec = self.inline_run(file_name)
251+
rec.assertoutcome(passed=1)
252+
spans = self.pop_spans()
253+
254+
assert len(spans) == 4
255+
221256
def test_pytest_command(self):
222257
"""Test that the pytest run command is stored on a test span."""
223258
py_file = self.testdir.makepyfile(
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
from unittest import mock
2+
3+
import pytest
4+
5+
from ddtrace.contrib.internal.pytest._utils import _pytest_version_supports_itr
6+
from ddtrace.internal.ci_visibility._api_client import TestVisibilityAPISettings
7+
from ddtrace.internal.ci_visibility.constants import COVERAGE_TAG_NAME
8+
from tests.contrib.pytest.test_pytest import PytestTestCaseBase
9+
from tests.contrib.pytest.test_pytest import _get_spans_from_list
10+
11+
12+
_TEST_PASS_CONTENT = """
13+
def test_func_pass():
14+
assert True
15+
"""
16+
17+
pytestmark = pytest.mark.skipif(not _pytest_version_supports_itr(), reason="pytest version does not support coverage")
18+
19+
20+
class PytestEarlyConfigTestCase(PytestTestCaseBase):
21+
"""
22+
Test that code coverage is enabled in the `pytest_load_initial_conftests` hook, regardless of the method used to
23+
enable ddtrace (command line, env var, or ini file).
24+
"""
25+
26+
@pytest.fixture(autouse=True, scope="function")
27+
def set_up_features(self):
28+
with mock.patch(
29+
"ddtrace.internal.ci_visibility.recorder.CIVisibility._check_enabled_features",
30+
return_value=TestVisibilityAPISettings(coverage_enabled=True),
31+
):
32+
yield
33+
34+
def test_coverage_not_enabled(self):
35+
self.testdir.makepyfile(test_pass=_TEST_PASS_CONTENT)
36+
self.inline_run()
37+
spans = self.pop_spans()
38+
assert not spans
39+
40+
def test_coverage_enabled_via_command_line_option(self):
41+
self.testdir.makepyfile(test_pass=_TEST_PASS_CONTENT)
42+
self.inline_run("--ddtrace")
43+
spans = self.pop_spans()
44+
[suite_span] = _get_spans_from_list(spans, "suite")
45+
[test_span] = _get_spans_from_list(spans, "test")
46+
assert (
47+
suite_span.get_struct_tag(COVERAGE_TAG_NAME) is not None or test_span.get_tag(COVERAGE_TAG_NAME) is not None
48+
)
49+
50+
def test_coverage_enabled_via_pytest_addopts_env_var(self):
51+
self.testdir.makepyfile(test_pass=_TEST_PASS_CONTENT)
52+
self.inline_run(extra_env={"PYTEST_ADDOPTS": "--ddtrace"})
53+
spans = self.pop_spans()
54+
[suite_span] = _get_spans_from_list(spans, "suite")
55+
[test_span] = _get_spans_from_list(spans, "test")
56+
assert (
57+
suite_span.get_struct_tag(COVERAGE_TAG_NAME) is not None or test_span.get_tag(COVERAGE_TAG_NAME) is not None
58+
)
59+
60+
def test_coverage_enabled_via_addopts_ini_file_option(self):
61+
self.testdir.makepyfile(test_pass=_TEST_PASS_CONTENT)
62+
self.testdir.makefile(".ini", pytest="[pytest]\naddopts = --ddtrace\n")
63+
self.inline_run()
64+
spans = self.pop_spans()
65+
[suite_span] = _get_spans_from_list(spans, "suite")
66+
[test_span] = _get_spans_from_list(spans, "test")
67+
assert (
68+
suite_span.get_struct_tag(COVERAGE_TAG_NAME) is not None or test_span.get_tag(COVERAGE_TAG_NAME) is not None
69+
)
70+
71+
def test_coverage_enabled_via_ddtrace_ini_file_option(self):
72+
self.testdir.makepyfile(test_pass=_TEST_PASS_CONTENT)
73+
self.testdir.makefile(".ini", pytest="[pytest]\nddtrace = 1\n")
74+
self.inline_run()
75+
spans = self.pop_spans()
76+
[suite_span] = _get_spans_from_list(spans, "suite")
77+
[test_span] = _get_spans_from_list(spans, "test")
78+
assert (
79+
suite_span.get_struct_tag(COVERAGE_TAG_NAME) is not None or test_span.get_tag(COVERAGE_TAG_NAME) is not None
80+
)

0 commit comments

Comments
 (0)