0
# XML Document Construction
1
2
Low-level utilities for building XML test reports with proper JUnit schema compliance. Provides context management, counter tracking, and CDATA section handling for generating well-formed XML documents.
3
4
## Capabilities
5
6
### TestXMLBuilder Class
7
8
Main XML document builder that encapsulates rules for creating JUnit-compatible XML test reports with proper hierarchy and formatting.
9
10
```python { .api }
11
class TestXMLBuilder:
12
def __init__(self):
13
"""Initialize XML document builder with empty document."""
14
15
def begin_context(self, tag, name):
16
"""
17
Begin new XML context (testsuites, testsuite, or testcase).
18
19
Parameters:
20
- tag: str, XML tag name
21
- name: str, context name attribute
22
"""
23
24
def end_context(self):
25
"""
26
End current context and append to parent.
27
28
Returns:
29
- bool: True if context was ended, False if no context
30
"""
31
32
def current_context(self):
33
"""
34
Get current XML context.
35
36
Returns:
37
- TestXMLContext: current context or None
38
"""
39
40
def context_tag(self):
41
"""
42
Get tag name of current context.
43
44
Returns:
45
- str: current context tag name
46
"""
47
48
def append(self, tag, content, **kwargs):
49
"""
50
Append XML element with attributes to current context.
51
52
Parameters:
53
- tag: str, XML tag name
54
- content: str, element content
55
- **kwargs: element attributes
56
57
Returns:
58
- Element: created XML element
59
"""
60
61
def append_cdata_section(self, tag, content):
62
"""
63
Append element with CDATA content to current context.
64
65
Parameters:
66
- tag: str, XML tag name
67
- content: str, CDATA content
68
69
Returns:
70
- Element: created XML element
71
"""
72
73
def increment_counter(self, counter_name):
74
"""
75
Increment counter in current context and all parents.
76
77
Parameters:
78
- counter_name: str, counter name ('tests', 'errors', 'failures', 'skipped')
79
"""
80
81
def finish(self):
82
"""
83
End all open contexts and return formatted XML document.
84
85
Returns:
86
- bytes: pretty-printed XML document
87
"""
88
```
89
90
#### Usage Examples
91
92
**Basic XML Report Construction**
93
```python
94
from xmlrunner.builder import TestXMLBuilder
95
96
builder = TestXMLBuilder()
97
98
# Create testsuites root
99
builder.begin_context('testsuites', 'all_tests')
100
101
# Create testsuite
102
builder.begin_context('testsuite', 'test_module.TestClass')
103
104
# Add testcase
105
builder.begin_context('testcase', 'test_example')
106
builder.append('success', '', message='Test passed')
107
builder.increment_counter('tests')
108
builder.end_context() # End testcase
109
110
builder.end_context() # End testsuite
111
builder.end_context() # End testsuites
112
113
# Generate XML
114
xml_content = builder.finish()
115
print(xml_content.decode('utf-8'))
116
```
117
118
**Error Reporting with CDATA**
119
```python
120
builder = TestXMLBuilder()
121
builder.begin_context('testsuites', 'all_tests')
122
builder.begin_context('testsuite', 'test_module.TestClass')
123
builder.begin_context('testcase', 'test_failure')
124
125
# Add failure with traceback
126
failure_info = """Traceback (most recent call last):
127
File "test.py", line 10, in test_failure
128
self.assertTrue(False)
129
AssertionError: False is not true"""
130
131
builder.append('failure', '', type='AssertionError', message='False is not true')
132
builder.append_cdata_section('failure', failure_info)
133
134
builder.increment_counter('tests')
135
builder.increment_counter('failures')
136
137
xml_content = builder.finish()
138
```
139
140
### TestXMLContext Class
141
142
Represents XML document hierarchy context with automatic counter tracking and time measurement.
143
144
```python { .api }
145
class TestXMLContext:
146
def __init__(self, xml_doc, parent_context=None):
147
"""
148
Initialize XML context.
149
150
Parameters:
151
- xml_doc: Document, XML document instance
152
- parent_context: TestXMLContext or None, parent context
153
"""
154
155
def begin(self, tag, name):
156
"""
157
Begin context by creating XML element.
158
159
Parameters:
160
- tag: str, XML tag name
161
- name: str, name attribute value
162
"""
163
164
def end(self):
165
"""
166
End context and set timing/counter attributes.
167
168
Returns:
169
- Element: completed XML element
170
"""
171
172
def element_tag(self):
173
"""
174
Get tag name of this context's element.
175
176
Returns:
177
- str: tag name
178
"""
179
180
def increment_counter(self, counter_name):
181
"""
182
Increment counter if valid for this context type.
183
184
Parameters:
185
- counter_name: str, counter name
186
"""
187
188
def elapsed_time(self):
189
"""
190
Get formatted elapsed time for this context.
191
192
Returns:
193
- str: elapsed time in seconds (3 decimal places)
194
"""
195
196
def timestamp(self):
197
"""
198
Get ISO-8601 formatted timestamp for context end.
199
200
Returns:
201
- str: timestamp string
202
"""
203
204
# Attributes
205
xml_doc: Document
206
parent: TestXMLContext | None
207
element: Element
208
counters: dict[str, int]
209
```
210
211
#### Counter Rules
212
213
Different XML elements support different counters:
214
215
- **testsuites**: supports tests, errors, failures counters
216
- **testsuite**: supports tests, errors, failures, skipped counters
217
- **testcase**: no counters (individual test level)
218
219
### Text Processing Utilities
220
221
Functions for cleaning and processing text content for XML compatibility.
222
223
```python { .api }
224
def replace_nontext(text, replacement='\uFFFD'):
225
"""
226
Replace invalid XML characters in text.
227
228
Parameters:
229
- text: str, input text
230
- replacement: str, replacement character
231
232
Returns:
233
- str: cleaned text with invalid XML characters replaced
234
"""
235
236
# Constants
237
UTF8: str = 'UTF-8' # Default encoding for XML documents
238
INVALID_XML_1_0_UNICODE_RE: Pattern # Regex for invalid XML 1.0 characters
239
```
240
241
#### Usage Examples
242
243
**Text Cleaning**
244
```python
245
from xmlrunner.builder import replace_nontext
246
247
# Clean problematic characters from test output
248
raw_output = "Test output with \x00 null character"
249
clean_output = replace_nontext(raw_output)
250
# Result: "Test output with � null character"
251
252
# Use in XML building
253
builder.append_cdata_section('system-out', clean_output)
254
```
255
256
### CDATA Section Handling
257
258
The builder properly handles CDATA sections, including splitting content that contains the CDATA end marker.
259
260
```python
261
# Content with CDATA end marker is automatically split
262
problematic_content = "Some content ]]> with end marker ]]> inside"
263
builder.append_cdata_section('system-out', problematic_content)
264
265
# Results in multiple CDATA sections:
266
# <system-out><![CDATA[Some content ]]]]><![CDATA[> with end marker ]]]]><![CDATA[> inside]]></system-out>
267
```
268
269
### XML Schema Compliance
270
271
The builder generates XML that complies with JUnit schema requirements:
272
273
- Proper element hierarchy (testsuites → testsuite → testcase)
274
- Required attributes (name, tests, time, timestamp)
275
- Valid counter attributes based on element type
276
- Proper CDATA escaping for content with special characters
277
- UTF-8 encoding with XML declaration
278
279
### Performance Considerations
280
281
The XML builder is optimized for typical test report sizes:
282
283
- Uses DOM for structured building (suitable for reports with thousands of tests)
284
- Incremental counter updates avoid full tree traversal
285
- CDATA splitting handles edge cases without performance impact
286
- Memory usage scales linearly with test count
287
288
### Integration with Result System
289
290
The builder is used internally by _XMLTestResult but can be used independently:
291
292
```python
293
from xmlrunner.builder import TestXMLBuilder
294
from xml.dom.minidom import Document
295
296
# Direct usage for custom XML generation
297
builder = TestXMLBuilder()
298
299
# Build custom structure
300
builder.begin_context('testsuites', 'custom_run')
301
builder.begin_context('testsuite', 'my_tests')
302
303
for test_name, result in test_results.items():
304
builder.begin_context('testcase', test_name)
305
if result['passed']:
306
builder.increment_counter('tests')
307
else:
308
builder.append('failure', '', type='CustomError', message=result['error'])
309
builder.increment_counter('tests')
310
builder.increment_counter('failures')
311
builder.end_context()
312
313
xml_bytes = builder.finish()
314
```