0
# Content Attachments
1
2
Attach arbitrary content (files, logs, screenshots, debug data) to test results for enhanced debugging and result reporting capabilities.
3
4
## Capabilities
5
6
### Core Content System
7
8
MIME-like content objects for attaching data to test results.
9
10
```python { .api }
11
class Content:
12
"""
13
A MIME-like Content object for test attachments.
14
15
Content objects can be serialized to bytes and provide
16
structured data attachment capabilities for test results.
17
"""
18
19
def __init__(self, content_type, get_bytes):
20
"""
21
Create a Content object.
22
23
Args:
24
content_type (ContentType): MIME content type
25
get_bytes (callable): Function returning byte iterator
26
"""
27
28
def iter_bytes(self):
29
"""
30
Iterate over bytestrings of the serialized content.
31
32
Yields:
33
bytes: Chunks of content data
34
"""
35
36
def iter_text(self):
37
"""
38
Iterate over text of the serialized content.
39
40
Only valid for text MIME types. Uses ISO-8859-1 if
41
no charset parameter is present.
42
43
Yields:
44
str: Text chunks
45
46
Raises:
47
ValueError: If content type is not text/*
48
"""
49
50
def as_text(self):
51
"""
52
Return all content as text string.
53
54
Loads all content into memory. For large content,
55
use iter_text() instead.
56
57
Returns:
58
str: Complete text content
59
60
Raises:
61
ValueError: If content type is not text/*
62
"""
63
64
def __eq__(self, other):
65
"""
66
Compare content objects for equality.
67
68
Args:
69
other (Content): Content to compare with
70
71
Returns:
72
bool: True if content types and data are equal
73
"""
74
75
class TracebackContent(Content):
76
"""
77
Content object for Python tracebacks.
78
79
Specialized content type for capturing and
80
attaching exception tracebacks to test results.
81
"""
82
83
def __init__(self, err, test):
84
"""
85
Create traceback content from exception.
86
87
Args:
88
err: Exception information (type, value, traceback)
89
test: Test case that generated the exception
90
"""
91
```
92
93
### Content Type System
94
95
MIME content type representation for proper content handling.
96
97
```python { .api }
98
class ContentType:
99
"""
100
A content type from IANA media types registry.
101
102
Represents MIME content types with parameters
103
for proper content handling and display.
104
"""
105
106
def __init__(self, primary_type, sub_type, parameters=None):
107
"""
108
Create a ContentType.
109
110
Args:
111
primary_type (str): Primary type (e.g., "text", "application")
112
sub_type (str): Sub type (e.g., "plain", "json")
113
parameters (dict): Optional type parameters
114
"""
115
116
@property
117
def type(self):
118
"""
119
Primary content type.
120
121
Returns:
122
str: Primary type (e.g., "text")
123
"""
124
125
@property
126
def subtype(self):
127
"""
128
Content subtype.
129
130
Returns:
131
str: Sub type (e.g., "plain")
132
"""
133
134
@property
135
def parameters(self):
136
"""
137
Content type parameters.
138
139
Returns:
140
dict: Parameters like charset, boundary, etc.
141
"""
142
143
def __repr__(self):
144
"""
145
String representation of content type.
146
147
Returns:
148
str: MIME type string (e.g., "text/plain; charset=utf8")
149
"""
150
151
# Predefined content types
152
JSON = ContentType("application", "json")
153
UTF8_TEXT = ContentType("text", "plain", {"charset": "utf8"})
154
```
155
156
### Content Creation Functions
157
158
Utility functions for creating common content types.
159
160
```python { .api }
161
def text_content(text):
162
"""
163
Create text content from string.
164
165
Args:
166
text (str): Text content to attach
167
168
Returns:
169
Content: Text content object with UTF-8 encoding
170
"""
171
172
def json_content(json_data):
173
"""
174
Create JSON content from Python objects.
175
176
Args:
177
json_data: Python object to serialize as JSON
178
179
Returns:
180
Content: JSON content object
181
"""
182
183
def content_from_file(path, content_type=None, chunk_size=DEFAULT_CHUNK_SIZE):
184
"""
185
Create content object from file.
186
187
Args:
188
path (str): File path to read
189
content_type (ContentType): Optional content type
190
chunk_size (int): Read chunk size
191
192
Returns:
193
Content: File content object
194
"""
195
196
def content_from_stream(stream, content_type, seek_offset=None,
197
seek_whence=0, chunk_size=DEFAULT_CHUNK_SIZE):
198
"""
199
Create content object from stream/file-like object.
200
201
Args:
202
stream: File-like object to read from
203
content_type (ContentType): Content type
204
seek_offset (int): Optional seek position
205
seek_whence (int): Seek reference point
206
chunk_size (int): Read chunk size
207
208
Returns:
209
Content: Stream content object
210
"""
211
212
def attach_file(test, name, path, content_type=None):
213
"""
214
Attach file content to test results.
215
216
Convenience function for attaching files to test cases
217
with automatic content type detection.
218
219
Args:
220
test: TestCase to attach content to
221
name (str): Attachment name/identifier
222
path (str): File path to attach
223
content_type (ContentType): Optional content type
224
"""
225
```
226
227
### Content Constants
228
229
Configuration constants for content handling.
230
231
```python { .api }
232
DEFAULT_CHUNK_SIZE = 4096
233
"""Default chunk size for reading content streams."""
234
235
STDOUT_LINE = "\nStdout:\n%s"
236
"""Format string for stdout content presentation."""
237
238
STDERR_LINE = "\nStderr:\n%s"
239
"""Format string for stderr content presentation."""
240
```
241
242
## Usage Examples
243
244
### Basic Content Attachment
245
246
```python
247
import testtools
248
from testtools.content import text_content, json_content
249
250
class MyTest(testtools.TestCase):
251
252
def test_with_debug_info(self):
253
# Attach text debug information
254
debug_info = "Processing started at 10:30 AM\nUser: alice\nMode: batch"
255
self.addDetail('debug_log', text_content(debug_info))
256
257
# Attach structured data
258
state = {
259
'user_id': 12345,
260
'session': 'abc-def-123',
261
'permissions': ['read', 'write'],
262
'timestamp': '2023-10-15T10:30:00Z'
263
}
264
self.addDetail('application_state', json_content(state))
265
266
# Perform test
267
result = process_user_request()
268
self.assertEqual(result.status, 'success')
269
270
def test_with_file_attachment(self):
271
# Generate test data file
272
test_data_file = '/tmp/test_data.csv'
273
with open(test_data_file, 'w') as f:
274
f.write('name,age,city\nAlice,30,Boston\nBob,25,Seattle\n')
275
276
# Attach the file to test results
277
testtools.content.attach_file(
278
self,
279
'input_data',
280
test_data_file,
281
testtools.content_type.ContentType('text', 'csv')
282
)
283
284
# Test file processing
285
result = process_csv_file(test_data_file)
286
self.assertEqual(len(result), 2)
287
```
288
289
### Content from Files and Streams
290
291
```python
292
import testtools
293
from testtools.content import content_from_file, content_from_stream
294
import io
295
296
class FileProcessingTest(testtools.TestCase):
297
298
def test_log_file_processing(self):
299
log_file = '/var/log/application.log'
300
301
# Attach log file content
302
if os.path.exists(log_file):
303
log_content = content_from_file(
304
log_file,
305
testtools.content_type.UTF8_TEXT,
306
chunk_size=8192
307
)
308
self.addDetail('application_logs', log_content)
309
310
# Test log processing
311
result = analyze_logs(log_file)
312
self.assertGreater(result.warning_count, 0)
313
314
def test_stream_processing(self):
315
# Create in-memory stream
316
data_stream = io.StringIO("line1\nline2\nline3\n")
317
318
# Attach stream content
319
stream_content = content_from_stream(
320
data_stream,
321
testtools.content_type.UTF8_TEXT,
322
seek_offset=0
323
)
324
self.addDetail('input_stream', stream_content)
325
326
# Test stream processing
327
data_stream.seek(0) # Reset for actual processing
328
result = process_stream(data_stream)
329
self.assertEqual(len(result), 3)
330
```
331
332
### Advanced Content Usage
333
334
```python
335
import testtools
336
from testtools.content import Content, ContentType
337
import gzip
338
import json
339
340
class AdvancedContentTest(testtools.TestCase):
341
342
def test_with_compressed_content(self):
343
# Create custom compressed content
344
original_data = json.dumps({
345
'large_dataset': list(range(10000)),
346
'metadata': {'compression': 'gzip'}
347
})
348
349
def get_compressed_bytes():
350
compressed = gzip.compress(original_data.encode('utf-8'))
351
yield compressed
352
353
compressed_content = Content(
354
ContentType('application', 'json', {'encoding': 'gzip'}),
355
get_compressed_bytes
356
)
357
358
self.addDetail('compressed_data', compressed_content)
359
360
# Test with large dataset
361
result = process_large_dataset()
362
self.assertIsNotNone(result)
363
364
def test_with_binary_content(self):
365
# Attach binary file (e.g., screenshot)
366
screenshot_path = '/tmp/test_screenshot.png'
367
if os.path.exists(screenshot_path):
368
binary_content = content_from_file(
369
screenshot_path,
370
ContentType('image', 'png')
371
)
372
self.addDetail('failure_screenshot', binary_content)
373
374
# Test UI functionality
375
result = interact_with_ui()
376
self.assertTrue(result.success)
377
378
def test_multiple_content_types(self):
379
# Attach multiple types of debugging content
380
381
# Configuration data
382
config = {'debug': True, 'timeout': 30}
383
self.addDetail('configuration', json_content(config))
384
385
# Log excerpts
386
recent_logs = get_recent_log_entries()
387
self.addDetail('recent_logs', text_content('\n'.join(recent_logs)))
388
389
# Performance metrics
390
metrics = measure_performance()
391
self.addDetail('performance_metrics', json_content(metrics))
392
393
# Environment information
394
env_info = {
395
'python_version': sys.version,
396
'platform': platform.platform(),
397
'memory_usage': get_memory_usage()
398
}
399
self.addDetail('environment', json_content(env_info))
400
401
# Run the actual test
402
result = complex_operation()
403
self.assertEqual(result.status, 'completed')
404
```
405
406
### Content in Test Fixtures
407
408
```python
409
import testtools
410
from testtools.content import text_content
411
from fixtures import Fixture
412
413
class LoggingFixture(Fixture):
414
"""Fixture that captures logs and attaches them to tests."""
415
416
def __init__(self):
417
super().__init__()
418
self.log_entries = []
419
420
def _setUp(self):
421
# Set up log capture
422
self.original_handler = setup_log_capture(self.log_entries)
423
self.addCleanup(restore_log_handler, self.original_handler)
424
425
def get_log_content(self):
426
"""Get captured logs as content."""
427
return text_content('\n'.join(self.log_entries))
428
429
class MyIntegrationTest(testtools.TestCase):
430
431
def test_with_log_capture(self):
432
# Use logging fixture
433
log_fixture = self.useFixture(LoggingFixture())
434
435
# Perform operations that generate logs
436
service = ExternalService()
437
result = service.complex_operation()
438
439
# Attach captured logs
440
self.addDetail('operation_logs', log_fixture.get_log_content())
441
442
# Verify results
443
self.assertEqual(result.status, 'success')
444
self.assertIn('INFO', '\n'.join(log_fixture.log_entries))
445
```
446
447
### Content with Test Result Analysis
448
449
```python
450
import testtools
451
from testtools.content import json_content
452
453
class TestResultAnalyzer(testtools.TestResult):
454
"""Custom result class that analyzes attached content."""
455
456
def __init__(self):
457
super().__init__()
458
self.content_analysis = {}
459
460
def addSuccess(self, test, details=None):
461
super().addSuccess(test, details)
462
if details:
463
self.analyze_content(test.id(), details)
464
465
def addFailure(self, test, err, details=None):
466
super().addFailure(test, err, details)
467
if details:
468
self.analyze_content(test.id(), details)
469
470
def analyze_content(self, test_id, details):
471
"""Analyze attached content for insights."""
472
analysis = {}
473
474
for name, content in details.items():
475
if content.content_type.type == 'application' and \
476
content.content_type.subtype == 'json':
477
# Analyze JSON content
478
try:
479
data = json.loads(content.as_text())
480
analysis[name] = {
481
'type': 'json',
482
'keys': list(data.keys()) if isinstance(data, dict) else None,
483
'size': len(str(data))
484
}
485
except:
486
analysis[name] = {'type': 'json', 'error': 'parse_failed'}
487
488
elif content.content_type.type == 'text':
489
# Analyze text content
490
text = content.as_text()
491
analysis[name] = {
492
'type': 'text',
493
'lines': len(text.split('\n')),
494
'chars': len(text),
495
'contains_error': 'ERROR' in text.upper()
496
}
497
498
self.content_analysis[test_id] = analysis
499
500
def get_content_summary(self):
501
"""Get summary of all attached content."""
502
return json_content(self.content_analysis)
503
504
# Usage
505
result = TestResultAnalyzer()
506
suite.run(result)
507
508
# Get content analysis
509
content_summary = result.get_content_summary()
510
print("Content analysis:", content_summary.as_text())
511
```