1212from .config import SUBPROC_REPORT_PATH
1313
1414
15+ def _get_xfail_reason (item : pytest .Item ) -> str | None :
16+ """Extract xfail reason from marker if present.
17+
18+ Returns the xfail reason string, or None if no xfail marker.
19+ """
20+ xfail_marker = item .get_closest_marker ("xfail" )
21+ if not xfail_marker :
22+ return None
23+
24+ # Try to get reason from kwargs first, then from args
25+ reason = xfail_marker .kwargs .get ("reason" )
26+ if reason :
27+ return str (reason )
28+
29+ # If no reason in kwargs, try first positional arg
30+ if xfail_marker .args :
31+ return str (xfail_marker .args [0 ])
32+
33+ # Default if no reason provided
34+ return "xfail"
35+
36+
1537class _TestRecord (TypedDict , total = False ):
1638 """Structure for test phase results from subprocess."""
1739
@@ -25,7 +47,7 @@ class _TestRecord(TypedDict, total=False):
2547 keywords : list [str ]
2648 sections : list [tuple [str , str ]]
2749 user_properties : list [tuple [str , Any ]]
28- wasxfail : bool
50+ wasxfail : str # xfail reason string
2951
3052
3153def _format_crash_reason (returncode : int ) -> str :
@@ -82,8 +104,10 @@ def pytest_runtest_logreport(report: pytest.TestReport) -> None:
82104 "keywords" : list (report .keywords ),
83105 "sections" : getattr (report , "sections" , []), # captured logs, etc.
84106 "user_properties" : getattr (report , "user_properties" , []),
85- "wasxfail" : hasattr (report , "wasxfail" ),
86107 }
108+ # Store xfail reason if present
109+ if hasattr (report , "wasxfail" ):
110+ rec ["wasxfail" ] = str (report .wasxfail )
87111 with Path (path ).open ("a" , encoding = "utf-8" ) as f :
88112 f .write (json .dumps (rec ) + "\n " )
89113
@@ -99,7 +123,7 @@ def _emit_report(
99123 stderr : str = "" ,
100124 sections : list [tuple [str , str ]] | None = None ,
101125 user_properties : list [tuple [str , Any ]] | None = None ,
102- wasxfail : bool = False ,
126+ wasxfail : str | None = None ,
103127) -> None :
104128 """Emit a test report for a specific test phase."""
105129 call = pytest .CallInfo .from_call (lambda : None , when = when )
@@ -111,7 +135,7 @@ def _emit_report(
111135 rep .user_properties = user_properties
112136
113137 if wasxfail :
114- rep .wasxfail = "reason: xfail"
138+ rep .wasxfail = wasxfail
115139
116140 # For skipped tests, longrepr needs to be a tuple (path, lineno, reason)
117141 if outcome == "skipped" and longrepr :
@@ -159,15 +183,15 @@ def _emit_failure_for_items(
159183 empty output in later phases can clear the captured output from earlier phases.
160184 """
161185 for it in items :
162- xfail_marker = it . get_closest_marker ( "xfail" )
186+ xfail_reason = _get_xfail_reason ( it )
163187 _emit_report (it , when = "setup" , outcome = "passed" , stdout = stdout , stderr = stderr )
164- if xfail_marker :
188+ if xfail_reason :
165189 _emit_report (
166190 it ,
167191 when = "call" ,
168192 outcome = "skipped" ,
169193 longrepr = error_message ,
170- wasxfail = True ,
194+ wasxfail = xfail_reason ,
171195 stdout = stdout ,
172196 stderr = stderr ,
173197 )
0 commit comments