@@ -66,12 +66,34 @@ def repl(matchobj):
66
66
return py .xml .raw (illegal_xml_re .sub (repl , py .xml .escape (arg )))
67
67
68
68
69
+ def merge_family (left , right ):
70
+ result = {}
71
+ for kl , vl in left .items ():
72
+ for kr , vr in right .items ():
73
+ if not isinstance (vl , list ):
74
+ raise TypeError (type (vl ))
75
+ result [kl ] = vl + vr
76
+ left .update (result )
77
+
78
+
79
+ families = {}
80
+ families ["_base" ] = {"testcase" : ["classname" , "name" ]}
81
+ families ["_base_legacy" ] = {"testcase" : ["file" , "line" , "url" ]}
82
+
83
+ # xUnit 1.x inherits legacy attributes
84
+ families ["xunit1" ] = families ["_base" ].copy ()
85
+ merge_family (families ["xunit1" ], families ["_base_legacy" ])
86
+
87
+ # xUnit 2.x uses strict base attributes
88
+ families ["xunit2" ] = families ["_base" ]
89
+
90
+
69
91
class _NodeReporter (object ):
70
92
def __init__ (self , nodeid , xml ):
71
-
72
93
self .id = nodeid
73
94
self .xml = xml
74
95
self .add_stats = self .xml .add_stats
96
+ self .family = self .xml .family
75
97
self .duration = 0
76
98
self .properties = []
77
99
self .nodes = []
@@ -119,8 +141,20 @@ def record_testreport(self, testreport):
119
141
self .attrs = attrs
120
142
self .attrs .update (existing_attrs ) # restore any user-defined attributes
121
143
144
+ # Preserve legacy testcase behavior
145
+ if self .family == "xunit1" :
146
+ return
147
+
148
+ # Filter out attributes not permitted by this test family.
149
+ # Including custom attributes because they are not valid here.
150
+ temp_attrs = {}
151
+ for key in self .attrs .keys ():
152
+ if key in families [self .family ]["testcase" ]:
153
+ temp_attrs [key ] = self .attrs [key ]
154
+ self .attrs = temp_attrs
155
+
122
156
def to_xml (self ):
123
- testcase = Junit .testcase (time = self .duration , ** self .attrs )
157
+ testcase = Junit .testcase (time = "%.3f" % self .duration , ** self .attrs )
124
158
testcase .append (self .make_properties_node ())
125
159
for node in self .nodes :
126
160
testcase .append (node )
@@ -269,16 +303,26 @@ def record_xml_attribute(request):
269
303
from _pytest .warning_types import PytestWarning
270
304
271
305
request .node .warn (PytestWarning ("record_xml_attribute is an experimental feature" ))
306
+
307
+ # Declare noop
308
+ def add_attr_noop (name , value ):
309
+ pass
310
+
311
+ attr_func = add_attr_noop
272
312
xml = getattr (request .config , "_xml" , None )
273
- if xml is not None :
274
- node_reporter = xml .node_reporter (request .node .nodeid )
275
- return node_reporter .add_attribute
276
- else :
277
313
278
- def add_attr_noop (name , value ):
279
- pass
314
+ if xml is not None and xml .family != "xunit1" :
315
+ request .node .warn (
316
+ PytestWarning (
317
+ "record_xml_attribute is incompatible with junit_family: "
318
+ "%s (use: legacy|xunit1)" % xml .family
319
+ )
320
+ )
321
+ elif xml is not None :
322
+ node_reporter = xml .node_reporter (request .node .nodeid )
323
+ attr_func = node_reporter .add_attribute
280
324
281
- return add_attr_noop
325
+ return attr_func
282
326
283
327
284
328
def pytest_addoption (parser ):
@@ -315,6 +359,11 @@ def pytest_addoption(parser):
315
359
"Duration time to report: one of total|call" ,
316
360
default = "total" ,
317
361
) # choices=['total', 'call'])
362
+ parser .addini (
363
+ "junit_family" ,
364
+ "Emit XML for schema: one of legacy|xunit1|xunit2" ,
365
+ default = "xunit1" ,
366
+ )
318
367
319
368
320
369
def pytest_configure (config ):
@@ -327,6 +376,7 @@ def pytest_configure(config):
327
376
config .getini ("junit_suite_name" ),
328
377
config .getini ("junit_logging" ),
329
378
config .getini ("junit_duration_report" ),
379
+ config .getini ("junit_family" ),
330
380
)
331
381
config .pluginmanager .register (config ._xml )
332
382
@@ -361,13 +411,15 @@ def __init__(
361
411
suite_name = "pytest" ,
362
412
logging = "no" ,
363
413
report_duration = "total" ,
414
+ family = "xunit1" ,
364
415
):
365
416
logfile = os .path .expanduser (os .path .expandvars (logfile ))
366
417
self .logfile = os .path .normpath (os .path .abspath (logfile ))
367
418
self .prefix = prefix
368
419
self .suite_name = suite_name
369
420
self .logging = logging
370
421
self .report_duration = report_duration
422
+ self .family = family
371
423
self .stats = dict .fromkeys (["error" , "passed" , "failure" , "skipped" ], 0 )
372
424
self .node_reporters = {} # nodeid -> _NodeReporter
373
425
self .node_reporters_ordered = []
@@ -376,6 +428,10 @@ def __init__(
376
428
self .open_reports = []
377
429
self .cnt_double_fail_tests = 0
378
430
431
+ # Replaces convenience family with real family
432
+ if self .family == "legacy" :
433
+ self .family = "xunit1"
434
+
379
435
def finalize (self , report ):
380
436
nodeid = getattr (report , "nodeid" , report )
381
437
# local hack to handle xdist report order
@@ -545,7 +601,7 @@ def pytest_sessionfinish(self):
545
601
name = self .suite_name ,
546
602
errors = self .stats ["error" ],
547
603
failures = self .stats ["failure" ],
548
- skips = self .stats ["skipped" ],
604
+ skipped = self .stats ["skipped" ],
549
605
tests = numtests ,
550
606
time = "%.3f" % suite_time_delta ,
551
607
).unicode (indent = 0 )
0 commit comments