Skip to content

Commit 0e8cedc

Browse files
Fix[pytest]: Skipped XFail tests now marked as skipped rather than xfail (#2732) (#2750)
* 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]> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
1 parent 4411849 commit 0e8cedc

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
@@ -162,24 +162,14 @@ def pytest_runtest_makereport(item, call):
162162

163163
try:
164164
result = outcome.get_result()
165-
166-
if hasattr(result, "wasxfail") or "xfail" in result.keywords:
167-
if result.skipped:
168-
# XFail tests that fail are recorded skipped by pytest
169-
span.set_tag(test.RESULT, test.Status.XFAIL.value)
170-
span.set_tag(test.XFAIL_REASON, result.wasxfail)
171-
else:
172-
span.set_tag(test.RESULT, test.Status.XPASS.value)
173-
if result.passed:
174-
# XPass (strict=False) are recorded passed by pytest
175-
span.set_tag(test.XFAIL_REASON, result.wasxfail)
176-
else:
177-
# XPass (strict=True) are recorded failed by pytest, longrepr contains reason
178-
span.set_tag(test.XFAIL_REASON, result.longrepr)
165+
xfail = hasattr(result, "wasxfail") or "xfail" in result.keywords
166+
has_skip_keyword = any(x in result.keywords for x in ["skip", "skipif", "skipped"])
179167

180168
if result.skipped:
181-
if hasattr(result, "wasxfail"):
169+
if xfail and not has_skip_keyword:
182170
# XFail tests that fail are recorded skipped by pytest, should be passed instead
171+
span.set_tag(test.RESULT, test.Status.XFAIL.value)
172+
span.set_tag(test.XFAIL_REASON, result.wasxfail)
183173
span.set_tag(test.STATUS, test.Status.PASS.value)
184174
else:
185175
span.set_tag(test.STATUS, test.Status.SKIP.value)
@@ -188,7 +178,15 @@ def pytest_runtest_makereport(item, call):
188178
span.set_tag(test.SKIP_REASON, reason)
189179
elif result.passed:
190180
span.set_tag(test.STATUS, test.Status.PASS.value)
181+
if xfail and not has_skip_keyword:
182+
# XPass (strict=False) are recorded passed by pytest
183+
span.set_tag(test.XFAIL_REASON, getattr(result, "wasxfail", "XFail"))
184+
span.set_tag(test.RESULT, test.Status.XPASS.value)
191185
else:
186+
if xfail and not has_skip_keyword:
187+
# XPass (strict=True) are recorded failed by pytest, longrepr contains reason
188+
span.set_tag(test.XFAIL_REASON, result.longrepr)
189+
span.set_tag(test.RESULT, test.Status.XPASS.value)
192190
raise RuntimeWarning(result)
193191
except Exception:
194192
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
@@ -212,7 +212,7 @@ def test_1(self, item):
212212
assert json.loads(spans[0].meta[test.PARAMETERS]) == {"arguments": {"item": "Could not encode"}, "metadata": {}}
213213

214214
def test_skip(self):
215-
"""Test parametrize case."""
215+
"""Test skip case."""
216216
py_file = self.testdir.makepyfile(
217217
"""
218218
import pytest
@@ -236,6 +236,62 @@ def test_body():
236236
assert spans[1].get_tag(test.STATUS) == test.Status.SKIP.value
237237
assert spans[1].get_tag(test.SKIP_REASON) == "body"
238238

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

256-
assert len(spans) == 1
316+
assert len(spans) == 2
257317
assert spans[0].get_tag(test.STATUS) == test.Status.PASS.value
258318
assert spans[0].get_tag(test.RESULT) == test.Status.XFAIL.value
259319
assert spans[0].get_tag(test.XFAIL_REASON) == "test should fail"
320+
assert spans[1].get_tag(test.STATUS) == test.Status.PASS.value
321+
assert spans[1].get_tag(test.RESULT) == test.Status.XFAIL.value
322+
assert spans[1].get_tag(test.XFAIL_REASON) == "test should xfail"
260323

261324
def test_xpass_not_strict(self):
262325
"""Test xpass (unexpected passing) with strict=False, should be marked as pass."""
@@ -265,19 +328,26 @@ def test_xpass_not_strict(self):
265328
import pytest
266329
267330
@pytest.mark.xfail(reason="test should fail")
268-
def test_should_fail():
331+
def test_should_fail_but_passes():
332+
pass
333+
334+
@pytest.mark.xfail(condition=True, reason="test should not xfail")
335+
def test_should_not_fail():
269336
pass
270337
"""
271338
)
272339
file_name = os.path.basename(py_file.strpath)
273340
rec = self.inline_run("--ddtrace", file_name)
274-
rec.assertoutcome(passed=1)
341+
rec.assertoutcome(passed=2)
275342
spans = self.pop_spans()
276343

277-
assert len(spans) == 1
344+
assert len(spans) == 2
278345
assert spans[0].get_tag(test.STATUS) == test.Status.PASS.value
279346
assert spans[0].get_tag(test.RESULT) == test.Status.XPASS.value
280347
assert spans[0].get_tag(test.XFAIL_REASON) == "test should fail"
348+
assert spans[1].get_tag(test.STATUS) == test.Status.PASS.value
349+
assert spans[1].get_tag(test.RESULT) == test.Status.XPASS.value
350+
assert spans[1].get_tag(test.XFAIL_REASON) == "test should not xfail"
281351

282352
def test_xpass_strict(self):
283353
"""Test xpass (unexpected passing) with strict=True, should be marked as fail."""
@@ -298,8 +368,8 @@ def test_should_fail():
298368
assert len(spans) == 1
299369
assert spans[0].get_tag(test.STATUS) == test.Status.FAIL.value
300370
assert spans[0].get_tag(test.RESULT) == test.Status.XPASS.value
301-
# Note: xpass (strict=True) does not mark the reason with result.wasxfail but into result.longrepr,
302-
# however this provides the entire traceback/error into longrepr.
371+
# Note: XFail (strict=True) does not mark the reason with result.wasxfail but into result.longrepr,
372+
# however it provides the entire traceback/error into longrepr.
303373
assert "test should fail" in spans[0].get_tag(test.XFAIL_REASON)
304374

305375
def test_tags(self):

0 commit comments

Comments
 (0)