Skip to content

Commit 4ddef1a

Browse files
fix(ci_visibility): handle skipif with non-positional arguments [backport 3.11] (#14162)
Backport 13fcc38 from #14150 to 3.11. The ddtrace pytest plugin currently assumes that the `skipif` marker will be provided the condition as a positional argument. It breaks if no condition is provided, or if the condition is passed as a keyword argument. This leads to the `_pytest_runtest_protocol_pre_yield()` call raising an exception, which leaves `coverage_collector` unset, which leads to the call to `_pytest_runtest_protocol_post_yield()` to fail. If the test was not finished during `pytest_runtest_protocol` (for example, because the `pytest-rerunfailures` or `flaky` plugins are in use, which causes ddtrace to skip its own `pytest_runtest_protocol`), the test span will be left unfinished and with a `fail` test status. This PR adds handling for keyword and absent `skipif` conditions, and also makes sure `coverage_collector` is set even if `_pytest_runtest_protocol_pre_yield()` fails. - [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)) - [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) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent dc72fd3 commit 4ddef1a

File tree

4 files changed

+133
-2
lines changed

4 files changed

+133
-2
lines changed

ddtrace/contrib/internal/pytest/_plugin_v2.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,7 @@ def pytest_runtest_protocol_wrapper(item, nextitem) -> None:
523523
try:
524524
coverage_collector = _pytest_runtest_protocol_pre_yield(item)
525525
except Exception: # noqa: E722
526+
coverage_collector = None
526527
log.debug("encountered error during pre-test", exc_info=True)
527528

528529
yield

ddtrace/contrib/internal/pytest/_utils.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,18 +184,29 @@ def _pytest_version_supports_attempt_to_fix():
184184
return _get_pytest_version_tuple() >= ATTEMPT_TO_FIX_MIN_SUPPORTED_VERSION
185185

186186

187+
def _get_skipif_condition(marker):
188+
if marker.args:
189+
condition = marker.args[0]
190+
elif marker.kwargs:
191+
condition = marker.kwargs.get("condition")
192+
else:
193+
condition = True # `skipif` with no condition is equivalent to plain `skip`.
194+
195+
return condition
196+
197+
187198
def _pytest_marked_to_skip(item: pytest.Item) -> bool:
188199
"""Checks whether Pytest will skip an item"""
189200
if item.get_closest_marker("skip") is not None:
190201
return True
191202

192-
return any(marker.args[0] for marker in item.iter_markers(name="skipif"))
203+
return any(_get_skipif_condition(marker) is True for marker in item.iter_markers(name="skipif"))
193204

194205

195206
def _is_test_unskippable(item: pytest.Item) -> bool:
196207
"""Returns True if a test has a skipif marker with value false and reason ITR_UNSKIPPABLE_REASON"""
197208
return any(
198-
(marker.args[0] is False and marker.kwargs.get("reason") == ITR_UNSKIPPABLE_REASON)
209+
(_get_skipif_condition(marker) is False and marker.kwargs.get("reason") == ITR_UNSKIPPABLE_REASON)
199210
for marker in item.iter_markers(name="skipif")
200211
)
201212

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
fixes:
3+
- |
4+
CI Visibility: This fix resolves an issue where using the pytest ``skipif`` marker with the condition passed as a
5+
keyword argument (or not provided at all) would cause the test to be reported as failed, in particular when the
6+
``flaky`` or ``pytest-rerunfailures`` were also used.

tests/contrib/pytest_flaky/test_pytest_flaky.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,116 @@ def test_pytest_no_flaky(self):
7070
assert flaky_spans[0].get_tag("test.status") == "fail"
7171
assert flaky_spans[1].get_tag("test.status") == "pass"
7272
assert rec.ret == 1
73+
74+
def test_skipif_without_condition(self):
75+
"""
76+
Test that the plugin does not break if `skipif` is used with no arguments, while advanced features are disabled
77+
by an external plugin.
78+
"""
79+
self.testdir.makepyfile(
80+
"""
81+
import pytest
82+
83+
@pytest.mark.skipif()
84+
def test_foo():
85+
assert True
86+
"""
87+
)
88+
rec = self.inline_run("--ddtrace", "-p", "flaky")
89+
rec.assertoutcome(skipped=1)
90+
spans = self.pop_spans()
91+
[test_span] = _get_spans_from_list(spans, "test", "test_foo")
92+
assert test_span.get_tag("test.status") == "skip"
93+
assert rec.ret == 0
94+
95+
def test_skipif_with_keyword_condition(self):
96+
"""
97+
Test that the plugin does not break if `skipif` is used with keyword arguments, while advanced features are
98+
disabled by an external plugin.
99+
"""
100+
self.testdir.makepyfile(
101+
"""
102+
import pytest
103+
104+
@pytest.mark.skipif(condition=1 > 0, reason="because I can")
105+
def test_skip():
106+
assert True
107+
108+
@pytest.mark.skipif(condition=1 < 0, reason="because I can't")
109+
def test_no_skip():
110+
assert True
111+
"""
112+
)
113+
rec = self.inline_run("--ddtrace", "-p", "flaky")
114+
rec.assertoutcome(skipped=1, passed=1)
115+
spans = self.pop_spans()
116+
[skip_test_span] = _get_spans_from_list(spans, "test", "test_skip")
117+
[no_skip_test_span] = _get_spans_from_list(spans, "test", "test_no_skip")
118+
assert skip_test_span.get_tag("test.status") == "skip"
119+
assert no_skip_test_span.get_tag("test.status") == "pass"
120+
assert rec.ret == 0
121+
122+
def test_skipif_with_string_condition(self):
123+
"""
124+
Test that the plugin does not break if `skipif` is used with a string condition, while advanced features are
125+
disabled by an external plugin.
126+
"""
127+
# TODO(vitor-de-araujo): string conditions are currently not evaluated.
128+
self.testdir.makepyfile(
129+
"""
130+
import pytest
131+
132+
@pytest.mark.skipif("1 > 0")
133+
def test_skip():
134+
assert True
135+
136+
@pytest.mark.skipif("1 < 0")
137+
def test_no_skip():
138+
assert True
139+
"""
140+
)
141+
rec = self.inline_run("--ddtrace", "-p", "flaky")
142+
rec.assertoutcome(skipped=1, passed=1)
143+
spans = self.pop_spans()
144+
[skip_test_span] = _get_spans_from_list(spans, "test", "test_skip")
145+
[no_skip_test_span] = _get_spans_from_list(spans, "test", "test_no_skip")
146+
assert skip_test_span.get_tag("test.status") == "skip"
147+
assert no_skip_test_span.get_tag("test.status") == "pass"
148+
assert rec.ret == 0
149+
150+
def test_skipif_with_string_keyword_condition(self):
151+
"""
152+
Test that the plugin does not break if `skipif` is used with a string condition passed as keyword, while
153+
advanced features are disabled by an external plugin.
154+
"""
155+
# TODO(vitor-de-araujo): string conditions are currently not evaluated.
156+
self.testdir.makepyfile(
157+
"""
158+
import pytest
159+
160+
@pytest.mark.skipif(condition="1 > 0", reason="")
161+
def test_skip():
162+
assert True
163+
"""
164+
)
165+
self.testdir.makepyfile(
166+
"""
167+
import pytest
168+
169+
@pytest.mark.skipif(condition="1 > 0", reason="because I can")
170+
def test_skip():
171+
assert True
172+
173+
@pytest.mark.skipif(condition="1 < 0", reason="because I can't")
174+
def test_no_skip():
175+
assert True
176+
"""
177+
)
178+
rec = self.inline_run("--ddtrace", "-p", "flaky")
179+
rec.assertoutcome(skipped=1, passed=1)
180+
spans = self.pop_spans()
181+
[skip_test_span] = _get_spans_from_list(spans, "test", "test_skip")
182+
[no_skip_test_span] = _get_spans_from_list(spans, "test", "test_no_skip")
183+
assert skip_test_span.get_tag("test.status") == "skip"
184+
assert no_skip_test_span.get_tag("test.status") == "pass"
185+
assert rec.ret == 0

0 commit comments

Comments
 (0)