Skip to content

Commit 441eb15

Browse files
Yun-Kimbrettlangdonmergify[bot]
authored
feat(pytest): added xfail/xpass test marking functionality (#2416)
* Added xfail/xpass test marking functionality into pytest plugin. * Added changelog entry * Changelog entry Co-authored-by: Brett Langdon <[email protected]> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
1 parent a67d461 commit 441eb15

File tree

5 files changed

+100
-2
lines changed

5 files changed

+100
-2
lines changed

ddtrace/contrib/pytest/plugin.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,6 @@ def pytest_runtest_protocol(item, nextitem):
105105
markers = [marker.kwargs for marker in item.iter_markers(name="dd_tags")]
106106
for tags in markers:
107107
span.set_tags(tags)
108-
109108
_store_span(item, span)
110109

111110
yield
@@ -132,8 +131,27 @@ def pytest_runtest_makereport(item, call):
132131

133132
try:
134133
result = outcome.get_result()
134+
135+
if hasattr(result, "wasxfail") or "xfail" in result.keywords:
136+
if result.skipped:
137+
# XFail tests that fail are recorded skipped by pytest
138+
span.set_tag(test.RESULT, test.Status.XFAIL.value)
139+
span.set_tag(test.XFAIL_REASON, result.wasxfail)
140+
else:
141+
span.set_tag(test.RESULT, test.Status.XPASS.value)
142+
if result.passed:
143+
# XPass (strict=False) are recorded passed by pytest
144+
span.set_tag(test.XFAIL_REASON, result.wasxfail)
145+
else:
146+
# XPass (strict=True) are recorded failed by pytest, longrepr contains reason
147+
span.set_tag(test.XFAIL_REASON, result.longrepr)
148+
135149
if result.skipped:
136-
span.set_tag(test.STATUS, test.Status.SKIP.value)
150+
if hasattr(result, "wasxfail"):
151+
# XFail tests that fail are recorded skipped by pytest, should be passed instead
152+
span.set_tag(test.STATUS, test.Status.PASS.value)
153+
else:
154+
span.set_tag(test.STATUS, test.Status.SKIP.value)
137155
reason = _extract_reason(call)
138156
if reason is not None:
139157
span.set_tag(test.SKIP_REASON, reason)

ddtrace/ext/test.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
# Test Name
1515
NAME = TEST_NAME = "test.name"
1616

17+
# Pytest Result (XFail, XPass)
18+
RESULT = TEST_RESULT = "pytest.result"
19+
1720
# Skip Reason
1821
SKIP_REASON = TEST_SKIP_REASON = "test.skip_reason"
1922

@@ -29,8 +32,13 @@
2932
# Test Type
3033
TYPE = TEST_TYPE = "test.type"
3134

35+
# XFail Reason
36+
XFAIL_REASON = TEST_XFAIL_REASON = "pytest.xfail.reason"
37+
3238

3339
class Status(Enum):
3440
PASS = "pass"
3541
FAIL = "fail"
3642
SKIP = "skip"
43+
XFAIL = "xfail"
44+
XPASS = "xpass"

docs/spelling_wordlist.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,3 +125,4 @@ versioned
125125
vertica
126126
whitelist
127127
wsgi
128+
xfail
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
upgrade:
3+
- |
4+
The ddtrace pytest plugin can now label spans from test cases marked xfail with the tag "pytest.result"
5+
and the reason for being marked xfail under the tag "pytest.xfail.reason".

tests/contrib/pytest/test_pytest.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,72 @@ def test_body():
145145
assert spans[1].get_tag(test.STATUS) == test.Status.SKIP.value
146146
assert spans[1].get_tag(test.SKIP_REASON) == "body"
147147

148+
def test_xfail_fails(self):
149+
"""Test xfail (expected failure) which fails, should be marked as pass."""
150+
py_file = self.testdir.makepyfile(
151+
"""
152+
import pytest
153+
154+
@pytest.mark.xfail(reason="test should fail")
155+
def test_should_fail():
156+
assert 0
157+
"""
158+
)
159+
file_name = os.path.basename(py_file.strpath)
160+
rec = self.inline_run("--ddtrace", file_name)
161+
# pytest records xfail as skipped
162+
rec.assertoutcome(skipped=1)
163+
spans = self.pop_spans()
164+
165+
assert len(spans) == 1
166+
assert spans[0].get_tag(test.STATUS) == test.Status.PASS.value
167+
assert spans[0].get_tag(test.RESULT) == test.Status.XFAIL.value
168+
assert spans[0].get_tag(test.XFAIL_REASON) == "test should fail"
169+
170+
def test_xpass_not_strict(self):
171+
"""Test xpass (unexpected passing) with strict=False, should be marked as pass."""
172+
py_file = self.testdir.makepyfile(
173+
"""
174+
import pytest
175+
176+
@pytest.mark.xfail(reason="test should fail")
177+
def test_should_fail():
178+
pass
179+
"""
180+
)
181+
file_name = os.path.basename(py_file.strpath)
182+
rec = self.inline_run("--ddtrace", file_name)
183+
rec.assertoutcome(passed=1)
184+
spans = self.pop_spans()
185+
186+
assert len(spans) == 1
187+
assert spans[0].get_tag(test.STATUS) == test.Status.PASS.value
188+
assert spans[0].get_tag(test.RESULT) == test.Status.XPASS.value
189+
assert spans[0].get_tag(test.XFAIL_REASON) == "test should fail"
190+
191+
def test_xpass_strict(self):
192+
"""Test xpass (unexpected passing) with strict=True, should be marked as fail."""
193+
py_file = self.testdir.makepyfile(
194+
"""
195+
import pytest
196+
197+
@pytest.mark.xfail(reason="test should fail", strict=True)
198+
def test_should_fail():
199+
pass
200+
"""
201+
)
202+
file_name = os.path.basename(py_file.strpath)
203+
rec = self.inline_run("--ddtrace", file_name)
204+
rec.assertoutcome(failed=1)
205+
spans = self.pop_spans()
206+
207+
assert len(spans) == 1
208+
assert spans[0].get_tag(test.STATUS) == test.Status.FAIL.value
209+
assert spans[0].get_tag(test.RESULT) == test.Status.XPASS.value
210+
# Note: xpass (strict=True) does not mark the reason with result.wasxfail but into result.longrepr,
211+
# however this provides the entire traceback/error into longrepr.
212+
assert "test should fail" in spans[0].get_tag(test.XFAIL_REASON)
213+
148214
def test_tags(self):
149215
"""Test ddspan tags."""
150216
py_file = self.testdir.makepyfile(

0 commit comments

Comments
 (0)