Skip to content

Commit 7a02725

Browse files
Fix[pytest]: Skipped XFail tests now marked as skipped rather than xfail (#2732) (#2749)
* Fixed bug marking skipped xfails as xfail rather than skip and raising AttributeErrors * Merged XFail and result clauses * Changed variable name for clarity Co-authored-by: Kyle Verhoog <[email protected]> Co-authored-by: Brett Langdon <[email protected]> (cherry picked from commit c695018) Co-authored-by: Yun Kim <[email protected]>
1 parent 27f83db commit 7a02725

File tree

3 files changed

+96
-23
lines changed

3 files changed

+96
-23
lines changed

ddtrace/contrib/pytest/plugin.py

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -148,24 +148,14 @@ def pytest_runtest_makereport(item, call):
148148

149149
try:
150150
result = outcome.get_result()
151-
152-
if hasattr(result, "wasxfail") or "xfail" in result.keywords:
153-
if result.skipped:
154-
# XFail tests that fail are recorded skipped by pytest
155-
span.set_tag(test.RESULT, test.Status.XFAIL.value)
156-
span.set_tag(test.XFAIL_REASON, result.wasxfail)
157-
else:
158-
span.set_tag(test.RESULT, test.Status.XPASS.value)
159-
if result.passed:
160-
# XPass (strict=False) are recorded passed by pytest
161-
span.set_tag(test.XFAIL_REASON, result.wasxfail)
162-
else:
163-
# XPass (strict=True) are recorded failed by pytest, longrepr contains reason
164-
span.set_tag(test.XFAIL_REASON, result.longrepr)
151+
xfail = hasattr(result, "wasxfail") or "xfail" in result.keywords
152+
has_skip_keyword = any(x in result.keywords for x in ["skip", "skipif", "skipped"])
165153

166154
if result.skipped:
167-
if hasattr(result, "wasxfail"):
155+
if xfail and not has_skip_keyword:
168156
# XFail tests that fail are recorded skipped by pytest, should be passed instead
157+
span.set_tag(test.RESULT, test.Status.XFAIL.value)
158+
span.set_tag(test.XFAIL_REASON, result.wasxfail)
169159
span.set_tag(test.STATUS, test.Status.PASS.value)
170160
else:
171161
span.set_tag(test.STATUS, test.Status.SKIP.value)
@@ -174,7 +164,15 @@ def pytest_runtest_makereport(item, call):
174164
span.set_tag(test.SKIP_REASON, reason)
175165
elif result.passed:
176166
span.set_tag(test.STATUS, test.Status.PASS.value)
167+
if xfail and not has_skip_keyword:
168+
# XPass (strict=False) are recorded passed by pytest
169+
span.set_tag(test.XFAIL_REASON, getattr(result, "wasxfail", "XFail"))
170+
span.set_tag(test.RESULT, test.Status.XPASS.value)
177171
else:
172+
if xfail and not has_skip_keyword:
173+
# XPass (strict=True) are recorded failed by pytest, longrepr contains reason
174+
span.set_tag(test.XFAIL_REASON, result.longrepr)
175+
span.set_tag(test.RESULT, test.Status.XPASS.value)
178176
raise RuntimeWarning(result)
179177
except Exception:
180178
span.set_traceback()
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
fixes:
3+
- |
4+
Fixes a bug in the pytest plugin where xfail test cases in a test file with a
5+
module-wide skip raises attribute errors and are marked as xfail rather than skipped.

tests/contrib/pytest/test_pytest.py

Lines changed: 78 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ def test_1(self, item):
209209
assert json.loads(spans[0].meta[test.PARAMETERS]) == {"arguments": {"item": "Could not encode"}, "metadata": {}}
210210

211211
def test_skip(self):
212-
"""Test parametrize case."""
212+
"""Test skip case."""
213213
py_file = self.testdir.makepyfile(
214214
"""
215215
import pytest
@@ -233,6 +233,62 @@ def test_body():
233233
assert spans[1].get_tag(test.STATUS) == test.Status.SKIP.value
234234
assert spans[1].get_tag(test.SKIP_REASON) == "body"
235235

236+
def test_skip_module_with_xfail_cases(self):
237+
"""Test Xfail test cases for a module that is skipped entirely, which should be treated as skip tests."""
238+
py_file = self.testdir.makepyfile(
239+
"""
240+
import pytest
241+
242+
pytestmark = pytest.mark.skip(reason="reason")
243+
244+
@pytest.mark.xfail(reason="XFail Case")
245+
def test_xfail():
246+
pass
247+
248+
@pytest.mark.xfail(condition=False, reason="XFail Case")
249+
def test_xfail_conditional():
250+
pass
251+
"""
252+
)
253+
file_name = os.path.basename(py_file.strpath)
254+
rec = self.inline_run("--ddtrace", file_name)
255+
rec.assertoutcome(skipped=2)
256+
spans = self.pop_spans()
257+
258+
assert len(spans) == 2
259+
assert spans[0].get_tag(test.STATUS) == test.Status.SKIP.value
260+
assert spans[0].get_tag(test.SKIP_REASON) == "reason"
261+
assert spans[1].get_tag(test.STATUS) == test.Status.SKIP.value
262+
assert spans[1].get_tag(test.SKIP_REASON) == "reason"
263+
264+
def test_skipif_module(self):
265+
"""Test XFail test cases for a module that is skipped entirely with the skipif marker."""
266+
py_file = self.testdir.makepyfile(
267+
"""
268+
import pytest
269+
270+
pytestmark = pytest.mark.skipif(True, reason="reason")
271+
272+
@pytest.mark.xfail(reason="XFail")
273+
def test_xfail():
274+
pass
275+
276+
@pytest.mark.xfail(condition=False, reason="XFail Case")
277+
def test_xfail_conditional():
278+
pass
279+
"""
280+
)
281+
file_name = os.path.basename(py_file.strpath)
282+
rec = self.inline_run("--ddtrace", file_name)
283+
rec.assertoutcome(skipped=2)
284+
spans = self.pop_spans()
285+
286+
assert len(spans) == 2
287+
assert spans[0].get_tag(test.STATUS) == test.Status.SKIP.value
288+
assert spans[0].get_tag(test.SKIP_REASON) == "reason"
289+
assert spans[1].get_tag(test.STATUS) == test.Status.SKIP.value
290+
assert spans[1].get_tag(test.SKIP_REASON) == "reason"
291+
236292
def test_xfail_fails(self):
237293
"""Test xfail (expected failure) which fails, should be marked as pass."""
238294
py_file = self.testdir.makepyfile(
@@ -242,18 +298,25 @@ def test_xfail_fails(self):
242298
@pytest.mark.xfail(reason="test should fail")
243299
def test_should_fail():
244300
assert 0
301+
302+
@pytest.mark.xfail(condition=True, reason="test should xfail")
303+
def test_xfail_conditional():
304+
assert 0
245305
"""
246306
)
247307
file_name = os.path.basename(py_file.strpath)
248308
rec = self.inline_run("--ddtrace", file_name)
249309
# pytest records xfail as skipped
250-
rec.assertoutcome(skipped=1)
310+
rec.assertoutcome(skipped=2)
251311
spans = self.pop_spans()
252312

253-
assert len(spans) == 1
313+
assert len(spans) == 2
254314
assert spans[0].get_tag(test.STATUS) == test.Status.PASS.value
255315
assert spans[0].get_tag(test.RESULT) == test.Status.XFAIL.value
256316
assert spans[0].get_tag(test.XFAIL_REASON) == "test should fail"
317+
assert spans[1].get_tag(test.STATUS) == test.Status.PASS.value
318+
assert spans[1].get_tag(test.RESULT) == test.Status.XFAIL.value
319+
assert spans[1].get_tag(test.XFAIL_REASON) == "test should xfail"
257320

258321
def test_xpass_not_strict(self):
259322
"""Test xpass (unexpected passing) with strict=False, should be marked as pass."""
@@ -262,19 +325,26 @@ def test_xpass_not_strict(self):
262325
import pytest
263326
264327
@pytest.mark.xfail(reason="test should fail")
265-
def test_should_fail():
328+
def test_should_fail_but_passes():
329+
pass
330+
331+
@pytest.mark.xfail(condition=True, reason="test should not xfail")
332+
def test_should_not_fail():
266333
pass
267334
"""
268335
)
269336
file_name = os.path.basename(py_file.strpath)
270337
rec = self.inline_run("--ddtrace", file_name)
271-
rec.assertoutcome(passed=1)
338+
rec.assertoutcome(passed=2)
272339
spans = self.pop_spans()
273340

274-
assert len(spans) == 1
341+
assert len(spans) == 2
275342
assert spans[0].get_tag(test.STATUS) == test.Status.PASS.value
276343
assert spans[0].get_tag(test.RESULT) == test.Status.XPASS.value
277344
assert spans[0].get_tag(test.XFAIL_REASON) == "test should fail"
345+
assert spans[1].get_tag(test.STATUS) == test.Status.PASS.value
346+
assert spans[1].get_tag(test.RESULT) == test.Status.XPASS.value
347+
assert spans[1].get_tag(test.XFAIL_REASON) == "test should not xfail"
278348

279349
def test_xpass_strict(self):
280350
"""Test xpass (unexpected passing) with strict=True, should be marked as fail."""
@@ -295,8 +365,8 @@ def test_should_fail():
295365
assert len(spans) == 1
296366
assert spans[0].get_tag(test.STATUS) == test.Status.FAIL.value
297367
assert spans[0].get_tag(test.RESULT) == test.Status.XPASS.value
298-
# Note: xpass (strict=True) does not mark the reason with result.wasxfail but into result.longrepr,
299-
# however this provides the entire traceback/error into longrepr.
368+
# Note: XFail (strict=True) does not mark the reason with result.wasxfail but into result.longrepr,
369+
# however it provides the entire traceback/error into longrepr.
300370
assert "test should fail" in spans[0].get_tag(test.XFAIL_REASON)
301371

302372
def test_tags(self):

0 commit comments

Comments
 (0)