Skip to content

Commit 8e3ea51

Browse files
github-actions[bot]gnufedebrettlangdon
authored
fix(ci-visibility): proper test suite status with xdist [backport 3.7] (#13427)
Co-authored-by: Federico Mon <[email protected]> Co-authored-by: Brett Langdon <[email protected]>
1 parent aa2c619 commit 8e3ea51

7 files changed

+1039
-1
lines changed

ddtrace/contrib/internal/pytest/_plugin_v2.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -679,7 +679,10 @@ def _pytest_sessionfinish(session: pytest.Session, exitstatus: int) -> None:
679679
InternalTestSession.finish(force_finish_children=True)
680680

681681

682+
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
682683
def pytest_sessionfinish(session: pytest.Session, exitstatus: int) -> None:
684+
yield
685+
683686
if not is_test_visibility_enabled():
684687
return
685688

ddtrace/internal/ci_visibility/api/_base.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ def __post_init__(self):
9090

9191
class SPECIAL_STATUS(Enum):
9292
UNFINISHED = 1
93+
NONSTARTED = 2
9394

9495

9596
CIDT = TypeVar("CIDT", TestModuleId, TestSuiteId, TestId) # Child item ID types
@@ -415,6 +416,8 @@ def get_span_id(self) -> Optional[int]:
415416
def get_status(self) -> Union[TestStatus, SPECIAL_STATUS]:
416417
if self.is_finished():
417418
return self._status
419+
if not self.is_started():
420+
return SPECIAL_STATUS.NONSTARTED
418421
return SPECIAL_STATUS.UNFINISHED
419422

420423
def get_raw_status(self) -> TestStatus:
@@ -562,7 +565,10 @@ def get_status(self) -> Union[TestStatus, SPECIAL_STATUS]:
562565

563566
for child in self._children.values():
564567
child_status = child.get_status()
565-
if child_status == SPECIAL_STATUS.UNFINISHED:
568+
if child_status == SPECIAL_STATUS.NONSTARTED:
569+
# This means that the child was never started, so we don't count it
570+
continue
571+
elif child_status == SPECIAL_STATUS.UNFINISHED:
566572
# There's no point in continuing to count if we care about unfinished children
567573
log.debug("Item %s has unfinished children", self)
568574
return SPECIAL_STATUS.UNFINISHED
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
fixes:
3+
- |
4+
CI Visibility: This fix resolves an issue where ddtrace pytest plugin used with xdist would report test suites as failing even when all tests pass.
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import os
2+
import subprocess
3+
from unittest import mock
4+
5+
import pytest
6+
7+
from ddtrace.contrib.internal.pytest._utils import _USE_PLUGIN_V2
8+
from ddtrace.internal.ci_visibility._api_client import TestVisibilityAPISettings
9+
from tests.ci_visibility.util import _get_default_ci_env_vars
10+
from tests.utils import TracerTestCase
11+
from tests.utils import snapshot
12+
13+
14+
######
15+
# Skip these tests if they are not running under riot
16+
riot_env_value = os.getenv("RIOT", None)
17+
if not riot_env_value:
18+
pytest.importorskip("xdist", reason="Pytest xdist tests, not running under riot")
19+
######
20+
21+
22+
pytestmark = pytest.mark.skipif(not _USE_PLUGIN_V2, reason="Tests in this module are for v2 of the pytest plugin")
23+
24+
SNAPSHOT_IGNORES = [
25+
"meta.ci.workspace_path",
26+
"meta.error.stack",
27+
"meta.library_version",
28+
"meta.os.architecture",
29+
"meta.os.platform",
30+
"meta.os.version",
31+
"meta.runtime-id",
32+
"meta.runtime.version",
33+
"meta.test.framework_version",
34+
"meta.test_module_id",
35+
"meta.test_session_id",
36+
"meta.test_suite_id",
37+
"metrics._dd.top_level",
38+
"metrics._dd.tracer_kr",
39+
"metrics._sampling_priority_v1",
40+
"metrics.process_id",
41+
"duration",
42+
"start",
43+
]
44+
SNAPSHOT_IGNORES_PATCH_ALL = SNAPSHOT_IGNORES + ["meta.http.useragent"]
45+
46+
SNAPSHOT_IGNORES_ITR_COVERAGE = ["metrics.test.source.start", "metrics.test.source.end", "meta.test.source.file"]
47+
48+
49+
class PytestXdistSnapshotTestCase(TracerTestCase):
50+
@pytest.fixture(autouse=True)
51+
def fixtures(self, testdir, monkeypatch, git_repo):
52+
self.testdir = testdir
53+
self.monkeypatch = monkeypatch
54+
self.git_repo = git_repo
55+
56+
@snapshot(ignores=SNAPSHOT_IGNORES)
57+
def test_pytest_xdist_will_include_lines_pct(self):
58+
tools = """
59+
def add_two_number_list(list_1, list_2):
60+
output_list = []
61+
for number_a, number_b in zip(list_1, list_2):
62+
output_list.append(number_a + number_b)
63+
return output_list
64+
65+
def multiply_two_number_list(list_1, list_2):
66+
output_list = []
67+
for number_a, number_b in zip(list_1, list_2):
68+
output_list.append(number_a * number_b)
69+
return output_list
70+
"""
71+
self.testdir.makepyfile(tools=tools)
72+
test_tools = """
73+
from tools import add_two_number_list
74+
75+
def test_add_two_number_list():
76+
a_list = [1,2,3,4,5,6,7,8]
77+
b_list = [2,3,4,5,6,7,8,9]
78+
actual_output = add_two_number_list(a_list, b_list)
79+
80+
assert actual_output == [3,5,7,9,11,13,15,17]
81+
"""
82+
self.testdir.makepyfile(test_tools=test_tools)
83+
self.testdir.chdir()
84+
with mock.patch(
85+
"ddtrace.internal.ci_visibility._api_client._TestVisibilityAPIClientBase.fetch_settings",
86+
return_value=TestVisibilityAPISettings(False, False, False, False),
87+
):
88+
subprocess.run(
89+
["ddtrace-run", "coverage", "run", "--include=tools.py", "-m", "pytest", "--ddtrace", "-n", "2"],
90+
env=_get_default_ci_env_vars(
91+
dict(
92+
DD_API_KEY="foobar.baz",
93+
DD_CIVISIBILITY_ITR_ENABLED="false",
94+
DD_PATCH_MODULES="sqlite3:false",
95+
CI_PROJECT_DIR=str(self.testdir.tmpdir),
96+
DD_CIVISIBILITY_AGENTLESS_ENABLED="false",
97+
)
98+
),
99+
)
100+
101+
@snapshot(ignores=SNAPSHOT_IGNORES)
102+
def test_pytest_xdist_wont_include_lines_pct_if_report_empty(self):
103+
tools = """
104+
def add_two_number_list(list_1, list_2):
105+
output_list = []
106+
for number_a, number_b in zip(list_1, list_2):
107+
output_list.append(number_a + number_b)
108+
return output_list
109+
110+
def multiply_two_number_list(list_1, list_2):
111+
output_list = []
112+
for number_a, number_b in zip(list_1, list_2):
113+
output_list.append(number_a * number_b)
114+
return output_list
115+
"""
116+
self.testdir.makepyfile(tools=tools)
117+
test_tools = """
118+
from tools import add_two_number_list
119+
120+
def test_add_two_number_list():
121+
a_list = [1,2,3,4,5,6,7,8]
122+
b_list = [2,3,4,5,6,7,8,9]
123+
actual_output = add_two_number_list(a_list, b_list)
124+
125+
assert actual_output == [3,5,7,9,11,13,15,17]
126+
"""
127+
self.testdir.makepyfile(test_tools=test_tools)
128+
self.testdir.chdir()
129+
with mock.patch(
130+
"ddtrace.internal.ci_visibility._api_client._TestVisibilityAPIClientBase.fetch_settings",
131+
return_value=TestVisibilityAPISettings(False, False, False, False),
132+
):
133+
subprocess.run(
134+
["ddtrace-run", "coverage", "run", "--include=nothing.py", "-m", "pytest", "--ddtrace", "-n", "2"],
135+
env=_get_default_ci_env_vars(
136+
dict(
137+
DD_API_KEY="foobar.baz",
138+
DD_CIVISIBILITY_ITR_ENABLED="false",
139+
DD_PATCH_MODULES="sqlite3:false",
140+
CI_PROJECT_DIR=str(self.testdir.tmpdir),
141+
DD_CIVISIBILITY_AGENTLESS_ENABLED="false",
142+
)
143+
),
144+
)
145+
146+
@snapshot(ignores=SNAPSHOT_IGNORES_PATCH_ALL)
147+
def test_pytest_xdist_with_ddtrace_patch_all(self):
148+
call_httpx = """
149+
import httpx
150+
151+
def call_httpx():
152+
return httpx.get("http://localhost:9126/bad_path.cgi")
153+
"""
154+
self.testdir.makepyfile(call_httpx=call_httpx)
155+
test_call_httpx = """
156+
from call_httpx import call_httpx
157+
158+
def test_call_urllib():
159+
r = call_httpx()
160+
assert r.status_code == 404
161+
"""
162+
self.testdir.makepyfile(test_call_httpx=test_call_httpx)
163+
self.testdir.chdir()
164+
with mock.patch(
165+
"ddtrace.internal.ci_visibility._api_client._TestVisibilityAPIClientBase.fetch_settings",
166+
return_value=TestVisibilityAPISettings(False, False, False, False),
167+
):
168+
subprocess.run(
169+
["pytest", "--ddtrace", "--ddtrace-patch-all", "-n", "2"],
170+
env=_get_default_ci_env_vars(
171+
dict(
172+
DD_API_KEY="foobar.baz",
173+
DD_CIVISIBILITY_ITR_ENABLED="false",
174+
CI_PROJECT_DIR=str(self.testdir.tmpdir),
175+
DD_CIVISIBILITY_AGENTLESS_ENABLED="false",
176+
DD_PATCH_MODULES="httpx:true",
177+
)
178+
),
179+
)

0 commit comments

Comments
 (0)